From 266c67d4d328cddd6ea21d5c76dd77bbf8b9ace5 Mon Sep 17 00:00:00 2001 From: Hassaan Mohsin Date: Tue, 30 Jun 2026 17:07:21 -0700 Subject: [PATCH 1/3] 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/3] 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/3] 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