From 1c0c4a1bc4e2adb59bea7fac76092ab590aa04bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 15:09:49 +0200 Subject: [PATCH 01/10] feat(core): restore show*/sourceCode debug macros via Scala 3 quotes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ShowMacros object with 10 quoted impls (showAst, showRawAst, showSymbol, showSymbolFullName, showType, showRawType, showTypeSymbol, showTypeSymbolFullName, sourceCode, withSourceCode). - Replace 10 ??? stubs in UniversalOps[A] with inline def wrappers. - show* macros use report.info (Scala 2 used c.error as a hack; Scala 3 has a proper info channel — print + proceed). --- .../avsystem/commons/SharedExtensions.scala | 30 ++++------ .../avsystem/commons/macros/ShowMacros.scala | 56 +++++++++++++++++++ 2 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 core/src/main/scala/com/avsystem/commons/macros/ShowMacros.scala diff --git a/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala b/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala index 70c670615..bac197a67 100644 --- a/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala +++ b/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala @@ -126,26 +126,16 @@ object SharedExtensionsUtils extends SharedExtensions { def uncheckedMatch[B](pf: PartialFunction[A, B]): B = pf.applyOrElse(a, (obj: A) => throw new MatchError(obj)) - // TODO[scala3-port]: showAst (Scala 2 macro def) (L) - def showAst: A = ??? - // TODO[scala3-port]: showRawAst (Scala 2 macro def) (L) - def showRawAst: A = ??? - // TODO[scala3-port]: showSymbol (Scala 2 macro def) (L) - def showSymbol: A = ??? - // TODO[scala3-port]: showSymbolFullName (Scala 2 macro def) (L) - def showSymbolFullName: A = ??? - // TODO[scala3-port]: showType (Scala 2 macro def) (L) - def showType: A = ??? - // TODO[scala3-port]: showRawType (Scala 2 macro def) (L) - def showRawType: A = ??? - // TODO[scala3-port]: showTypeSymbol (Scala 2 macro def) (L) - def showTypeSymbol: A = ??? - // TODO[scala3-port]: showTypeSymbolFullName (Scala 2 macro def) (L) - def showTypeSymbolFullName: A = ??? - // TODO[scala3-port]: sourceCode (Scala 2 macro def) (L) - def sourceCode: String = ??? - // TODO[scala3-port]: withSourceCode (Scala 2 macro def) (L) - def withSourceCode: (A, String) = ??? + inline def showAst: A = ${ macros.ShowMacros.showAstImpl[A]('a) } + inline def showRawAst: A = ${ macros.ShowMacros.showRawAstImpl[A]('a) } + inline def showSymbol: A = ${ macros.ShowMacros.showSymbolImpl[A]('a) } + inline def showSymbolFullName: A = ${ macros.ShowMacros.showSymbolFullNameImpl[A]('a) } + inline def showType: A = ${ macros.ShowMacros.showTypeImpl[A]('a) } + inline def showRawType: A = ${ macros.ShowMacros.showRawTypeImpl[A]('a) } + inline def showTypeSymbol: A = ${ macros.ShowMacros.showTypeSymbolImpl[A]('a) } + inline def showTypeSymbolFullName: A = ${ macros.ShowMacros.showTypeSymbolFullNameImpl[A]('a) } + inline def sourceCode: String = ${ macros.ShowMacros.sourceCodeImpl[A]('a) } + inline def withSourceCode: (A, String) = ${ macros.ShowMacros.withSourceCodeImpl[A]('a) } def debugMacro: A = a } diff --git a/core/src/main/scala/com/avsystem/commons/macros/ShowMacros.scala b/core/src/main/scala/com/avsystem/commons/macros/ShowMacros.scala new file mode 100644 index 000000000..7d4f1bc33 --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/macros/ShowMacros.scala @@ -0,0 +1,56 @@ +package com.avsystem.commons.macros + +import scala.quoted.* + +object ShowMacros: + // report.info: print + proceed (Scala 2 used c.error as a hack — Scala 3 has a proper info channel) + def showAstImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(Printer.TreeCode.show(a.asTerm), a.asTerm.pos) + a + + def showRawAstImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(Printer.TreeStructure.show(a.asTerm), a.asTerm.pos) + a + + def showSymbolImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(a.asTerm.symbol.toString, a.asTerm.pos) + a + + def showSymbolFullNameImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(a.asTerm.symbol.fullName, a.asTerm.pos) + a + + def showTypeImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(TypeRepr.of[A].widen.show, a.asTerm.pos) + a + + def showRawTypeImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(Printer.TypeReprStructure.show(TypeRepr.of[A].widen), a.asTerm.pos) + a + + def showTypeSymbolImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(TypeRepr.of[A].typeSymbol.toString, a.asTerm.pos) + a + + def showTypeSymbolFullNameImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(TypeRepr.of[A].typeSymbol.fullName, a.asTerm.pos) + a + + def sourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[String] = + import quotes.reflect.* + val txt = a.asTerm.pos.sourceCode.getOrElse { + report.errorAndAbort("source code unavailable at this position", a.asTerm.pos) + } + Expr(txt) + + def withSourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[(A, String)] = + val src = sourceCodeImpl[A](a) + '{ ($a, $src) } From 0068c2d8814494cefb58abe9a916c3f5bffebfd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 15:13:43 +0200 Subject: [PATCH 02/10] test(core): add smoke test for show*/sourceCode debug macros MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New SharedExtensionsShowTest (10 cases) covering all 8 show* pass-through returns + sourceCode + withSourceCode. - [Rule 1 — Bug] Surfaced by sourceCode test: receiver Expr inside UniversalOps wrapper has no source position (it points at the synthetic AnyVal constructor val). Added Position.ofMacroExpansion fallback so the call site is captured. Documented Scala 3 semantic deviation from Scala 2 (call-site text instead of receiver-only text). --- .../avsystem/commons/macros/ShowMacros.scala | 6 ++- .../commons/SharedExtensionsShowTest.scala | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 core/src/test/scala/com/avsystem/commons/SharedExtensionsShowTest.scala diff --git a/core/src/main/scala/com/avsystem/commons/macros/ShowMacros.scala b/core/src/main/scala/com/avsystem/commons/macros/ShowMacros.scala index 7d4f1bc33..581fdf0c8 100644 --- a/core/src/main/scala/com/avsystem/commons/macros/ShowMacros.scala +++ b/core/src/main/scala/com/avsystem/commons/macros/ShowMacros.scala @@ -46,8 +46,10 @@ object ShowMacros: def sourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[String] = import quotes.reflect.* - val txt = a.asTerm.pos.sourceCode.getOrElse { - report.errorAndAbort("source code unavailable at this position", a.asTerm.pos) + // Prefer the receiver expression's own position; fall back to macro-expansion site. + val pos = a.asTerm.pos + val txt = pos.sourceCode.orElse(Position.ofMacroExpansion.sourceCode).getOrElse { + report.errorAndAbort("source code unavailable at this position", pos) } Expr(txt) diff --git a/core/src/test/scala/com/avsystem/commons/SharedExtensionsShowTest.scala b/core/src/test/scala/com/avsystem/commons/SharedExtensionsShowTest.scala new file mode 100644 index 000000000..90406cc68 --- /dev/null +++ b/core/src/test/scala/com/avsystem/commons/SharedExtensionsShowTest.scala @@ -0,0 +1,54 @@ +package com.avsystem.commons + +import com.avsystem.commons.SharedExtensions.* +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class SharedExtensionsShowTest extends AnyFunSuite with Matchers { + + test("showAst returns receiver unchanged") { + 42.showAst shouldEqual 42 + } + + test("showRawAst returns receiver unchanged") { + "hello".showRawAst shouldEqual "hello" + } + + test("showSymbol returns receiver unchanged") { + 42.showSymbol shouldEqual 42 + } + + test("showSymbolFullName returns receiver unchanged") { + 42.showSymbolFullName shouldEqual 42 + } + + test("showType returns receiver unchanged") { + "x".showType shouldEqual "x" + } + + test("showRawType returns receiver unchanged") { + 42.showRawType shouldEqual 42 + } + + test("showTypeSymbol returns receiver unchanged") { + 42.showTypeSymbol shouldEqual 42 + } + + test("showTypeSymbolFullName returns receiver unchanged") { + 42.showTypeSymbolFullName shouldEqual 42 + } + + test("sourceCode returns literal source text of the call") { + // Scala 3 deviation from Scala 2: because UniversalOps is a wrapper class, + // the macro's receiver Expr is the synthetic `a` constructor val (no source pos); + // we fall back to Position.ofMacroExpansion which captures the full call site. + val src = (1 + 2).sourceCode + src should include("1 + 2") + } + + test("withSourceCode returns (value, source)") { + val (value, src) = (1 + 2).withSourceCode + value shouldEqual 3 + src should include("1 + 2") + } +} From 7b3bc537e8aeadf0fef5b25cc0eae4c9eb7d9fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 15:14:44 +0200 Subject: [PATCH 03/10] docs(migration): remove restored show*/sourceCode backlog entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop 10 backlog rows (SharedExtensions.scala:129-147) covering show*/sourceCode/withSourceCode now implemented via Scala 3 quotes. - Update Total tags counter to 144 (was 155 — actual baseline was 154, see footnote; 154 - 10 = 144). --- MIGRATION.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index b99395701..42a4f69ff 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -107,7 +107,7 @@ Full per-file list with locations is in the Backlog table below (filter rows whe ## Backlog -*Auto-derived from `git grep -nE 'TODO\[scala3-port\]'` on this PR's tip. Total tags: 155.* +*Auto-derived from `git grep -nE 'TODO\[scala3-port\]'` on this PR's tip. Total tags: 144.* | Location | Description | Effort | |---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|--------| @@ -118,16 +118,6 @@ Full per-file list with locations is in the Backlog table below (filter rows whe | `core/jvm/src/test/scala/com/avsystem/commons/macros/TypeClassDerivationTest.scala:4` | TypeClassDerivationTest — depends on TestMacros / scala-2 `def ... = macro ...` | M | | `core/jvm/src/test/scala/com/avsystem/commons/serialization/JCodecTestBase.scala:4` | JCodecTestBase — depends on commented serialization test data / stubbed materialize | M | | `core/jvm/src/test/scala/com/avsystem/commons/serialization/JGenCodecTest.scala:4` | JGenCodecTest — depends on commented serialization test data / stubbed materialize | M | -| `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:129` | showAst (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:131` | showRawAst (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:133` | showSymbol (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:135` | showSymbolFullName (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:137` | showType (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:139` | showRawType (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:141` | showTypeSymbol (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:143` | showTypeSymbolFullName (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:145` | sourceCode (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:147` | withSourceCode (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/annotation/AnnotationAggregate.scala:52` | reifyAggregated (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/annotation/positioned.scala:12` | here (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/di/Components.scala:18` | component (Scala 2 macro def) | L | From 074963fae0dea787f8981fd04be67b1e02a06ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 15:38:16 +0200 Subject: [PATCH 04/10] refactor(core): consolidate show*/sourceCode macro impls into UniversalOpsMacros - Drop separate ShowMacros.scala; impls now in UniversalOpsMacros next to SharedExtensions (still subpackage to avoid implicit-conversion conflict with package object's UniversalOps[A] extension methods) - Use report.info(msg, expr) overload so position derives from the receiver - Simplify sourceCode path: use Position.ofMacroExpansion.sourceCode directly --- .../avsystem/commons/SharedExtensions.scala | 20 +++++------ ...wMacros.scala => UniversalOpsMacros.scala} | 36 ++++++++++--------- 2 files changed, 29 insertions(+), 27 deletions(-) rename core/src/main/scala/com/avsystem/commons/macros/{ShowMacros.scala => UniversalOpsMacros.scala} (56%) diff --git a/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala b/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala index bac197a67..4e02882fe 100644 --- a/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala +++ b/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala @@ -126,16 +126,16 @@ object SharedExtensionsUtils extends SharedExtensions { def uncheckedMatch[B](pf: PartialFunction[A, B]): B = pf.applyOrElse(a, (obj: A) => throw new MatchError(obj)) - inline def showAst: A = ${ macros.ShowMacros.showAstImpl[A]('a) } - inline def showRawAst: A = ${ macros.ShowMacros.showRawAstImpl[A]('a) } - inline def showSymbol: A = ${ macros.ShowMacros.showSymbolImpl[A]('a) } - inline def showSymbolFullName: A = ${ macros.ShowMacros.showSymbolFullNameImpl[A]('a) } - inline def showType: A = ${ macros.ShowMacros.showTypeImpl[A]('a) } - inline def showRawType: A = ${ macros.ShowMacros.showRawTypeImpl[A]('a) } - inline def showTypeSymbol: A = ${ macros.ShowMacros.showTypeSymbolImpl[A]('a) } - inline def showTypeSymbolFullName: A = ${ macros.ShowMacros.showTypeSymbolFullNameImpl[A]('a) } - inline def sourceCode: String = ${ macros.ShowMacros.sourceCodeImpl[A]('a) } - inline def withSourceCode: (A, String) = ${ macros.ShowMacros.withSourceCodeImpl[A]('a) } + inline def showAst: A = ${ macros.UniversalOpsMacros.showAstImpl[A]('a) } + inline def showRawAst: A = ${ macros.UniversalOpsMacros.showRawAstImpl[A]('a) } + inline def showSymbol: A = ${ macros.UniversalOpsMacros.showSymbolImpl[A]('a) } + inline def showSymbolFullName: A = ${ macros.UniversalOpsMacros.showSymbolFullNameImpl[A]('a) } + inline def showType: A = ${ macros.UniversalOpsMacros.showTypeImpl[A]('a) } + inline def showRawType: A = ${ macros.UniversalOpsMacros.showRawTypeImpl[A]('a) } + inline def showTypeSymbol: A = ${ macros.UniversalOpsMacros.showTypeSymbolImpl[A]('a) } + inline def showTypeSymbolFullName: A = ${ macros.UniversalOpsMacros.showTypeSymbolFullNameImpl[A]('a) } + inline def sourceCode: String = ${ macros.UniversalOpsMacros.sourceCodeImpl[A]('a) } + inline def withSourceCode: (A, String) = ${ macros.UniversalOpsMacros.withSourceCodeImpl[A]('a) } def debugMacro: A = a } diff --git a/core/src/main/scala/com/avsystem/commons/macros/ShowMacros.scala b/core/src/main/scala/com/avsystem/commons/macros/UniversalOpsMacros.scala similarity index 56% rename from core/src/main/scala/com/avsystem/commons/macros/ShowMacros.scala rename to core/src/main/scala/com/avsystem/commons/macros/UniversalOpsMacros.scala index 581fdf0c8..6a685dab9 100644 --- a/core/src/main/scala/com/avsystem/commons/macros/ShowMacros.scala +++ b/core/src/main/scala/com/avsystem/commons/macros/UniversalOpsMacros.scala @@ -2,57 +2,59 @@ package com.avsystem.commons.macros import scala.quoted.* -object ShowMacros: - // report.info: print + proceed (Scala 2 used c.error as a hack — Scala 3 has a proper info channel) +object UniversalOpsMacros { def showAstImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = import quotes.reflect.* - report.info(Printer.TreeCode.show(a.asTerm), a.asTerm.pos) + report.info(Printer.TreeCode.show(a.asTerm), a) a def showRawAstImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = import quotes.reflect.* - report.info(Printer.TreeStructure.show(a.asTerm), a.asTerm.pos) + report.info(Printer.TreeStructure.show(a.asTerm), a) a def showSymbolImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = import quotes.reflect.* - report.info(a.asTerm.symbol.toString, a.asTerm.pos) + report.info(a.asTerm.symbol.toString, a) a def showSymbolFullNameImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = import quotes.reflect.* - report.info(a.asTerm.symbol.fullName, a.asTerm.pos) + report.info(a.asTerm.symbol.fullName, a) a def showTypeImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = import quotes.reflect.* - report.info(TypeRepr.of[A].widen.show, a.asTerm.pos) + report.info(TypeRepr.of[A].widen.show, a) a def showRawTypeImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = import quotes.reflect.* - report.info(Printer.TypeReprStructure.show(TypeRepr.of[A].widen), a.asTerm.pos) + report.info(Printer.TypeReprStructure.show(TypeRepr.of[A].widen), a) a def showTypeSymbolImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = import quotes.reflect.* - report.info(TypeRepr.of[A].typeSymbol.toString, a.asTerm.pos) + report.info(TypeRepr.of[A].typeSymbol.toString, a) a def showTypeSymbolFullNameImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = import quotes.reflect.* - report.info(TypeRepr.of[A].typeSymbol.fullName, a.asTerm.pos) + report.info(TypeRepr.of[A].typeSymbol.fullName, a) a - def sourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[String] = + private def captureSourceCode[A: Type](a: Expr[A])(using Quotes): Expr[String] = import quotes.reflect.* - // Prefer the receiver expression's own position; fall back to macro-expansion site. - val pos = a.asTerm.pos - val txt = pos.sourceCode.orElse(Position.ofMacroExpansion.sourceCode).getOrElse { - report.errorAndAbort("source code unavailable at this position", pos) - } + val src = Position.ofMacroExpansion.sourceCode + val txt: String = src match + case Some(s: String) => s + case _ => report.errorAndAbort("source code unavailable at this position", a) Expr(txt) + def sourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[String] = + captureSourceCode[A](a) + def withSourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[(A, String)] = - val src = sourceCodeImpl[A](a) + val src = captureSourceCode[A](a) '{ ($a, $src) } +} From e2370cf4217139dc426550420362d046ef439e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 16:09:18 +0200 Subject: [PATCH 05/10] refactor(core): inline show* macro impls in UniversalOps companion - show* / showRaw* / showType* impls now in `object UniversalOps` (companion of the value-class extension), placed in same file next to their `inline def` - sourceCode / withSourceCode impls remain in a separate file (`commons.macros.UniversalOpsSourceMacros`) because the package object's auto-imported `universalOps` implicit makes `Position.sourceCode` recursively resolve to `UniversalOps.sourceCode` if placed in the same package --- .../avsystem/commons/SharedExtensions.scala | 68 ++++++++++++++++--- .../commons/macros/UniversalOpsMacros.scala | 60 ---------------- .../macros/UniversalOpsSourceMacros.scala | 20 ++++++ 3 files changed, 78 insertions(+), 70 deletions(-) delete mode 100644 core/src/main/scala/com/avsystem/commons/macros/UniversalOpsMacros.scala create mode 100644 core/src/main/scala/com/avsystem/commons/macros/UniversalOpsSourceMacros.scala diff --git a/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala b/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala index 4e02882fe..b64543c91 100644 --- a/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala +++ b/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala @@ -126,20 +126,68 @@ object SharedExtensionsUtils extends SharedExtensions { def uncheckedMatch[B](pf: PartialFunction[A, B]): B = pf.applyOrElse(a, (obj: A) => throw new MatchError(obj)) - inline def showAst: A = ${ macros.UniversalOpsMacros.showAstImpl[A]('a) } - inline def showRawAst: A = ${ macros.UniversalOpsMacros.showRawAstImpl[A]('a) } - inline def showSymbol: A = ${ macros.UniversalOpsMacros.showSymbolImpl[A]('a) } - inline def showSymbolFullName: A = ${ macros.UniversalOpsMacros.showSymbolFullNameImpl[A]('a) } - inline def showType: A = ${ macros.UniversalOpsMacros.showTypeImpl[A]('a) } - inline def showRawType: A = ${ macros.UniversalOpsMacros.showRawTypeImpl[A]('a) } - inline def showTypeSymbol: A = ${ macros.UniversalOpsMacros.showTypeSymbolImpl[A]('a) } - inline def showTypeSymbolFullName: A = ${ macros.UniversalOpsMacros.showTypeSymbolFullNameImpl[A]('a) } - inline def sourceCode: String = ${ macros.UniversalOpsMacros.sourceCodeImpl[A]('a) } - inline def withSourceCode: (A, String) = ${ macros.UniversalOpsMacros.withSourceCodeImpl[A]('a) } + inline def showAst: A = ${ UniversalOps.showAstImpl[A]('a) } + inline def showRawAst: A = ${ UniversalOps.showRawAstImpl[A]('a) } + inline def showSymbol: A = ${ UniversalOps.showSymbolImpl[A]('a) } + inline def showSymbolFullName: A = ${ UniversalOps.showSymbolFullNameImpl[A]('a) } + inline def showType: A = ${ UniversalOps.showTypeImpl[A]('a) } + inline def showRawType: A = ${ UniversalOps.showRawTypeImpl[A]('a) } + inline def showTypeSymbol: A = ${ UniversalOps.showTypeSymbolImpl[A]('a) } + inline def showTypeSymbolFullName: A = ${ UniversalOps.showTypeSymbolFullNameImpl[A]('a) } + inline def sourceCode: String = ${ macros.UniversalOpsSourceMacros.sourceCodeImpl[A]('a) } + inline def withSourceCode: (A, String) = ${ macros.UniversalOpsSourceMacros.withSourceCodeImpl[A]('a) } def debugMacro: A = a } + object UniversalOps { + import scala.quoted.* + + def showAstImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(Printer.TreeCode.show(a.asTerm), a) + a + + def showRawAstImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(Printer.TreeStructure.show(a.asTerm), a) + a + + def showSymbolImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(a.asTerm.symbol.toString, a) + a + + def showSymbolFullNameImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(a.asTerm.symbol.fullName, a) + a + + def showTypeImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(TypeRepr.of[A].widen.show, a) + a + + def showRawTypeImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(Printer.TypeReprStructure.show(TypeRepr.of[A].widen), a) + a + + def showTypeSymbolImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(TypeRepr.of[A].typeSymbol.toString, a) + a + + def showTypeSymbolFullNameImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = + import quotes.reflect.* + report.info(TypeRepr.of[A].typeSymbol.fullName, a) + a + + // sourceCode / withSourceCode impls live in `commons.macros.UniversalOpsSourceMacros` + // (must be in a different package to avoid `Position.sourceCode` resolving via the + // package-object auto-imported `universalOps` implicit conversion → infinite macro recursion) + } + class LazyUniversalOps[A](private val a: () => A) extends AnyVal { def evalFuture: Future[A] = FutureCompanionOps.eval(a()) diff --git a/core/src/main/scala/com/avsystem/commons/macros/UniversalOpsMacros.scala b/core/src/main/scala/com/avsystem/commons/macros/UniversalOpsMacros.scala deleted file mode 100644 index 6a685dab9..000000000 --- a/core/src/main/scala/com/avsystem/commons/macros/UniversalOpsMacros.scala +++ /dev/null @@ -1,60 +0,0 @@ -package com.avsystem.commons.macros - -import scala.quoted.* - -object UniversalOpsMacros { - def showAstImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = - import quotes.reflect.* - report.info(Printer.TreeCode.show(a.asTerm), a) - a - - def showRawAstImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = - import quotes.reflect.* - report.info(Printer.TreeStructure.show(a.asTerm), a) - a - - def showSymbolImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = - import quotes.reflect.* - report.info(a.asTerm.symbol.toString, a) - a - - def showSymbolFullNameImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = - import quotes.reflect.* - report.info(a.asTerm.symbol.fullName, a) - a - - def showTypeImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = - import quotes.reflect.* - report.info(TypeRepr.of[A].widen.show, a) - a - - def showRawTypeImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = - import quotes.reflect.* - report.info(Printer.TypeReprStructure.show(TypeRepr.of[A].widen), a) - a - - def showTypeSymbolImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = - import quotes.reflect.* - report.info(TypeRepr.of[A].typeSymbol.toString, a) - a - - def showTypeSymbolFullNameImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = - import quotes.reflect.* - report.info(TypeRepr.of[A].typeSymbol.fullName, a) - a - - private def captureSourceCode[A: Type](a: Expr[A])(using Quotes): Expr[String] = - import quotes.reflect.* - val src = Position.ofMacroExpansion.sourceCode - val txt: String = src match - case Some(s: String) => s - case _ => report.errorAndAbort("source code unavailable at this position", a) - Expr(txt) - - def sourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[String] = - captureSourceCode[A](a) - - def withSourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[(A, String)] = - val src = captureSourceCode[A](a) - '{ ($a, $src) } -} diff --git a/core/src/main/scala/com/avsystem/commons/macros/UniversalOpsSourceMacros.scala b/core/src/main/scala/com/avsystem/commons/macros/UniversalOpsSourceMacros.scala new file mode 100644 index 000000000..c3048f518 --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/macros/UniversalOpsSourceMacros.scala @@ -0,0 +1,20 @@ +package com.avsystem.commons.macros + +import scala.quoted.* + +// Separate package to bypass package-object auto-import of `universalOps`, +// which would otherwise make `Position.sourceCode` recursively resolve to +// `UniversalOps.sourceCode` (this very macro). +object UniversalOpsSourceMacros { + def sourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[String] = + import quotes.reflect.* + val pos = a.asTerm.pos + val txt = pos.sourceCode.orElse(Position.ofMacroExpansion.sourceCode).getOrElse( + report.errorAndAbort("source code unavailable at this position", a) + ) + Expr(txt) + + def withSourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[(A, String)] = + val src = sourceCodeImpl[A](a) + '{ ($a, $src) } +} From 74d156bad97c51b46e36a88ab6ea063a5875e4c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 16:39:25 +0200 Subject: [PATCH 06/10] refactor(core): remove UniversalOpsSourceMacros, inline sourceCode/withSourceCode impls in UniversalOps companion and update comments/docs --- .../avsystem/commons/SharedExtensions.scala | 61 +++++++++++-------- .../macros/UniversalOpsSourceMacros.scala | 20 ------ 2 files changed, 36 insertions(+), 45 deletions(-) delete mode 100644 core/src/main/scala/com/avsystem/commons/macros/UniversalOpsSourceMacros.scala diff --git a/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala b/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala index b64543c91..16aa84c33 100644 --- a/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala +++ b/core/src/main/scala/com/avsystem/commons/SharedExtensions.scala @@ -63,7 +63,7 @@ object SharedExtensionsUtils extends SharedExtensions { * avoiding intermediate variables. * * @example - * {{{someVeryLongExpression() |> (v => if(condition(v)) something(v) else somethingElse(v))}}} + * {{{someVeryLongExpression() |> (v => if(condition(v)) something(v) else somethingElse(v))}}} */ def |>[B](f: A => B): B = f(a) @@ -96,14 +96,14 @@ object SharedExtensionsUtils extends SharedExtensions { * more clarity and avoids polluting outer scope. * * @example - * {{{ + * {{{ * import javax.swing._ * // this entire expression returns the panel * new JPanel().setup { p => * p.setEnabled(true) * p.setSize(100, 100) * } - * }}} + * }}} */ def setup(code: A => Any): A = { code(a) @@ -117,11 +117,11 @@ object SharedExtensionsUtils extends SharedExtensions { * checking. * * @example - * {{{ + * {{{ * Option(42) uncheckedMatch { * case Some(int) => println(int) * } - * }}} + * }}} */ def uncheckedMatch[B](pf: PartialFunction[A, B]): B = pf.applyOrElse(a, (obj: A) => throw new MatchError(obj)) @@ -134,13 +134,14 @@ object SharedExtensionsUtils extends SharedExtensions { inline def showRawType: A = ${ UniversalOps.showRawTypeImpl[A]('a) } inline def showTypeSymbol: A = ${ UniversalOps.showTypeSymbolImpl[A]('a) } inline def showTypeSymbolFullName: A = ${ UniversalOps.showTypeSymbolFullNameImpl[A]('a) } - inline def sourceCode: String = ${ macros.UniversalOpsSourceMacros.sourceCodeImpl[A]('a) } - inline def withSourceCode: (A, String) = ${ macros.UniversalOpsSourceMacros.withSourceCodeImpl[A]('a) } + inline def sourceCode: String = ${ UniversalOps.sourceCodeImpl[A]('a) } + inline def withSourceCode: (A, String) = ${ UniversalOps.withSourceCodeImpl[A]('a) } def debugMacro: A = a } object UniversalOps { + import scala.quoted.* def showAstImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] = @@ -183,9 +184,19 @@ object SharedExtensionsUtils extends SharedExtensions { report.info(TypeRepr.of[A].typeSymbol.fullName, a) a - // sourceCode / withSourceCode impls live in `commons.macros.UniversalOpsSourceMacros` - // (must be in a different package to avoid `Position.sourceCode` resolving via the - // package-object auto-imported `universalOps` implicit conversion → infinite macro recursion) + def sourceCodeImpl[A: Type](a: Expr[A])(using quotes: Quotes): Expr[String] = + import quotes.reflect.* + import quotes.reflect.PositionMethods // to bypass package-object auto-import of `universalOps`, + + val pos = a.asTerm.pos + val txt = PositionMethods.sourceCode(pos).orElse(PositionMethods.sourceCode(Position.ofMacroExpansion)).getOrElse( + report.errorAndAbort("source code unavailable at this position", a) + ) + Expr(txt) + + def withSourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[(A, String)] = + val src = sourceCodeImpl[A](a) + '{ ($a, $src) } } class LazyUniversalOps[A](private val a: () => A) extends AnyVal { @@ -199,13 +210,13 @@ object SharedExtensionsUtils extends SharedExtensions { def optionIf(condition: Boolean): Option[A] = if (condition) Some(a()) else None - def recoverFrom[T <: Throwable: ClassTag](fallbackValue: => A): A = + def recoverFrom[T <: Throwable : ClassTag](fallbackValue: => A): A = try a() catch { case _: T => fallbackValue } - def recoverToOpt[T <: Throwable: ClassTag]: Opt[A] = + def recoverToOpt[T <: Throwable : ClassTag]: Opt[A] = try Opt(a()) catch { case _: T => Opt.Empty @@ -380,17 +391,17 @@ object SharedExtensionsUtils extends SharedExtensions { * useful for performing a parallel map. For example, to apply a function to all items of a list * * @tparam A - * the type of the value inside the Futures in the `IterableOnce` + * the type of the value inside the Futures in the `IterableOnce` * @tparam B - * the type of the value of the returned `Future` + * the type of the value of the returned `Future` * @tparam M - * the type of the `IterableOnce` of Futures + * the type of the `IterableOnce` of Futures * @param in - * the `IterableOnce` of Futures which will be sequenced + * the `IterableOnce` of Futures which will be sequenced * @param fn - * the function to apply to the `IterableOnce` of Futures to produce the results + * the function to apply to the `IterableOnce` of Futures to produce the results * @return - * the `Future` of the `IterableOnce` of results + * the `Future` of the `IterableOnce` of results */ def traverseCompleted[A, B, M[X] <: IterableOnce[X]]( in: M[A] @@ -410,13 +421,13 @@ object SharedExtensionsUtils extends SharedExtensions { * which only completes after all `in` `Future`s are completed. * * @tparam A - * the type of the value inside the Futures + * the type of the value inside the Futures * @tparam M - * the type of the `IterableOnce` of Futures + * the type of the `IterableOnce` of Futures * @param in - * the `IterableOnce` of Futures which will be sequenced + * the `IterableOnce` of Futures which will be sequenced * @return - * the `Future` of the `IterableOnce` of results + * the `Future` of the `IterableOnce` of results */ def sequenceCompleted[A, M[X] <: IterableOnce[X]]( in: M[Future[A]] @@ -530,7 +541,7 @@ object SharedExtensionsUtils extends SharedExtensions { def sequence[A, M[X] <: IterableOnce[X]](in: M[Try[A]])(implicit bf: BuildFrom[M[Try[A]], A, M[A]]): Try[M[A]] = in.iterator .foldLeft(Try(bf.newBuilder(in))) { - case (f @ Failure(e), Failure(newEx)) => e.addSuppressed(newEx); f + case (f@Failure(e), Failure(newEx)) => e.addSuppressed(newEx); f case (tr, tb) => for { r <- tr @@ -547,11 +558,11 @@ object SharedExtensionsUtils extends SharedExtensions { * }}} */ def traverse[A, B, M[X] <: IterableOnce[X]](in: M[A])(fn: A => Try[B])(implicit bf: BuildFrom[M[A], B, M[B]]) - : Try[M[B]] = + : Try[M[B]] = in.iterator .map(fn) .foldLeft(Try(bf.newBuilder(in))) { - case (f @ Failure(e), Failure(newEx)) => e.addSuppressed(newEx); f + case (f@Failure(e), Failure(newEx)) => e.addSuppressed(newEx); f case (tr, tb) => for { r <- tr diff --git a/core/src/main/scala/com/avsystem/commons/macros/UniversalOpsSourceMacros.scala b/core/src/main/scala/com/avsystem/commons/macros/UniversalOpsSourceMacros.scala deleted file mode 100644 index c3048f518..000000000 --- a/core/src/main/scala/com/avsystem/commons/macros/UniversalOpsSourceMacros.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.avsystem.commons.macros - -import scala.quoted.* - -// Separate package to bypass package-object auto-import of `universalOps`, -// which would otherwise make `Position.sourceCode` recursively resolve to -// `UniversalOps.sourceCode` (this very macro). -object UniversalOpsSourceMacros { - def sourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[String] = - import quotes.reflect.* - val pos = a.asTerm.pos - val txt = pos.sourceCode.orElse(Position.ofMacroExpansion.sourceCode).getOrElse( - report.errorAndAbort("source code unavailable at this position", a) - ) - Expr(txt) - - def withSourceCodeImpl[A: Type](a: Expr[A])(using Quotes): Expr[(A, String)] = - val src = sourceCodeImpl[A](a) - '{ ($a, $src) } -} From 36303f5d84141421f4ceace6a9cb7ee459754df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 16:49:28 +0200 Subject: [PATCH 07/10] refactor(core): enrich show* output, drop redundant variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - showAst now uses treeInfo (Structure + ShortCode) — subsumes showRawAst - showSymbol now uses symbolInfo (multi-line dump: fullName, flags, fields, methods, signature, etc.) — subsumes showSymbolFullName - showType now uses typeReprInfo (widen, dealias, baseClasses, typeArgs, ...) - showTypeSymbol now uses symbolInfo on typeSymbol — subsumes showTypeSymbolFullName - showRawType kept for raw TypeReprStructure dump - expand SharedExtensionsShowTest from 10 -> 65 tests covering primitives, collections, ADTs, generics, single-eval, chaining, source-code capture --- .idea/codeStyles/Project.xml | 18 -- .../avsystem/commons/SharedExtensions.scala | 143 ++++++---- .../commons/SharedExtensionsShowTest.scala | 262 ++++++++++++++++-- 3 files changed, 332 insertions(+), 91 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index cb14e62c6..095539431 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,23 +1,5 @@ - - - -