From 266c67d4d328cddd6ea21d5c76dd77bbf8b9ace5 Mon Sep 17 00:00:00 2001 From: Hassaan Mohsin Date: Tue, 30 Jun 2026 17:07:21 -0700 Subject: [PATCH 1/8] experiment with `into` type conversion --- forja/src/P.scala | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 forja/src/P.scala diff --git a/forja/src/P.scala b/forja/src/P.scala new file mode 100644 index 0000000..6b7af53 --- /dev/null +++ b/forja/src/P.scala @@ -0,0 +1,20 @@ +package forja + +into opaque type P[T] = P.Erased + +object P: + trait Meta[T] + object Meta: + def derived[T]: Meta[T] = ??? + end Meta + + given [T] => (meta: Meta[T]) => Conversion[T, P[T]]: + def apply(x: T): Erased = ??? + end given + + trait Erased + + extension [T] (p: P[T]) + def fixpoint(fn: PartialFunction[T, T]): P[T] = ??? + end extension +end P From 6b61aecbe38a1ea76bcfde47cd4903388844d558 Mon Sep 17 00:00:00 2001 From: Finn Hackett Date: Sat, 4 Jul 2026 02:02:29 +0000 Subject: [PATCH 2/8] try to implement P.Meta with macros --- build.mill | 3 +- forja/src/P.scala | 206 +++++++++++++++++++++++++++++++++++++++++- forja/src/PTest.scala | 10 ++ 3 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 forja/src/PTest.scala diff --git a/build.mill b/build.mill index 9e7b38c..ac4ca98 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", @@ -21,6 +21,7 @@ trait ForjaModule extends ScalaModule, ScalafixModule: "-Xcheck-macros", "-explain-cyclic", "-preview", + "-experimental", ) override def forkArgs = super.forkArgs() ++ Seq( // TODO: fix when Scala 3.8? diff --git a/forja/src/P.scala b/forja/src/P.scala index 6b7af53..b4291e0 100644 --- a/forja/src/P.scala +++ b/forja/src/P.scala @@ -1,20 +1,216 @@ package forja +import scala.quoted.Quotes +import scala.quoted.Type +import scala.quoted.quotes +import scala.quoted.Expr + into opaque type P[T] = P.Erased object P: - trait Meta[T] + trait Meta[T]: + def erase(t: T): Erased + end Meta + object Meta: - def derived[T]: Meta[T] = ??? + inline def derived[T]: Meta[T] = ${ derivedImpl } + + private def derivedImpl[T : Type](using Quotes): Expr[Meta[T]] = + import quotes.reflect.* + + val tp = TypeRepr.of[T] + val classSym = tp.classSymbol.get + if classSym.flags.is(Flags.Sealed) && classSym.flags.is(Flags.Abstract) + then + // TODO: the derives clause only applies to the type on which it is declared. + // So, if you say a whole enum derives P.Meta, we get one instance for the overall enum type. + // Two interesting questions: + // 1. We are Meta[T] and we got given U <: T; do we have to do runtime type dispatch every time? + // 2. How to accept being truly given a supertype, so passing T above also works. + ??? + else if classSym.flags.is(Flags.Case) + then + val instSym = Symbol.newMethod( + Symbol.spliceOwner, + "inst", + MethodType( + classSym.caseFields.map(fld => s"fld$$${fld.name}") + )( + { _ => + classSym.caseFields.map(fld => fld.info) + }, { _ => + TypeRepr.of[Erased] + }, + ), + ) + + Block( + List( + DefDef( + instSym, + { + case List(instArgs) => + val freshCls = Symbol.newClass( + owner = instSym, + name = s"Erased${classSym.name}", + parents = List(TypeRepr.of[Object], TypeRepr.of[Erased]), + decls = sym => { + List( + Symbol.newMethod(sym, "rewriteInner", MethodType(List("fn"))(_ => List(TypeRepr.of[Erased => Erased]), _ => TypeRepr.of[Erased])), + ) ::: + classSym.caseFields.map: fld => + Symbol.newVal( + sym, + s"fld$$${fld.name}", + fld.info, + Flags.EmptyFlags, + Symbol.noSymbol, + ) + }, + selfType = None, + ) + Some { + Block( + List( + ClassDef( + cls = freshCls, + parents = List(TypeTree.of[Object], TypeTree.of[Erased]), + body = List( + DefDef(freshCls.methodMember("rewriteInner").head, { + case List(List(fn)) => + val sym = freshCls.methodMember("rewriteInner").head + given Quotes = sym.asQuotes + Some: + ValDef.let( + sym, + freshCls.declaredFields.map { fld => + fld.info.asType match + case '[ft] => + Expr.summon[RewriteInner[ft]] match + case Some(rwInner) => + '{ + $rwInner.rewriteInner( + ${ This(freshCls).select(fld).asExprOf[ft] }, + ${fn.asExprOf[Erased => Erased]}, + ) + }.asTerm + case None => + report.errorAndAbort(s"no rewrite rule for ${TypeRepr.of[ft].show}") + end match + end match + }, + ) { binds => + val didChangeExpr = freshCls.declaredFields + .zip(binds) + .map: (fld, bind) => + '{ ${This(freshCls).select(fld).asExpr}.asInstanceOf[AnyRef] ne ${bind.asExpr}.asInstanceOf[AnyRef] } + .foldLeft('{ false })((l, r) => '{ $l || $r }) + end didChangeExpr + '{ + val didChange = $didChangeExpr + if didChange + then ${ + Ref(instSym) + .appliedToArgs(freshCls.declaredFields.map(This(freshCls).select)) + .asExprOf[Erased] + } + else ${This(freshCls).asExprOf[Erased]} + } + .asTerm + } + case _ => ??? + }) + ) ::: freshCls.declaredFields.zip(instArgs).map: (fld, instArg) => + ValDef.apply(fld, Some(instArg.asExpr.asTerm)), + ) + ), + New(TypeTree.ref(freshCls)) + .select(freshCls.primaryConstructor) + .appliedToArgs(Nil), + ) + } + case _ => ??? + }, + ), + ), + '{ + new Meta[T]: + def erase(t: T): Erased = ${ + Ref(instSym) + .appliedToArgs: + classSym.caseFields + .map: fld => + '{ t }.asTerm.select(fld) + .asExprOf[Erased] + } + end new + } + .asTerm + ) + .asExprOf[Meta[T]] + else + report.errorAndAbort(s"${tp.show} is neither a case class nor a sealed abstract type") + end if + end derivedImpl end Meta given [T] => (meta: Meta[T]) => Conversion[T, P[T]]: - def apply(x: T): Erased = ??? + def apply(x: T): P[T] = meta.erase(x) end given - trait Erased + trait Erased: + def rewriteInner(fn: Erased => Erased): Erased + end Erased extension [T] (p: P[T]) - def fixpoint(fn: PartialFunction[T, T]): P[T] = ??? + def fixpoint(fn: [U] => P[U] => P[U]): P[T] = ??? end extension + + trait RewriteInner[T]: + def rewriteInner(t: T, fn: Erased => Erased): T + end RewriteInner + + object RewriteInner: + given RewriteInner[Byte]: + inline def rewriteInner(t: Byte, fn: Erased => Erased): Byte = t + end given + + given RewriteInner[Char]: + inline def rewriteInner(t: Char, fn: Erased => Erased): Char = t + end given + + given RewriteInner[Short]: + inline def rewriteInner(t: Short, fn: Erased => Erased): Short = t + end given + + given RewriteInner[Int]: + inline def rewriteInner(t: Int, fn: Erased => Erased): Int = t + end given + + given RewriteInner[Long]: + inline def rewriteInner(t: Long, fn: Erased => Erased): Long = t + end given + + given RewriteInner[Float]: + inline def rewriteInner(t: Float, fn: Erased => Erased): Float = t + end given + + given RewriteInner[Double]: + inline def rewriteInner(t: Double, fn: Erased => Erased): Double = t + end given + + given RewriteInner[String]: + inline def rewriteInner(t: String, fn: Erased => Erased): String = t + end given + + given [T] => RewriteInner[P[T]]: + inline def rewriteInner(t: P[T], fn: Erased => Erased): Erased = t.rewriteInner(fn) + end given + + given [T <: AnyRef] => (rewriteElem: RewriteInner[T]) => RewriteInner[List[T]]: + def rewriteInner(t: List[T], fn: Erased => Erased): List[T] = + t.mapConserve(rewriteElem.rewriteInner(_, fn)) + end rewriteInner + end given + end RewriteInner end P diff --git a/forja/src/PTest.scala b/forja/src/PTest.scala new file mode 100644 index 0000000..dc999fc --- /dev/null +++ b/forja/src/PTest.scala @@ -0,0 +1,10 @@ +package forja + +object PTest: + final case class Foo(x: Int, y: String) derives P.Meta + + def main(args: Array[String]): Unit = + println(summon[P.Meta[Foo]].erase(Foo(42, "43")).rewriteInner(identity)) + val foo: P[Foo] = Foo(43, "44") + end main +end PTest From 4c83f73d6757d2acb69069ba6ffe4f17fc1a794b Mon Sep 17 00:00:00 2001 From: Finn Hackett Date: Sat, 4 Jul 2026 07:08:17 +0000 Subject: [PATCH 3/8] better P.Meta codegen (yes we can have constructors!) --- forja/src/P.scala | 236 ++++++++++++++++++++++-------------------- forja/src/PTest.scala | 1 + 2 files changed, 126 insertions(+), 111 deletions(-) diff --git a/forja/src/P.scala b/forja/src/P.scala index b4291e0..27142cc 100644 --- a/forja/src/P.scala +++ b/forja/src/P.scala @@ -30,122 +30,136 @@ object P: ??? else if classSym.flags.is(Flags.Case) then - val instSym = Symbol.newMethod( + val erasedSym = Symbol.newClass( + owner = Symbol.spliceOwner, + name = s"Erased${classSym.name}", + parents = _ => List(TypeRepr.of[Object], TypeRepr.of[Erased]), + decls = sym => { + List( + Symbol.newMethod(sym, "rewriteInner", MethodType(List("fn"))(_ => List(TypeRepr.of[Erased => Erased]), _ => TypeRepr.of[Erased])), + ) + }, + selfType = None, + clsFlags = Flags.EmptyFlags, + clsPrivateWithin = Symbol.noSymbol, + clsAnnotations = Nil, + conMethodType = { resultTpe => + MethodType(classSym.caseFields.map(fld => s"fld$$${fld.name}"))( + _ => classSym.caseFields.map(_.info), + _ => resultTpe, + ) + }, + conFlags = Flags.EmptyFlags, + conPrivateWithin = Symbol.noSymbol, + conParamFlags = List(classSym.caseFields.map(_ => Flags.ParamAccessor)), + conParamPrivateWithins = List(classSym.caseFields.map(_ => Symbol.noSymbol)), + ) + val metaSym = Symbol.newClass( Symbol.spliceOwner, - "inst", - MethodType( - classSym.caseFields.map(fld => s"fld$$${fld.name}") - )( - { _ => - classSym.caseFields.map(fld => fld.info) - }, { _ => - TypeRepr.of[Erased] - }, - ), + s"Meta${classSym.name}", + List(TypeRepr.of[Object], TypeRepr.of[Meta[T]]), + { sym => + List( + Symbol.newMethod( + sym, + "erase", + MethodType(List("t"))( + { sym => List(TypeRepr.of[T]) }, + { sym => TypeRepr.of[Erased] }, + ), + Flags.Inline & Flags.Method, + Symbol.noSymbol, + ), + ) + }, + None, ) Block( List( - DefDef( - instSym, - { - case List(instArgs) => - val freshCls = Symbol.newClass( - owner = instSym, - name = s"Erased${classSym.name}", - parents = List(TypeRepr.of[Object], TypeRepr.of[Erased]), - decls = sym => { - List( - Symbol.newMethod(sym, "rewriteInner", MethodType(List("fn"))(_ => List(TypeRepr.of[Erased => Erased]), _ => TypeRepr.of[Erased])), - ) ::: - classSym.caseFields.map: fld => - Symbol.newVal( - sym, - s"fld$$${fld.name}", - fld.info, - Flags.EmptyFlags, - Symbol.noSymbol, - ) - }, - selfType = None, - ) - Some { - Block( - List( - ClassDef( - cls = freshCls, - parents = List(TypeTree.of[Object], TypeTree.of[Erased]), - body = List( - DefDef(freshCls.methodMember("rewriteInner").head, { - case List(List(fn)) => - val sym = freshCls.methodMember("rewriteInner").head - given Quotes = sym.asQuotes - Some: - ValDef.let( - sym, - freshCls.declaredFields.map { fld => - fld.info.asType match - case '[ft] => - Expr.summon[RewriteInner[ft]] match - case Some(rwInner) => - '{ - $rwInner.rewriteInner( - ${ This(freshCls).select(fld).asExprOf[ft] }, - ${fn.asExprOf[Erased => Erased]}, - ) - }.asTerm - case None => - report.errorAndAbort(s"no rewrite rule for ${TypeRepr.of[ft].show}") - end match - end match - }, - ) { binds => - val didChangeExpr = freshCls.declaredFields - .zip(binds) - .map: (fld, bind) => - '{ ${This(freshCls).select(fld).asExpr}.asInstanceOf[AnyRef] ne ${bind.asExpr}.asInstanceOf[AnyRef] } - .foldLeft('{ false })((l, r) => '{ $l || $r }) - end didChangeExpr - '{ - val didChange = $didChangeExpr - if didChange - then ${ - Ref(instSym) - .appliedToArgs(freshCls.declaredFields.map(This(freshCls).select)) - .asExprOf[Erased] - } - else ${This(freshCls).asExprOf[Erased]} - } - .asTerm - } - case _ => ??? - }) - ) ::: freshCls.declaredFields.zip(instArgs).map: (fld, instArg) => - ValDef.apply(fld, Some(instArg.asExpr.asTerm)), - ) - ), - New(TypeTree.ref(freshCls)) - .select(freshCls.primaryConstructor) - .appliedToArgs(Nil), - ) - } - case _ => ??? - }, + ClassDef( + cls = erasedSym, + parents = List(TypeTree.of[Object], TypeTree.of[Erased]), + body = List( + DefDef(erasedSym.declaredMethod("rewriteInner").head, { + case List(List(fn)) => + val sym = erasedSym.methodMember("rewriteInner").head + given Quotes = sym.asQuotes + Some: + ValDef.let( + sym, + erasedSym.declaredFields.map { fld => + fld.info.asType match + case '[ft] => + Expr.summon[RewriteInner[ft]] match + case Some(rwInner) => + '{ + $rwInner.rewriteInner( + ${ This(erasedSym).select(fld).asExprOf[ft] }, + ${ fn.asExprOf[Erased => Erased] }, + ) + }.asTerm + case None => + report.errorAndAbort(s"no rewrite rule for ${TypeRepr.of[ft].show}") + end match + end match + }, + ) { binds => + val didChangeExpr = erasedSym.declaredFields + .zip(binds) + .map: (fld, bind) => + This(erasedSym).select(fld).asExpr match + case '{ $nv: AnyRef } => + '{ $nv ne ${ bind.asExpr }.asInstanceOf[AnyRef] } + case '{ $nv: nvT } => + '{ $nv != ${ bind.asExpr} } + end match + .foldLeft('{ false })((l, r) => '{ $l || $r }) + end didChangeExpr + '{ + val didChange = $didChangeExpr + if didChange + then ${ + New(TypeIdent(erasedSym)) + .select(erasedSym.primaryConstructor) + .appliedToArgs(erasedSym.declaredFields.map(This(erasedSym).select)) + .asExprOf[Erased] + } + else ${This(erasedSym).asExprOf[Erased]} + } + .asTerm + } + case _ => ??? + }) + ) ), + ClassDef( + metaSym, + List(TypeTree.of[Object], TypeTree.of[Meta[T]]), + List( + DefDef( + metaSym.declaredMethod("erase").head, + { + case List(List(t)) => + Some: + New(TypeIdent(erasedSym)) + .select(erasedSym.primaryConstructor) + .appliedToArgs: + classSym.caseFields + .map: fld => + t + .asExpr + .asTerm + .select(fld) + case _ => ??? + }, + ) + ), + ) ), - '{ - new Meta[T]: - def erase(t: T): Erased = ${ - Ref(instSym) - .appliedToArgs: - classSym.caseFields - .map: fld => - '{ t }.asTerm.select(fld) - .asExprOf[Erased] - } - end new - } - .asTerm + New(TypeIdent(metaSym)) + .select(metaSym.primaryConstructor) + .appliedToArgs(Nil) ) .asExprOf[Meta[T]] else @@ -154,8 +168,8 @@ object P: end derivedImpl end Meta - given [T] => (meta: Meta[T]) => Conversion[T, P[T]]: - def apply(x: T): P[T] = meta.erase(x) + given [T, U <: T] => (meta: Meta[T]) => Conversion[U, P[T]]: + def apply(x: U): P[T] = meta.erase(x) end given trait Erased: diff --git a/forja/src/PTest.scala b/forja/src/PTest.scala index dc999fc..2fc5458 100644 --- a/forja/src/PTest.scala +++ b/forja/src/PTest.scala @@ -6,5 +6,6 @@ object PTest: def main(args: Array[String]): Unit = println(summon[P.Meta[Foo]].erase(Foo(42, "43")).rewriteInner(identity)) val foo: P[Foo] = Foo(43, "44") + println(foo) end main end PTest From 8bac3e10a1d2096c3b543f6d15dc57cdf187c736 Mon Sep 17 00:00:00 2001 From: Finn Hackett Date: Sun, 5 Jul 2026 00:40:27 +0000 Subject: [PATCH 4/8] finish simpler implementation of P.Meta for sealed supertypes, make better error messages --- forja/src/P.scala | 218 ++++++++++++++++++++++++++---------------- forja/src/PTest.scala | 10 ++ 2 files changed, 147 insertions(+), 81 deletions(-) diff --git a/forja/src/P.scala b/forja/src/P.scala index 27142cc..39b02dd 100644 --- a/forja/src/P.scala +++ b/forja/src/P.scala @@ -4,12 +4,25 @@ import scala.quoted.Quotes import scala.quoted.Type import scala.quoted.quotes import scala.quoted.Expr +import scala.util.NotGiven +import scala.quoted.Varargs +import scala.annotation.implicitNotFound +import scala.compiletime.summonInline into opaque type P[T] = P.Erased object P: + @implicitNotFound("${T} or its sealed supertype needs to derive P.Meta") trait Meta[T]: def erase(t: T): Erased + // Gotcha: this could more sensibly convert to P[T], but some interaction of the opaque type + // being in scope here and the macro definitions below breaks things. The error looks like + // the macro hardcodes Erased where I lexically wrote P[T], causing the compiler to notice later + // on that def conversion: Conversion[T, Erased] would not implement the P[T] version. + // I guess this is because the macro is expanded in another file... + // Instead, we use Conversion's variance to replace Erased with P[T] during the given that calls + // this method. + def conversion: Conversion[T, Erased] end Meta object Meta: @@ -18,21 +31,70 @@ object P: private def derivedImpl[T : Type](using Quotes): Expr[Meta[T]] = import quotes.reflect.* - val tp = TypeRepr.of[T] - val classSym = tp.classSymbol.get - if classSym.flags.is(Flags.Sealed) && classSym.flags.is(Flags.Abstract) + val tpTree = TypeTree.of[T] + println(s"process: ${tpTree.show}") + if tpTree.symbol.flags.is(Flags.Sealed) && tpTree.symbol.flags.is(Flags.Abstract) then - // TODO: the derives clause only applies to the type on which it is declared. - // So, if you say a whole enum derives P.Meta, we get one instance for the overall enum type. - // Two interesting questions: - // 1. We are Meta[T] and we got given U <: T; do we have to do runtime type dispatch every time? - // 2. How to accept being truly given a supertype, so passing T above also works. - ??? - else if classSym.flags.is(Flags.Case) + println(s"abstract: ${tpTree.show}") + '{ + final class GeneratedMeta extends Conversion[T, Erased], Meta[T]: + // TODO: array is simpler, but if it's ever a bottleneck then we can generate a synthetic fieldset + private val subMetas: Array[Meta[?]] = + ${ + val childMetas = tpTree.symbol.children.map: childSym => + childSym.typeRef.asType match + case '[ch] => '{ derived[ch] } + end childMetas + '{ Array(${Varargs(childMetas)}*) } + } + end subMetas + + def erase(t: T): Erased = + val subMetasProxy = subMetas + ${ + Match( + '{t}.asTerm, + tpTree.symbol.children.zipWithIndex.map { (childSym, idx) => + def branchBody = + '{ subMetasProxy(${Expr(idx)}).asInstanceOf[Meta[T]].erase(t) }.asTerm + end branchBody + if childSym.isTerm + then + CaseDef( + Ref(childSym), + None, + branchBody, + ) + else if childSym.isType + then + TypeTree.ref(childSym).tpe.asType match + case '[cT] => + CaseDef( + Typed(Wildcard(), TypeTree.of[cT]), + None, + branchBody, + ) + else + report.errorAndAbort(s"neither a term nor a type $childSym") + end if + }, + ) + .asExprOf[Erased] + } + end erase + + def conversion: Conversion[T, Erased] = this + + def apply(t: T): Erased = erase(t) + end GeneratedMeta + + new GeneratedMeta + } + else if tpTree.symbol.flags.is(Flags.Case) then val erasedSym = Symbol.newClass( owner = Symbol.spliceOwner, - name = s"Erased${classSym.name}", + name = s"Erased${tpTree.symbol.name}", parents = _ => List(TypeRepr.of[Object], TypeRepr.of[Erased]), decls = sym => { List( @@ -44,35 +106,15 @@ object P: clsPrivateWithin = Symbol.noSymbol, clsAnnotations = Nil, conMethodType = { resultTpe => - MethodType(classSym.caseFields.map(fld => s"fld$$${fld.name}"))( - _ => classSym.caseFields.map(_.info), + MethodType(tpTree.symbol.caseFields.map(fld => s"fld$$${fld.name}"))( + _ => tpTree.symbol.caseFields.map(fld => Ref(fld).tpe.widen), _ => resultTpe, ) }, conFlags = Flags.EmptyFlags, conPrivateWithin = Symbol.noSymbol, - conParamFlags = List(classSym.caseFields.map(_ => Flags.ParamAccessor)), - conParamPrivateWithins = List(classSym.caseFields.map(_ => Symbol.noSymbol)), - ) - val metaSym = Symbol.newClass( - Symbol.spliceOwner, - s"Meta${classSym.name}", - List(TypeRepr.of[Object], TypeRepr.of[Meta[T]]), - { sym => - List( - Symbol.newMethod( - sym, - "erase", - MethodType(List("t"))( - { sym => List(TypeRepr.of[T]) }, - { sym => TypeRepr.of[Erased] }, - ), - Flags.Inline & Flags.Method, - Symbol.noSymbol, - ), - ) - }, - None, + conParamFlags = List(tpTree.symbol.caseFields.map(_ => Flags.ParamAccessor)), + conParamPrivateWithins = List(tpTree.symbol.caseFields.map(_ => Symbol.noSymbol)), ) Block( @@ -88,22 +130,27 @@ object P: Some: ValDef.let( sym, - erasedSym.declaredFields.map { fld => - fld.info.asType match - case '[ft] => - Expr.summon[RewriteInner[ft]] match - case Some(rwInner) => - '{ - $rwInner.rewriteInner( - ${ This(erasedSym).select(fld).asExprOf[ft] }, - ${ fn.asExprOf[Erased => Erased] }, + erasedSym.declaredFields + .zip(tpTree.symbol.caseFields) + .map { (fld, origFld) => + Ref(fld).tpe.widen.asType match + case '[ft] => + Implicits.search(TypeRepr.of[RewriteInner[ft]]) match + case success: ImplicitSearchSuccess => + '{ + ${ success.tree.asExprOf[RewriteInner[ft]] }.rewriteInner( + ${ This(erasedSym).select(fld).asExprOf[ft] }, + ${ fn.asExprOf[Erased => Erased] }, + ) + }.asTerm + case failure: ImplicitSearchFailure => + report.errorAndAbort( + failure.explanation, + origFld.pos.getOrElse(Position.ofMacroExpansion), ) - }.asTerm - case None => - report.errorAndAbort(s"no rewrite rule for ${TypeRepr.of[ft].show}") - end match - end match - }, + end match + end match + }, ) { binds => val didChangeExpr = erasedSym.declaredFields .zip(binds) @@ -129,48 +176,52 @@ object P: } .asTerm } - case _ => ??? + case _ => throw RuntimeException("unreachable") }) ) ), - ClassDef( - metaSym, - List(TypeTree.of[Object], TypeTree.of[Meta[T]]), - List( - DefDef( - metaSym.declaredMethod("erase").head, - { - case List(List(t)) => - Some: - New(TypeIdent(erasedSym)) - .select(erasedSym.primaryConstructor) - .appliedToArgs: - classSym.caseFields - .map: fld => - t - .asExpr - .asTerm - .select(fld) - case _ => ??? - }, - ) - ), - ) ), - New(TypeIdent(metaSym)) - .select(metaSym.primaryConstructor) - .appliedToArgs(Nil) + '{ + final class GeneratedMeta extends Conversion[T, Erased], Meta[T]: + def erase(t: T): Erased = + ${ + New(TypeIdent(erasedSym)) + .select(erasedSym.primaryConstructor) + .appliedToArgs: + tpTree.symbol.caseFields + .map: fld => + '{t} + .asTerm + .select(fld) + .asExprOf[Erased] + } + end erase + + def conversion: Conversion[T, Erased] = this + + def apply(t: T): Erased = erase(t) + end GeneratedMeta + + new GeneratedMeta + } + .asTerm, ) .asExprOf[Meta[T]] else - report.errorAndAbort(s"${tp.show} is neither a case class nor a sealed abstract type") + report.errorAndAbort(s"${tpTree.show} is neither a case class nor a sealed abstract type") end if end derivedImpl end Meta - given [T, U <: T] => (meta: Meta[T]) => Conversion[U, P[T]]: - def apply(x: U): P[T] = meta.erase(x) - end given + // Implicit lookup on traits with variance can be glitchy, so this manually implements the + // "subtyping" rule that a Meta[U <: T] can be implemented via a Meta[T]. It is just redundant that + // the first operation in erase(u) will therefore be to check that u instanceof U. + inline given [T, U <: T] => (meta: Meta[T]) => (=>NotGiven[U =:= T]) => Meta[U] = meta.asInstanceOf + + // This awkward pattern lets us customize the implicit not found message. + // Technically the Conversion is considered to "succeed", even if all it does is immediately + // fail summonInline, therefore showing the failure for looking up Meta[T], not Conversion[T, P[T]]. + inline given [T] => Conversion[T, P[T]] = summonInline[Meta[T]].conversion trait Erased: def rewriteInner(fn: Erased => Erased): Erased @@ -180,11 +231,16 @@ object P: def fixpoint(fn: [U] => P[U] => P[U]): P[T] = ??? end extension + @implicitNotFound("no rule found to rewrite P[?] inside ${T}") trait RewriteInner[T]: def rewriteInner(t: T, fn: Erased => Erased): T end RewriteInner object RewriteInner: + given RewriteInner[Boolean]: + inline def rewriteInner(t: Boolean, fn: Erased => Erased): Boolean = t + end given + given RewriteInner[Byte]: inline def rewriteInner(t: Byte, fn: Erased => Erased): Byte = t end given diff --git a/forja/src/PTest.scala b/forja/src/PTest.scala index 2fc5458..560688a 100644 --- a/forja/src/PTest.scala +++ b/forja/src/PTest.scala @@ -3,9 +3,19 @@ package forja object PTest: final case class Foo(x: Int, y: String) derives P.Meta + enum Bar derives P.Meta: + case Ping + case Pong(x: Int, y: P[Foo]) + end Bar + def main(args: Array[String]): Unit = println(summon[P.Meta[Foo]].erase(Foo(42, "43")).rewriteInner(identity)) val foo: P[Foo] = Foo(43, "44") println(foo) + + val ping: P[Bar.Ping.type] = Bar.Ping + val pong: P[Bar.Pong] = Bar.Pong(44, foo) + println(ping) + println(pong) end main end PTest From 9b2c1175343cd554cd2079aab66cf36116cc3b63 Mon Sep 17 00:00:00 2001 From: Finn Hackett Date: Fri, 10 Jul 2026 08:51:11 +0000 Subject: [PATCH 5/8] remove accumulated versions, add Lang experiment --- .devcontainer/Dockerfile | 22 - .devcontainer/devcontainer.json | 17 - build.mill | 1 - debugAdapterVSCode/package.json | 46 -- forja/src/Lang.scala | 145 ++++ forja/src/ModelChecker.scala | 73 -- forja/src/Node.scala | 603 ---------------- forja/src/P.scala | 286 -------- forja/src/PTest.scala | 21 - forja/src/Pass.scala | 278 -------- forja/src/Pattern.scala | 352 ---------- forja/src/Prod.scala | 303 -------- forja/src/Query.scala | 403 ----------- forja/src/Source.scala | 104 --- forja/src/SourceRange.scala | 300 -------- forja/src/Test.scala | 55 -- forja/src/TestUtil.scala | 26 - forja/src/Token.scala | 51 -- forja/src/Wf.scala | 178 ----- forja/src/syntax.scala | 240 ------- forja/src/util/FastPatchTree.scala | 92 --- forja/src/util/MonomorphicIndexedSeq.scala | 43 -- forja/src/util/ReflectiveEnumeration.scala | 85 --- forja/src/util/TupleOf.scala | 9 - forja/test/src/NodeSpanTest.scala | 46 -- forja/test/src/PatternTest.scala | 190 ----- forja/test/src/WfTest.scala | 88 --- forja/test/src/util/FastPatchTreeTest.scala | 31 - langs/calc/CalcEvaluator.scala | 101 --- langs/calc/CalcParser.scala | 154 ----- langs/calc/CalcReader.scala | 119 ---- langs/calc/README.md | 69 -- langs/calc/img/example1.jpg | Bin 58121 -> 0 bytes langs/calc/img/example2.jpg | Bin 90964 -> 0 bytes langs/calc/package.mill | 9 - langs/calc/package.scala | 71 -- langs/calc/package.test.scala | 360 ---------- langs/calc/src/CalcAST.scala | 33 - langs/calc/src/CalcEvaluator.scala | 79 --- langs/calc/src/CalcParser.scala | 186 ----- langs/calc/src/CalcReader.scala | 138 ---- langs/calc/test/src/CalcEvaluatorTest.scala | 68 -- langs/calc/test/src/CalcParserTest.scala | 183 ----- langs/calc/test/src/CalcReaderTest.scala | 164 ----- langs/package.mill | 1 - langs/tla/ExprMarker.scala | 371 ---------- langs/tla/ExprMarker.test.scala | 456 ------------ langs/tla/TLAParser.scala | 729 -------------------- langs/tla/TLAParser.test.scala | 49 -- langs/tla/TLAReader.scala | 578 ---------------- langs/tla/TLAReader.test.scala | 38 - langs/tla/defns.scala | 222 ------ langs/tla/package.scala | 378 ---------- scripts/rewrite_src.scala | 33 - scripts/update_license.scala | 102 --- src/Builtin.scala | 33 - src/EmbedMeta.scala | 102 --- src/EmbedMeta.test.scala | 61 -- src/Node.scala | 688 ------------------ src/PassSeq.scala | 116 ---- src/Token.scala | 158 ----- src/Token.test.scala | 45 -- src/dsl.scala | 26 - src/manip/DebugAdapter.scala | 713 ------------------- src/manip/Handle.scala | 166 ----- src/manip/Manip.FuzzCompare.test.scala | 323 --------- src/manip/Manip.scala | 606 ---------------- src/manip/Manip.test.scala | 192 ------ src/manip/ManipOps.scala | 474 ------------- src/manip/ReferenceTracer.scala | 398 ----------- src/manip/SegmentedStack.scala | 77 --- src/manip/SegmentedStack.test.scala | 78 --- src/manip/SeqPattern.scala | 211 ------ src/manip/SeqPattern.test.scala | 141 ---- src/manip/SeqPatternOps.scala | 220 ------ src/manip/Tracer.scala | 266 ------- src/sexpr/SExprReader.scala | 192 ------ src/sexpr/SExprReader.test.scala | 144 ---- src/sexpr/package.scala | 146 ---- src/sexpr/serialize.test.scala | 62 -- src/source/Reader.scala | 230 ------ src/source/Source.scala | 89 --- src/source/SourceRange.scala | 283 -------- src/source/SourceRange.test.scala | 165 ----- src/test/WithClonedCorpus.scala | 51 -- src/test/WithTLACorpus.scala | 22 - src/test/newlineUtils.scala | 22 - src/util/ById.scala | 39 -- src/util/DebugInfo.scala | 72 -- src/util/FuzzTestSuite.test.scala | 199 ------ src/util/HasInstanceArray.scala | 31 - src/util/Named.scala | 61 -- src/util/SymbolicMapFactory.scala | 106 --- src/util/TailCallsUtils.scala | 44 -- src/util/geny.scala | 23 - src/util/toShortString.scala | 23 - src/util/unreachable.scala | 22 - src/wf/Shape.scala | 88 --- src/wf/Wellformed.scala | 598 ---------------- src/wf/Wellformed.test.scala | 176 ----- src/wf/WellformedDef.scala | 85 --- 101 files changed, 145 insertions(+), 16701 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 debugAdapterVSCode/package.json create mode 100644 forja/src/Lang.scala delete mode 100644 forja/src/ModelChecker.scala delete mode 100644 forja/src/Node.scala delete mode 100644 forja/src/P.scala delete mode 100644 forja/src/PTest.scala delete mode 100644 forja/src/Pass.scala delete mode 100644 forja/src/Pattern.scala delete mode 100644 forja/src/Prod.scala delete mode 100644 forja/src/Query.scala delete mode 100644 forja/src/Source.scala delete mode 100644 forja/src/SourceRange.scala delete mode 100644 forja/src/Test.scala delete mode 100644 forja/src/TestUtil.scala delete mode 100644 forja/src/Token.scala delete mode 100644 forja/src/Wf.scala delete mode 100644 forja/src/syntax.scala delete mode 100644 forja/src/util/FastPatchTree.scala delete mode 100644 forja/src/util/MonomorphicIndexedSeq.scala delete mode 100644 forja/src/util/ReflectiveEnumeration.scala delete mode 100644 forja/src/util/TupleOf.scala delete mode 100644 forja/test/src/NodeSpanTest.scala delete mode 100644 forja/test/src/PatternTest.scala delete mode 100644 forja/test/src/WfTest.scala delete mode 100644 forja/test/src/util/FastPatchTreeTest.scala delete mode 100644 langs/calc/CalcEvaluator.scala delete mode 100644 langs/calc/CalcParser.scala delete mode 100644 langs/calc/CalcReader.scala delete mode 100644 langs/calc/README.md delete mode 100644 langs/calc/img/example1.jpg delete mode 100644 langs/calc/img/example2.jpg delete mode 100644 langs/calc/package.mill delete mode 100644 langs/calc/package.scala delete mode 100644 langs/calc/package.test.scala delete mode 100644 langs/calc/src/CalcAST.scala delete mode 100644 langs/calc/src/CalcEvaluator.scala delete mode 100644 langs/calc/src/CalcParser.scala delete mode 100644 langs/calc/src/CalcReader.scala delete mode 100644 langs/calc/test/src/CalcEvaluatorTest.scala delete mode 100644 langs/calc/test/src/CalcParserTest.scala delete mode 100644 langs/calc/test/src/CalcReaderTest.scala delete mode 100644 langs/package.mill delete mode 100644 langs/tla/ExprMarker.scala delete mode 100644 langs/tla/ExprMarker.test.scala delete mode 100644 langs/tla/TLAParser.scala delete mode 100644 langs/tla/TLAParser.test.scala delete mode 100644 langs/tla/TLAReader.scala delete mode 100644 langs/tla/TLAReader.test.scala delete mode 100644 langs/tla/defns.scala delete mode 100644 langs/tla/package.scala delete mode 100644 scripts/rewrite_src.scala delete mode 100755 scripts/update_license.scala delete mode 100644 src/Builtin.scala delete mode 100644 src/EmbedMeta.scala delete mode 100644 src/EmbedMeta.test.scala delete mode 100644 src/Node.scala delete mode 100644 src/PassSeq.scala delete mode 100644 src/Token.scala delete mode 100644 src/Token.test.scala delete mode 100644 src/dsl.scala delete mode 100644 src/manip/DebugAdapter.scala delete mode 100644 src/manip/Handle.scala delete mode 100644 src/manip/Manip.FuzzCompare.test.scala delete mode 100644 src/manip/Manip.scala delete mode 100644 src/manip/Manip.test.scala delete mode 100644 src/manip/ManipOps.scala delete mode 100644 src/manip/ReferenceTracer.scala delete mode 100644 src/manip/SegmentedStack.scala delete mode 100644 src/manip/SegmentedStack.test.scala delete mode 100644 src/manip/SeqPattern.scala delete mode 100644 src/manip/SeqPattern.test.scala delete mode 100644 src/manip/SeqPatternOps.scala delete mode 100644 src/manip/Tracer.scala delete mode 100644 src/sexpr/SExprReader.scala delete mode 100644 src/sexpr/SExprReader.test.scala delete mode 100644 src/sexpr/package.scala delete mode 100644 src/sexpr/serialize.test.scala delete mode 100644 src/source/Reader.scala delete mode 100644 src/source/Source.scala delete mode 100644 src/source/SourceRange.scala delete mode 100644 src/source/SourceRange.test.scala delete mode 100644 src/test/WithClonedCorpus.scala delete mode 100644 src/test/WithTLACorpus.scala delete mode 100644 src/test/newlineUtils.scala delete mode 100644 src/util/ById.scala delete mode 100644 src/util/DebugInfo.scala delete mode 100644 src/util/FuzzTestSuite.test.scala delete mode 100644 src/util/HasInstanceArray.scala delete mode 100644 src/util/Named.scala delete mode 100644 src/util/SymbolicMapFactory.scala delete mode 100644 src/util/TailCallsUtils.scala delete mode 100644 src/util/geny.scala delete mode 100644 src/util/toShortString.scala delete mode 100644 src/util/unreachable.scala delete mode 100644 src/wf/Shape.scala delete mode 100644 src/wf/Wellformed.scala delete mode 100644 src/wf/Wellformed.test.scala delete mode 100644 src/wf/WellformedDef.scala 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/build.mill b/build.mill index ac4ca98..022c235 100644 --- a/build.mill +++ b/build.mill @@ -21,7 +21,6 @@ trait ForjaModule extends ScalaModule, ScalafixModule: "-Xcheck-macros", "-explain-cyclic", "-preview", - "-experimental", ) override def forkArgs = super.forkArgs() ++ Seq( // TODO: fix when Scala 3.8? 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..a913026 --- /dev/null +++ b/forja/src/Lang.scala @@ -0,0 +1,145 @@ +package forja + +import scala.util.NotGiven +import forja.util.TupleOf + +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 Retract + class Replace[OtherNode <: Node] extends Retract + + // Must be defined in a nested object, or _all subclasses_ + // can see T is just Data and typecheck accordingly. + // That makes a huge mess. + // That said, X.Scope.T does not look terrible in type annotations. + object Scope: + into opaque type T = Data + def T(data: Data): T = data + extension (t: T) + def data: Data = t + end extension + end Scope + export Scope.* + + given Node.TNode.Aux[T, this.type] = new Node.TNode[T]: + type N = Node.this.type + end given + end Node + + object Node: + sealed trait TNode[T <: Node#T]: + type N <: Node + end TNode + object TNode: + type Aux[T <: Node#T, N0 <: Node] = TNode[T] { + type N = N0 + } + end TNode + end Node + + abstract class Sum extends Node: + // Plan: the user implements this type with a sealed class which all implementing productions extend. + // As a result the compiler will keep track of which values this Sum should accept. + // To extend, add a nexted Extend class that mashes this Case and its Case together. + // To shrink, we should take into account C <: Case, C.Retract being present and stop accepting that case. + type Case <: Node + + // TODO: Sum working at all. + // Technically it is a Node and so its T and Retract/Replace work, but it is not instantiable. + end Sum + + abstract class Term[Members <: NamedTuple.AnyNamedTuple] extends Node: + self: Singleton => + + inline def apply(using inline ng: NotGiven[Retract])(using et: EffectiveType[Members])(members: et.To): T = + T(Data(this, members.asInstanceOf[Tuple])) + end apply + end Term + + final class Data(val term: Term[?], val fields: Tuple) + + sealed trait EffectiveNodeType[From <: Node]: + type To <: Node + end EffectiveNodeType + object EffectiveNodeType: + type Aux[From <: Node, To0 <: Node] = EffectiveNodeType[From] { + type To = To0 + } + + def apply[From <: Node, To0 <: Node]: Aux[From, To0] = new EffectiveNodeType[From]: + type To = To0 + end apply + end EffectiveNodeType + + given [N <: Node] => (N: N) => NotGiven[N.Retract] => EffectiveNodeType.Aux[N, N] = EffectiveNodeType.apply + given [N1 <: Node, N2 <: Node] => (N1: N1) => (N1.Replace[N2]) => (et: EffectiveNodeType[N2]) => EffectiveNodeType.Aux[N1, et.To] = EffectiveNodeType.apply + + sealed trait EffectiveType[From]: + type To + end EffectiveType + object EffectiveType: + type Aux[From, To0] = EffectiveType[From] { + type To = To0 + } + + def apply[From, To0]: Aux[From, To0] = new EffectiveType[From] { + type To = To0 + } + + trait Ident[T] extends EffectiveType[T]: + type To = T + end Ident + 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. + given [T] => (tn: Node.TNode[T & Node#T]) => (ent: EffectiveNodeType[tn.N]) => (N2: ent.To) => EffectiveType.Aux[T, N2.T] = EffectiveType.apply + + given EffectiveType.Aux[EmptyTuple, EmptyTuple] = EffectiveType.apply + given [H, Tl <: Tuple] => (eh: EffectiveType[H]) => (et: EffectiveType[Tl]) => EffectiveType.Aux[H *: Tl, eh.To *: (et.To & Tuple)] = EffectiveType.apply + + given [Nt <: NamedTuple.AnyNamedTuple] => (et: EffectiveType[NamedTuple.DropNames[Nt]]) => EffectiveType.Aux[Nt, NamedTuple.NamedTuple[NamedTuple.Names[Nt], et.To & Tuple]] = EffectiveType.apply + + given EffectiveType.Ident[Boolean] + given EffectiveType.Ident[Byte] + given EffectiveType.Ident[Char] + given EffectiveType.Ident[Short] + given EffectiveType.Ident[Int] + given EffectiveType.Ident[Long] + given EffectiveType.Ident[Float] + given EffectiveType.Ident[Double] + + given EffectiveType.Ident[String] + + given [T] => (et: EffectiveType[T]) => EffectiveType.Aux[Option[T], Option[et.To]] = EffectiveType.apply + given [T] => (et: EffectiveType[T]) => EffectiveType.Aux[List[T], List[et.To]] = EffectiveType.apply +end Lang + +object Test: + trait L1 extends Lang: + object Foo extends Lang.Term[(i: Int, j: Int)] + end L1 + object L1 extends L1 + + val x = L1.Foo(i = 42, j = 43) + + trait L2 extends Lang.Extend[L1]: + export up.{ + Foo as _, + *, + } + object Bar extends Lang.Term[(s: String, opt: Option[up.Foo.T])] + + given up.Foo.Replace[Bar.type] {} + end L2 + object L2 extends L2 + + val y: L2.Bar.T = L2.Bar(s = "hi", opt = Some(L2.Bar("ho", None))) +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/P.scala b/forja/src/P.scala deleted file mode 100644 index 39b02dd..0000000 --- a/forja/src/P.scala +++ /dev/null @@ -1,286 +0,0 @@ -package forja - -import scala.quoted.Quotes -import scala.quoted.Type -import scala.quoted.quotes -import scala.quoted.Expr -import scala.util.NotGiven -import scala.quoted.Varargs -import scala.annotation.implicitNotFound -import scala.compiletime.summonInline - -into opaque type P[T] = P.Erased - -object P: - @implicitNotFound("${T} or its sealed supertype needs to derive P.Meta") - trait Meta[T]: - def erase(t: T): Erased - // Gotcha: this could more sensibly convert to P[T], but some interaction of the opaque type - // being in scope here and the macro definitions below breaks things. The error looks like - // the macro hardcodes Erased where I lexically wrote P[T], causing the compiler to notice later - // on that def conversion: Conversion[T, Erased] would not implement the P[T] version. - // I guess this is because the macro is expanded in another file... - // Instead, we use Conversion's variance to replace Erased with P[T] during the given that calls - // this method. - def conversion: Conversion[T, Erased] - end Meta - - object Meta: - inline def derived[T]: Meta[T] = ${ derivedImpl } - - private def derivedImpl[T : Type](using Quotes): Expr[Meta[T]] = - import quotes.reflect.* - - val tpTree = TypeTree.of[T] - println(s"process: ${tpTree.show}") - if tpTree.symbol.flags.is(Flags.Sealed) && tpTree.symbol.flags.is(Flags.Abstract) - then - println(s"abstract: ${tpTree.show}") - '{ - final class GeneratedMeta extends Conversion[T, Erased], Meta[T]: - // TODO: array is simpler, but if it's ever a bottleneck then we can generate a synthetic fieldset - private val subMetas: Array[Meta[?]] = - ${ - val childMetas = tpTree.symbol.children.map: childSym => - childSym.typeRef.asType match - case '[ch] => '{ derived[ch] } - end childMetas - '{ Array(${Varargs(childMetas)}*) } - } - end subMetas - - def erase(t: T): Erased = - val subMetasProxy = subMetas - ${ - Match( - '{t}.asTerm, - tpTree.symbol.children.zipWithIndex.map { (childSym, idx) => - def branchBody = - '{ subMetasProxy(${Expr(idx)}).asInstanceOf[Meta[T]].erase(t) }.asTerm - end branchBody - if childSym.isTerm - then - CaseDef( - Ref(childSym), - None, - branchBody, - ) - else if childSym.isType - then - TypeTree.ref(childSym).tpe.asType match - case '[cT] => - CaseDef( - Typed(Wildcard(), TypeTree.of[cT]), - None, - branchBody, - ) - else - report.errorAndAbort(s"neither a term nor a type $childSym") - end if - }, - ) - .asExprOf[Erased] - } - end erase - - def conversion: Conversion[T, Erased] = this - - def apply(t: T): Erased = erase(t) - end GeneratedMeta - - new GeneratedMeta - } - else if tpTree.symbol.flags.is(Flags.Case) - then - val erasedSym = Symbol.newClass( - owner = Symbol.spliceOwner, - name = s"Erased${tpTree.symbol.name}", - parents = _ => List(TypeRepr.of[Object], TypeRepr.of[Erased]), - decls = sym => { - List( - Symbol.newMethod(sym, "rewriteInner", MethodType(List("fn"))(_ => List(TypeRepr.of[Erased => Erased]), _ => TypeRepr.of[Erased])), - ) - }, - selfType = None, - clsFlags = Flags.EmptyFlags, - clsPrivateWithin = Symbol.noSymbol, - clsAnnotations = Nil, - conMethodType = { resultTpe => - MethodType(tpTree.symbol.caseFields.map(fld => s"fld$$${fld.name}"))( - _ => tpTree.symbol.caseFields.map(fld => Ref(fld).tpe.widen), - _ => resultTpe, - ) - }, - conFlags = Flags.EmptyFlags, - conPrivateWithin = Symbol.noSymbol, - conParamFlags = List(tpTree.symbol.caseFields.map(_ => Flags.ParamAccessor)), - conParamPrivateWithins = List(tpTree.symbol.caseFields.map(_ => Symbol.noSymbol)), - ) - - Block( - List( - ClassDef( - cls = erasedSym, - parents = List(TypeTree.of[Object], TypeTree.of[Erased]), - body = List( - DefDef(erasedSym.declaredMethod("rewriteInner").head, { - case List(List(fn)) => - val sym = erasedSym.methodMember("rewriteInner").head - given Quotes = sym.asQuotes - Some: - ValDef.let( - sym, - erasedSym.declaredFields - .zip(tpTree.symbol.caseFields) - .map { (fld, origFld) => - Ref(fld).tpe.widen.asType match - case '[ft] => - Implicits.search(TypeRepr.of[RewriteInner[ft]]) match - case success: ImplicitSearchSuccess => - '{ - ${ success.tree.asExprOf[RewriteInner[ft]] }.rewriteInner( - ${ This(erasedSym).select(fld).asExprOf[ft] }, - ${ fn.asExprOf[Erased => Erased] }, - ) - }.asTerm - case failure: ImplicitSearchFailure => - report.errorAndAbort( - failure.explanation, - origFld.pos.getOrElse(Position.ofMacroExpansion), - ) - end match - end match - }, - ) { binds => - val didChangeExpr = erasedSym.declaredFields - .zip(binds) - .map: (fld, bind) => - This(erasedSym).select(fld).asExpr match - case '{ $nv: AnyRef } => - '{ $nv ne ${ bind.asExpr }.asInstanceOf[AnyRef] } - case '{ $nv: nvT } => - '{ $nv != ${ bind.asExpr} } - end match - .foldLeft('{ false })((l, r) => '{ $l || $r }) - end didChangeExpr - '{ - val didChange = $didChangeExpr - if didChange - then ${ - New(TypeIdent(erasedSym)) - .select(erasedSym.primaryConstructor) - .appliedToArgs(erasedSym.declaredFields.map(This(erasedSym).select)) - .asExprOf[Erased] - } - else ${This(erasedSym).asExprOf[Erased]} - } - .asTerm - } - case _ => throw RuntimeException("unreachable") - }) - ) - ), - ), - '{ - final class GeneratedMeta extends Conversion[T, Erased], Meta[T]: - def erase(t: T): Erased = - ${ - New(TypeIdent(erasedSym)) - .select(erasedSym.primaryConstructor) - .appliedToArgs: - tpTree.symbol.caseFields - .map: fld => - '{t} - .asTerm - .select(fld) - .asExprOf[Erased] - } - end erase - - def conversion: Conversion[T, Erased] = this - - def apply(t: T): Erased = erase(t) - end GeneratedMeta - - new GeneratedMeta - } - .asTerm, - ) - .asExprOf[Meta[T]] - else - report.errorAndAbort(s"${tpTree.show} is neither a case class nor a sealed abstract type") - end if - end derivedImpl - end Meta - - // Implicit lookup on traits with variance can be glitchy, so this manually implements the - // "subtyping" rule that a Meta[U <: T] can be implemented via a Meta[T]. It is just redundant that - // the first operation in erase(u) will therefore be to check that u instanceof U. - inline given [T, U <: T] => (meta: Meta[T]) => (=>NotGiven[U =:= T]) => Meta[U] = meta.asInstanceOf - - // This awkward pattern lets us customize the implicit not found message. - // Technically the Conversion is considered to "succeed", even if all it does is immediately - // fail summonInline, therefore showing the failure for looking up Meta[T], not Conversion[T, P[T]]. - inline given [T] => Conversion[T, P[T]] = summonInline[Meta[T]].conversion - - trait Erased: - def rewriteInner(fn: Erased => Erased): Erased - end Erased - - extension [T] (p: P[T]) - def fixpoint(fn: [U] => P[U] => P[U]): P[T] = ??? - end extension - - @implicitNotFound("no rule found to rewrite P[?] inside ${T}") - trait RewriteInner[T]: - def rewriteInner(t: T, fn: Erased => Erased): T - end RewriteInner - - object RewriteInner: - given RewriteInner[Boolean]: - inline def rewriteInner(t: Boolean, fn: Erased => Erased): Boolean = t - end given - - given RewriteInner[Byte]: - inline def rewriteInner(t: Byte, fn: Erased => Erased): Byte = t - end given - - given RewriteInner[Char]: - inline def rewriteInner(t: Char, fn: Erased => Erased): Char = t - end given - - given RewriteInner[Short]: - inline def rewriteInner(t: Short, fn: Erased => Erased): Short = t - end given - - given RewriteInner[Int]: - inline def rewriteInner(t: Int, fn: Erased => Erased): Int = t - end given - - given RewriteInner[Long]: - inline def rewriteInner(t: Long, fn: Erased => Erased): Long = t - end given - - given RewriteInner[Float]: - inline def rewriteInner(t: Float, fn: Erased => Erased): Float = t - end given - - given RewriteInner[Double]: - inline def rewriteInner(t: Double, fn: Erased => Erased): Double = t - end given - - given RewriteInner[String]: - inline def rewriteInner(t: String, fn: Erased => Erased): String = t - end given - - given [T] => RewriteInner[P[T]]: - inline def rewriteInner(t: P[T], fn: Erased => Erased): Erased = t.rewriteInner(fn) - end given - - given [T <: AnyRef] => (rewriteElem: RewriteInner[T]) => RewriteInner[List[T]]: - def rewriteInner(t: List[T], fn: Erased => Erased): List[T] = - t.mapConserve(rewriteElem.rewriteInner(_, fn)) - end rewriteInner - end given - end RewriteInner -end P diff --git a/forja/src/PTest.scala b/forja/src/PTest.scala deleted file mode 100644 index 560688a..0000000 --- a/forja/src/PTest.scala +++ /dev/null @@ -1,21 +0,0 @@ -package forja - -object PTest: - final case class Foo(x: Int, y: String) derives P.Meta - - enum Bar derives P.Meta: - case Ping - case Pong(x: Int, y: P[Foo]) - end Bar - - def main(args: Array[String]): Unit = - println(summon[P.Meta[Foo]].erase(Foo(42, "43")).rewriteInner(identity)) - val foo: P[Foo] = Foo(43, "44") - println(foo) - - val ping: P[Bar.Ping.type] = Bar.Ping - val pong: P[Bar.Pong] = Bar.Pong(44, foo) - println(ping) - println(pong) - end main -end PTest 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/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/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 6ba2279af6b1268115d18935a02a91f97514d5dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58121 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 S*B)ODatPmMxL5ts^8WxU;TRAA diff --git a/langs/calc/img/example2.jpg b/langs/calc/img/example2.jpg deleted file mode 100644 index ac2ef1b675a6b25cbaa305699dbb255828dffa70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90964 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 - 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 From b52a4b1712a7701a9746a41952bcdf788b5aff65 Mon Sep 17 00:00:00 2001 From: Finn Hackett Date: Fri, 10 Jul 2026 21:29:35 +0000 Subject: [PATCH 6/8] implement Sum except for extension semantics --- forja/src/Lang.scala | 190 ++++++++++++++++---------- forja/src/util/InlineConversion.scala | 14 ++ forja/src/util/Instanceless.scala | 12 ++ 3 files changed, 145 insertions(+), 71 deletions(-) create mode 100644 forja/src/util/InlineConversion.scala create mode 100644 forja/src/util/Instanceless.scala diff --git a/forja/src/Lang.scala b/forja/src/Lang.scala index a913026..c398a6b 100644 --- a/forja/src/Lang.scala +++ b/forja/src/Lang.scala @@ -1,7 +1,11 @@ package forja import scala.util.NotGiven -import forja.util.TupleOf +import scala.reflect.TypeTest +import scala.annotation.publicInBinary +import scala.deriving.Mirror +import forja.util.Instanceless +import forja.util.InlineConversion trait Lang: final transparent inline given this.type = this @@ -12,29 +16,16 @@ object Lang: abstract class Node: final transparent inline given this.type = this - class Retract - class Replace[OtherNode <: Node] extends Retract - - // Must be defined in a nested object, or _all subclasses_ - // can see T is just Data and typecheck accordingly. - // That makes a huge mess. - // That said, X.Scope.T does not look terrible in type annotations. - object Scope: - into opaque type T = Data - def T(data: Data): T = data - extension (t: T) - def data: Data = t - end extension - end Scope - export Scope.* + class ReplaceWith[OtherNode <: Node] + class Retract extends ReplaceWith[Node.Empty.type] - given Node.TNode.Aux[T, this.type] = new Node.TNode[T]: - type N = Node.this.type - end given + 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]: + sealed trait TNode[T <: Node#T] extends Instanceless: type N <: Node end TNode object TNode: @@ -42,30 +33,76 @@ object Lang: type N = N0 } end TNode + + object Empty extends Node: + type T = Nothing + end Empty end Node abstract class Sum extends Node: - // Plan: the user implements this type with a sealed class which all implementing productions extend. - // As a result the compiler will keep track of which values this Sum should accept. - // To extend, add a nexted Extend class that mashes this Case and its Case together. - // To shrink, we should take into account C <: Case, C.Retract being present and stop accepting that case. 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] => NotGiven[ReplaceWith[?]] => (et: Sum.EffectiveType[Case]) => Conversion[et.T, this.T] = CastConversion[et.T, Sum.this.T] + end Sum + + sealed trait CastConversion[-T, +U] extends Conversion[T, U]: + def apply(x: T): U = x.asInstanceOf + end CastConversion + object CastConversion: + private object inst extends CastConversion[?, ?] + def apply[T, U]: CastConversion[T, U] = inst.asInstanceOf + end CastConversion + + 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]] - // TODO: Sum working at all. - // Technically it is a Node and so its T and Retract/Replace work, but it is not instantiable. + 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) - inline def apply(using inline ng: NotGiven[Retract])(using et: EffectiveType[Members])(members: et.To): T = - T(Data(this, members.asInstanceOf[Tuple])) + inline def apply(using inline ng: NotGiven[ReplaceWith[?]])(using et: EffectiveType[Members])(members: et.To): T = + T(members.asInstanceOf) end apply - end Term - final class Data(val term: Term[?], val fields: Tuple) + inline def unapply(using inline ng: NotGiven[ReplaceWith[?]])(using et: EffectiveType[Members])(t: T): Some[et.To] = + Some(t.members.asInstanceOf) + end unapply + end Term - sealed trait EffectiveNodeType[From <: Node]: + sealed trait EffectiveNodeType[From <: Node] extends Instanceless: type To <: Node end EffectiveNodeType object EffectiveNodeType: @@ -73,15 +110,13 @@ object Lang: type To = To0 } - def apply[From <: Node, To0 <: Node]: Aux[From, To0] = new EffectiveNodeType[From]: - type To = To0 - end apply + inline def apply[From <: Node, To <: Node](): EffectiveNodeType.Aux[From, To] = Instanceless[EffectiveNodeType.Aux[From, To]] end EffectiveNodeType - given [N <: Node] => (N: N) => NotGiven[N.Retract] => EffectiveNodeType.Aux[N, N] = EffectiveNodeType.apply - given [N1 <: Node, N2 <: Node] => (N1: N1) => (N1.Replace[N2]) => (et: EffectiveNodeType[N2]) => EffectiveNodeType.Aux[N1, et.To] = EffectiveNodeType.apply + inline given effectiveNodeIdentity: [N <: Node] => (N: N) => 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]: + sealed trait EffectiveType[From] extends Instanceless: type To end EffectiveType object EffectiveType: @@ -89,57 +124,70 @@ object Lang: type To = To0 } - def apply[From, To0]: Aux[From, To0] = new 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. - given [T] => (tn: Node.TNode[T & Node#T]) => (ent: EffectiveNodeType[tn.N]) => (N2: ent.To) => EffectiveType.Aux[T, N2.T] = EffectiveType.apply + inline given effectiveNode: [T] => (tn: Node.TNode[T & Node#T]) => (ent: EffectiveNodeType[tn.N]) => (N2: ent.To) => EffectiveType.Aux[T, N2.T] = EffectiveType() - given EffectiveType.Aux[EmptyTuple, EmptyTuple] = EffectiveType.apply - given [H, Tl <: Tuple] => (eh: EffectiveType[H]) => (et: EffectiveType[Tl]) => EffectiveType.Aux[H *: Tl, eh.To *: (et.To & Tuple)] = EffectiveType.apply + inline given effectiveTupleEmpty: EffectiveType.Aux[EmptyTuple, EmptyTuple] = EffectiveType() + inline given effectiveTupleCons: [H, Tl <: Tuple] => (eh: EffectiveType[H]) => (et: EffectiveType[Tl]) => EffectiveType.Aux[H *: Tl, eh.To *: (et.To & Tuple)] = EffectiveType() - given [Nt <: NamedTuple.AnyNamedTuple] => (et: EffectiveType[NamedTuple.DropNames[Nt]]) => EffectiveType.Aux[Nt, NamedTuple.NamedTuple[NamedTuple.Names[Nt], et.To & Tuple]] = EffectiveType.apply + inline given effectiveNamedTuple: [Nt <: NamedTuple.AnyNamedTuple] => (et: EffectiveType[NamedTuple.DropNames[Nt]]) => EffectiveType.Aux[Nt, NamedTuple.NamedTuple[NamedTuple.Names[Nt], et.To & Tuple]] = EffectiveType() - given EffectiveType.Ident[Boolean] - given EffectiveType.Ident[Byte] - given EffectiveType.Ident[Char] - given EffectiveType.Ident[Short] - given EffectiveType.Ident[Int] - given EffectiveType.Ident[Long] - given EffectiveType.Ident[Float] - given EffectiveType.Ident[Double] + 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() - given EffectiveType.Ident[String] + inline given EffectiveType.Ident[String] = EffectiveType.Ident() - given [T] => (et: EffectiveType[T]) => EffectiveType.Aux[Option[T], Option[et.To]] = EffectiveType.apply - given [T] => (et: EffectiveType[T]) => EffectiveType.Aux[List[T], List[et.To]] = EffectiveType.apply + 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: - trait L1 extends Lang: - object Foo extends Lang.Term[(i: Int, j: Int)] - end L1 - object L1 extends L1 + def main(args: Array[String]): Unit = + trait L1 extends Lang: + object Foo extends Lang.Term[(i: Int, j: Int)] - val x = L1.Foo(i = 42, j = 43) + object Ping extends Lang.Sum: + sealed trait Case extends Lang.Node - trait L2 extends Lang.Extend[L1]: - export up.{ - Foo as _, - *, - } - object Bar extends Lang.Term[(s: String, opt: Option[up.Foo.T])] + 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) + val ping: L1.Ping.T = L1.Ping.Pong(k = 12, foo = x) + + trait L2 extends Lang.Extend[L1]: + export up.{ + Foo as _, + *, + } + object Bar extends Lang.Term[(s: String, opt: Option[up.Foo.T])] - given up.Foo.Replace[Bar.type] {} - end L2 - object L2 extends L2 + 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))) + val y: L2.Bar.T = L2.Bar(s = "hi", opt = Some(L2.Bar("ho", None))) + val ping2: L2.Ping.T = y + end main end Test diff --git a/forja/src/util/InlineConversion.scala b/forja/src/util/InlineConversion.scala new file mode 100644 index 0000000..9397600 --- /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: + def apply[T, U]: ByCast[T, U] = null.asInstanceOf[ByCast[T, U]] + 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 From 1afd4ff5964350403b6d375b2b7aad4772b7b953 Mon Sep 17 00:00:00 2001 From: Finn Hackett Date: Fri, 10 Jul 2026 23:15:13 +0000 Subject: [PATCH 7/8] troubleshoot unapply --- forja/src/Lang.scala | 65 ++++++++++++++++++++------- forja/src/util/InlineConversion.scala | 2 +- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/forja/src/Lang.scala b/forja/src/Lang.scala index c398a6b..0773e63 100644 --- a/forja/src/Lang.scala +++ b/forja/src/Lang.scala @@ -6,6 +6,8 @@ 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 @@ -51,17 +53,9 @@ object Lang: end Opaque override type T = Opaque.T - inline given subtype: [T] => NotGiven[ReplaceWith[?]] => (et: Sum.EffectiveType[Case]) => Conversion[et.T, this.T] = CastConversion[et.T, Sum.this.T] + inline given subtype: [T] => (inline ng: NotGiven[ReplaceWith[?]]) => (et: Sum.EffectiveType[Case]) => InlineConversion.ByCast[et.T, this.T] = InlineConversion.ByCast() end Sum - sealed trait CastConversion[-T, +U] extends Conversion[T, U]: - def apply(x: T): U = x.asInstanceOf - end CastConversion - object CastConversion: - private object inst extends CastConversion[?, ?] - def apply[T, U]: CastConversion[T, U] = inst.asInstanceOf - end CastConversion - object Sum: sealed trait EffectiveType[C <: Sum#Case] extends Instanceless: type T <: Node#T @@ -91,14 +85,27 @@ object Lang: abstract class Term[Members <: NamedTuple.AnyNamedTuple] extends Node: self: Singleton => - final class T @publicInBinary private[Term] (private[Term] val members: Tuple) + 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 - inline def unapply(using inline ng: NotGiven[ReplaceWith[?]])(using et: EffectiveType[Members])(t: T): Some[et.To] = - Some(t.members.asInstanceOf) + // 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 @@ -113,7 +120,7 @@ object Lang: 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) => NotGiven[N.ReplaceWith[?]] => EffectiveNodeType.Aux[N, N] = 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: @@ -138,10 +145,22 @@ object Lang: // intersection. inline given effectiveNode: [T] => (tn: Node.TNode[T & Node#T]) => (ent: EffectiveNodeType[tn.N]) => (N2: ent.To) => EffectiveType.Aux[T, N2.T] = EffectiveType() - inline given effectiveTupleEmpty: EffectiveType.Aux[EmptyTuple, EmptyTuple] = EffectiveType() - inline given effectiveTupleCons: [H, Tl <: Tuple] => (eh: EffectiveType[H]) => (et: EffectiveType[Tl]) => EffectiveType.Aux[H *: Tl, eh.To *: (et.To & Tuple)] = 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 effectiveNamedTuple: [Nt <: NamedTuple.AnyNamedTuple] => (et: EffectiveType[NamedTuple.DropNames[Nt]]) => EffectiveType.Aux[Nt, NamedTuple.NamedTuple[NamedTuple.Names[Nt], et.To & Tuple]] = EffectiveType() + 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() @@ -172,7 +191,9 @@ object Test: 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.{ @@ -188,6 +209,18 @@ object Test: 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/util/InlineConversion.scala b/forja/src/util/InlineConversion.scala index 9397600..5d7191f 100644 --- a/forja/src/util/InlineConversion.scala +++ b/forja/src/util/InlineConversion.scala @@ -9,6 +9,6 @@ object InlineConversion: inline def apply(t: T): U = t.asInstanceOf[U] end ByCast object ByCast: - def apply[T, U]: ByCast[T, U] = null.asInstanceOf[ByCast[T, U]] + inline def apply[T, U](): ByCast[T, U] = null.asInstanceOf end ByCast end InlineConversion From c3f24a3a7710713aac414ca46881e700235d0ae0 Mon Sep 17 00:00:00 2001 From: Finn Hackett Date: Fri, 10 Jul 2026 23:50:36 +0000 Subject: [PATCH 8/8] re-enable formatting --- .github/workflows/test.yml | 1 + .github/workflows/validation.yml | 21 -------- .scalafix.conf | 2 +- .scalafmt.conf | 2 +- build.mill | 71 +------------------------ forja/src/Lang.scala | 91 ++++++++++++++++++++++---------- format_src.sh | 4 +- format_src_check.sh | 4 +- 8 files changed, 72 insertions(+), 124 deletions(-) delete mode 100644 .github/workflows/validation.yml 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 022c235..9a2b9b6 100644 --- a/build.mill +++ b/build.mill @@ -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/forja/src/Lang.scala b/forja/src/Lang.scala index 0773e63..bfacfc1 100644 --- a/forja/src/Lang.scala +++ b/forja/src/Lang.scala @@ -1,7 +1,6 @@ package forja import scala.util.NotGiven -import scala.reflect.TypeTest import scala.annotation.publicInBinary import scala.deriving.Mirror import forja.util.Instanceless @@ -23,7 +22,8 @@ object Lang: type T - inline given Node.TNode.Aux[T, this.type] = Instanceless[Node.TNode.Aux[T, this.type]] + inline given Node.TNode.Aux[T, this.type] = + Instanceless[Node.TNode.Aux[T, this.type]] end Node object Node: @@ -46,14 +46,18 @@ object Lang: 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 = + 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() + inline given subtype: [T] => (inline ng: NotGiven[ReplaceWith[?]]) + => (et: Sum.EffectiveType[Case]) + => InlineConversion.ByCast[et.T, this.T] = InlineConversion.ByCast() end Sum object Sum: @@ -65,7 +69,9 @@ object Lang: 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]] + 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: @@ -76,16 +82,23 @@ object Lang: type C = C0 } - inline def apply[Cases <: Tuple, C <: Node#T](): Aux[Cases, C] = Instanceless[Aux[Cases, C]] + 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() + 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): + 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 @@ -96,15 +109,19 @@ object Lang: Objects.hash(self, members) end hashCode end T - - inline def apply(using inline ng: NotGiven[ReplaceWith[?]])(using et: EffectiveType[Members])(members: et.To): 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 = + def unapply(t: T)(using + ng: NotGiven[ReplaceWith[?]], + )(using et: EffectiveType[Members]): et.To = t.members.asInstanceOf[et.To] end unapply end Term @@ -117,11 +134,18 @@ object Lang: type To = To0 } - inline def apply[From <: Node, To <: Node](): EffectiveNodeType.Aux[From, To] = Instanceless[EffectiveNodeType.Aux[From, To]] + 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() + 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 @@ -138,12 +162,15 @@ object Lang: 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]] + 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() + 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 @@ -153,14 +180,23 @@ object Lang: type To = To0 } - inline def apply[From <: Tuple, To <: Tuple](): Aux[From, To] = Instanceless[Aux[From, To]] + 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() + 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 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() @@ -173,8 +209,10 @@ object Lang: 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() + 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: @@ -196,10 +234,7 @@ object Test: println(ping) trait L2 extends Lang.Extend[L1]: - export up.{ - Foo as _, - *, - } + export up.{Foo as _, *} object Bar extends Lang.Term[(s: String, opt: Option[up.Foo.T])] given r1: up.Foo.ReplaceWith[Bar.type]() 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