Skip to content

0.4.0 - Method Builder Chain, DestructuredExpr, AnonymousInstance, ExprCodec.derived, Union Soundness, and Call-Site Scopes

Latest

Choose a tag to compare

@MateuszKubuszok MateuszKubuszok released this 26 Jun 18:04
· 2 commits to master since this release
e676b5b

After the 0.3.x line wrapped up with the "more exciting things to come in 0.4.0!" promise, here it is — the release where Hearth grew the building blocks for real derivation and metaprogramming libraries. 0.4.0 is a breaking feature release: it reworks the Method API into a typed builder chain, adds semantic expression decomposition (DestructuredExpr), compile-time anonymous instantiation (AnonymousInstance), semi-automatic ExprCodec.derived, and a call-site-aware lexical scope primitive — alongside a serious soundness pass on union types and a batch of cross-platform bug fixes. Most of this work was driven by, and verified end-to-end against, the Kindlings and refined-compat libraries.

Highlights

  • Method builder chain — a reworked, typed Method API (#272)

    • The old Method[+Instance, Returned] is replaced by an unparameterized Method sealed trait with a four-step builder chain: OnInstanceApplyTypesApplyValuesResult[R].
    • Type-parametric (polymorphic) methods are now representable; adds path-dependency detection, fold/foldF, prettyPrint, visibility scoping, modifier flags, type bounds, and clause interleaving.
    • Calling methods, folding over them, and pattern matching on them are all documented with runnable examples.
  • DestructuredExpr — semantic expression decomposition (#272)

    • Decomposes a macro expression into resolved nodes: MethodCall, Lambda, ParamRef, Literal, Singleton, Block, Varargs, and NonDestructurable.
    • Constructor calls (new Foo(args)) are parsed as MethodCalls, enabling cross-platform annotation/constructor-argument destructuring with no platform-specific code.
  • AnonymousInstance — compile-time anonymous subtype instantiation (#272, #287, #289)

    • Instantiate anonymous subtypes of a trait/class at macro-expansion time with validation, diamond-conflict detection, and constructor selection.
    • Supports overloaded and generic/polymorphic abstract-method overrides, symbolic/operator names, implicit/using clauses, abstract val targets, this.type (fluent) returns, by-name parameters, and context-function returns — surfaced through OverrideContext (returnType, typeParameters, returnsThisType). These were the building blocks Kindlings needed for ScalaMock-style mocking.
  • ExprCodec.derived[A] + semiQuote (#272)

    • ExprCodec.derived[A] — semi-automatic ExprCodec derivation for case classes, sealed traits, and singletons, using override maps from implicit scope.
    • semiQuote — converts runtime values back into expression trees (the inverse of Expr.semiEval); semiEval gains an EvalOverride overload for custom per-type dispatch.
    • toExpr on an unquotable type now reports a clean positioned diagnostic instead of throwing an AssertionError (#287).
  • Union types: soundness fixes & TypeTest fallback (#286)

    • Soundness fix: union member disjointness was unsound (List[Int] | Seq[String] could dispatch to the wrong branch). It is now checked via compiler class symbols (classSymbol + derivesFrom + boxed-symbol normalization), which also works for types compiled in the same run.
    • Singleton union members (Color.Red.type | Color.Blue.type) are matched by value via eqValue instead of being refused.
    • Same-erasure unions (List[Int] | List[String]) work when user-provided scala.reflect.TypeTest givens are in scope; refusals now name the missing instance.
  • New type/annotation/parameter APIs (@since 0.4.0) (#286)

    • Type.opaqueUnderlyingType — resolve the underlying type behind a Scala 3 opaque alias (with type-arg re-substitution for applied parameterized opaques).
    • Type.decompose1/decompose2 (returning live, re-applicable Ctors), Type.typeArguments, sameTypeConstructorAs on all CtorN/CtorK1/UntypedType, plus untyped dealias/typeConstructor (#284).
    • annotationsOfType[Ann] / hasAnnotationOfType[Ann] on Type/Method/Parameter, and Annotations.constructorArguments / decodedConstructorArguments (#283).
    • Parameter.isVararg (with cross-platform Seq[A] normalization, A* rendering, vararg splicing, and Java varargs support); fixed Scala 3 Parameter.isByName always returning false; new Parameter.byNameUnderlying (#289).
  • Call-site scopes: enclosingScope & source text (#287)

    • enclosingScope: NonEmptyVector[Enclosure] — the lexical enclosure chain from the current scope outwards (Method / Value / Class / Object / Package), each with name / fullName / position; Class and Object expose callable members plus a receiver. Enables macwire-style dependency injection and other call-site-aware macros. (Local vals inside the enclosing method body are populated on Scala 2; Scala 3 returns Nil as a documented follow-up.)
    • Position.sourceCode and Expr/UntypedExpr .position/.sourceCode, plus a runtime Text[T] materializer — the primitives expecty/munit-style assert macros need.
    • Positioned reporting overloads: reportInfo/reportWarn/reportError/reportErrorAndAbort(msg, position).
  • Standard-extensions performance: zero-closure codegen (#291)

    • IsCollectionOf.foreach now defaults to a zero-closure while loop (was iterable.foreach(<Function1>), allocating a closure per iteration for List/Vector/Map/Set).
    • IsEitherProviderForScalaEither.fold now generates a zero-closure match via MatchCase instead of either.fold(<closure>, <closure>) — enabling allocation-free fail-fast decoders.
    • MatchCase.matchOn (Scala 3) re-owns case bodies to the splice owner, fixing "Block contains definitions with different owners" for lambda-bearing case bodies.
    • IsCollectionOf.asMap: Option[IsMapOf[CollA, Item]] — typed map-ness check without a second IsCollection.parse or an erasure-unsound isInstanceOf.
  • Breaking changes for 0.4.0

    • CaseClass.caseFieldValuesAt's default visibility changed from Anywhere to AtCallSite — the derivation-safe default, so generated instance.field access compiles at the expansion point. Anywhere (all fields, for enumeration) and Everywhere (public only) remain available. (#287)
    • Accessible.Unrestricted was renamed to Accessible.Anywhere. (#287)
    • The Method[+Instance, Returned] API was replaced by the unparameterized Method builder chain (see above). (#272)
  • Cross-platform bug fixes

    • Self-referential implicit Type/Ctor definitions (implicit val ConfigT: Type[Configuration] = Type.of[Configuration]) no longer self-reference in cross-quotes — fixes a silent wrong-code/NPE case on Scala 2 (#285).
    • Scala 3 annotation expressions are now spliceable under -Xcheck-macros (trees rebuilt with proper spans); Scala 2 ExprCodec exprs no longer report the codec's free type param; Scala 3 no-pos reportWarn now emits a real warning.
    • Type.parents was broken on both platforms (Scala 2 infinite recursion; Scala 3 NoSuchMethodException on 3.3.x) — now derived from linearization/ClassInfoType.
    • Scala 3.8.4 compatibility: safeMemberType wrapper for TypeRepr.memberType, default-value handling on generic case classes, and context-function-return desugaring fixes.
  • Hearth-based libraries to learn from

    • Kindlings — a repository showing how Hearth features are used in practice (Show, Schema, codecs, mocking, DI, and more). The newest release based on Hearth 0.4.0 is 0.3.0.
    • refined-compat — a Hearth-based macro library that lets you migrate Refined codebases to Scala 3. Now released as 0.2.0.
  • Build & CI

    • Build simplified further via sbt-kubuszok 0.2.x; scala-steward pinned to Scala 3.3 LTS + sbt 1.x; scala-yaml bumped to 0.3.2; routine GitHub Actions/dependency bumps.

Summary

0.4.0 turns Hearth from a metaprogramming toolkit into a foundation for real derivation and code-generation libraries: a typed Method builder chain, semantic DestructuredExpr decomposition, compile-time AnonymousInstance instantiation, ExprCodec.derived with semiQuote, sound union dispatch with a TypeTest fallback, a call-site-aware enclosingScope primitive, and zero-closure standard-extension codegen. It is a breaking release (notably the Method API rework and the caseFieldValuesAt default change), and almost every feature was hardened against real downstream use in Kindlings and refined-compat.

Be sure to look at our documentation, star the project ⭐ and leave us some feedback!