Repository: scalacenter/tasty-query
Pull request #443 (2024-10-02 08:53:55)

Upgrade to Scala 3.5.0 and support its TASTy format.
Changes notably include:

* Support `Quote` and `QuotePattern` nodes.
* Support `FlexibleType`s.


Patch:
diff --git a/build.sbt b/build.sbt
index 2138d22..28035d7 100644
--- a/build.sbt
+++ b/build.sbt
@@ -3,7 +3,7 @@ import sbt.internal.util.ManagedLogger
 
 import org.scalajs.jsenv.nodejs.NodeJSEnv
 
-val usedScalaCompiler = "3.4.0"
+val usedScalaCompiler = "3.5.0"
 val usedTastyRelease = usedScalaCompiler
 val scala2Version = "2.13.14"
 
@@ -52,7 +52,7 @@ val strictCompileSettings = Seq(
   scalacOptions ++= Seq(
     "-Xfatal-warnings",
     "-Yexplicit-nulls",
-    "-Ysafe-init",
+    "-Wsafe-init",
     "-source:future",
   ),
 )
@@ -129,7 +129,8 @@ lazy val tastyQuery =
         )
       },
 
-      tastyMiMaPreviousArtifacts := mimaPreviousArtifacts.value,
+      // Temporarily disabled until we have a published version of tasty-query that can handle 3.4.x.
+      //tastyMiMaPreviousArtifacts := mimaPreviousArtifacts.value,
       tastyMiMaConfig ~= { prev =>
         import tastymima.intf._
         prev
diff --git a/tasty-query/shared/src/main/scala/tastyquery/Definitions.scala b/tasty-query/shared/src/main/scala/tastyquery/Definitions.scala
index 28a298f..b4787c1 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/Definitions.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/Definitions.scala
@@ -43,6 +43,8 @@ final class Definitions private[tastyquery] (
     scalaPackage.getPackageDeclOrCreate(termName("compiletime"))
   private val scalaCollectionImmutablePackage =
     scalaCollectionPackage.getPackageDeclOrCreate(termName("immutable"))
+  private val scalaQuotedPackage =
+    scalaPackage.getPackageDeclOrCreate(termName("quoted"))
   private val scalaRuntimePackage =
     scalaPackage.getPackageDeclOrCreate(termName("runtime"))
 
@@ -439,6 +441,8 @@ final class Definitions private[tastyquery] (
   lazy val SeqClass = scalaCollectionImmutablePackage.requiredClass("Seq")
   lazy val Function0Class = scalaPackage.requiredClass("Function0")
 
+  private[tastyquery] lazy val ContextFunction1Class = scalaPackage.requiredClass("ContextFunction1")
+
   def FunctionNClass(n: Int): ClassSymbol =
     withRestrictedContext(scalaPackage.findDecl(typeName(s"Function$n")).asClass)
 
@@ -482,6 +486,9 @@ final class Definitions private[tastyquery] (
 
   private[tastyquery] lazy val PolyFunctionClass = scalaPackage.optionalClass("PolyFunction")
 
+  private[tastyquery] lazy val QuotedExprClass = scalaQuotedPackage.requiredClass("Expr")
+  private[tastyquery] lazy val QuotesClass = scalaQuotedPackage.requiredClass("Quotes")
+
   private[tastyquery] def isPolyFunctionSub(tpe: Type)(using Context): Boolean =
     PolyFunctionClass.exists(cls => tpe.baseType(cls).isDefined)
 
diff --git a/tasty-query/shared/src/main/scala/tastyquery/Erasure.scala b/tasty-query/shared/src/main/scala/tastyquery/Erasure.scala
index df6d3ee..054ce8d 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/Erasure.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/Erasure.scala
@@ -111,6 +111,8 @@ private[tastyquery] object Erasure:
         preErase(tpe.parent, keepUnit)
       case tpe: RecType =>
         preErase(tpe.parent, keepUnit)
+      case tpe: FlexibleType =>
+        preErase(tpe.nonNullableType, keepUnit)
       case _: ByNameType =>
         defn.Function0Class.erasure
       case tpe: RepeatedType =>
diff --git a/tasty-query/shared/src/main/scala/tastyquery/Printers.scala b/tasty-query/shared/src/main/scala/tastyquery/Printers.scala
index 042ba16..82c412e 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/Printers.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/Printers.scala
@@ -77,6 +77,9 @@ private[tastyquery] object Printers:
         print("[")
         printCommaSeparatedList(tpe.args)(print(_))
         print("]")
+      case tpe: FlexibleType =>
+        print(tpe.nonNullableType)
+        print("?")
       case tpe: ByNameType =>
         print("=> ")
         print(tpe.resultType)
@@ -445,6 +448,32 @@ private[tastyquery] object Printers:
           print(c)
           print(">")
         printBlock(bindings, expr)(print(_))
+
+      case Quote(body, bodyType) =>
+        print("'[")
+        print(bodyType)
+        print("]")
+        printBlock(Nil, body)(print(_))
+
+      case Splice(expr, spliceType) =>
+        print("$[")
+        print(spliceType)
+        print("]")
+        printBlock(Nil, expr)(print(_))
+
+      case SplicePattern(pattern, targs, args, spliceType) =>
+        print("$[")
+        print(spliceType)
+        print("]")
+        print(pattern)
+        if targs.nonEmpty then
+          print("[")
+          printCommaSeparatedList(targs)(print(_))
+          print("]")
+        if args.nonEmpty then
+          print("(")
+          printCommaSeparatedList(args)(print(_))
+          print(")")
     end print
 
     def print(caze: CaseDef): Unit =
@@ -577,6 +606,26 @@ private[tastyquery] object Printers:
 
       case ExprPattern(expr) =>
         print(expr)
+
+      case QuotePattern(bindings, body, quotes, patternType) =>
+        print("'<")
+        print(quotes)
+        print(">")
+        body match
+          case Left(termBody) =>
+            print("{ ")
+            for binding <- bindings do
+              print(binding)
+              print("; ")
+            print(termBody)
+            print(" }")
+          case Right(typeBody) =>
+            print("[ ")
+            for binding <- bindings do
+              print(binding)
+              print("; ")
+            print(typeBody)
+            print(" ]")
     end print
 
     def print(tree: TypeTree): Unit = tree match
diff --git a/tasty-query/shared/src/main/scala/tastyquery/Subtyping.scala b/tasty-query/shared/src/main/scala/tastyquery/Subtyping.scala
index 9b2d95d..21b28d6 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/Subtyping.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/Subtyping.scala
@@ -238,6 +238,9 @@ private[tastyquery] object Subtyping:
       isSubType(tp1, tp2.first) || isSubType(tp1, tp2.second)
         || level4(tp1, tp2)
 
+    case tp2: FlexibleType =>
+      isSubType(tp1, tp2.nullableType)
+
     case tp2: ByNameType =>
       tp1 match
         case tp1: ByNameType => isSubType(tp1.resultType, tp2.resultType)
@@ -478,6 +481,9 @@ private[tastyquery] object Subtyping:
     case tp1: RecType =>
       isSubType(tp1.parent, tp2)
 
+    case tp1: FlexibleType =>
+      isSubType(tp1.nonNullableType, tp2)
+
     case tp1: AndType =>
       // TODO Try and simplify first
       isSubType(tp1.first, tp2) || isSubType(tp1.second, tp2)
diff --git a/tasty-query/shared/src/main/scala/tastyquery/Traversers.scala b/tasty-query/shared/src/main/scala/tastyquery/Traversers.scala
index d242760..76842b8 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/Traversers.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/Traversers.scala
@@ -120,6 +120,14 @@ object Traversers:
         traverse(expr)
         traverse(caller)
         traverse(bindings)
+      case Quote(body, bodyType) =>
+        traverse(body)
+      case Splice(expr, spliceType) =>
+        traverse(expr)
+      case SplicePattern(pattern, targs, args, spliceType) =>
+        traverse(pattern)
+        traverse(targs)
+        traverse(args)
       case _: ImportIdent | _: Ident | _: This | _: Literal =>
         ()
 
@@ -139,6 +147,10 @@ object Traversers:
         traverse(expr)
       case WildcardPattern(tpe) =>
         ()
+      case QuotePattern(bindings, body, quotes, patternType) =>
+        traverse(bindings)
+        body.fold(traverse(_), traverse(_))
+        traverse(quotes)
 
       // TypeTree-ish
       case TypeIdent(tpe) =>
diff --git a/tasty-query/shared/src/main/scala/tastyquery/Trees.scala b/tasty-query/shared/src/main/scala/tastyquery/Trees.scala
index 8158f02..1d5fca3 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/Trees.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/Trees.scala
@@ -638,6 +638,33 @@ object Trees {
     override final def withPos(pos: SourcePosition): ExprPattern = ExprPattern(expr)(pos)
   end ExprPattern
 
+  /** A tree representing a quote pattern `'{ type binding1; ...; body }` or `'[ type binding1; ...; body ]`.
+    *
+    * The `bindings` contain the list of quote pattern type variable definitions (`TypeTreeBind`s)
+    * in the order in which they are defined in the source.
+    *
+    * @param bindings
+    *   Type variable definitions
+    * @param body
+    *   Quoted pattern (without type variable definitions):
+    *   `Left(termTree)` for a term quote pattern `'{ ... }` or
+    *   `Right(typeTree)` for a type quote pattern `'[ ... ]`
+    * @param quotes
+    *   A reference to the given `Quotes` instance in scope
+    * @param patternType
+    *   The type of the pattern
+    */
+  final case class QuotePattern(
+    bindings: List[TypeTreeBind],
+    body: Either[TermTree, TypeTree],
+    quotes: TermTree,
+    patternType: Type
+  )(pos: SourcePosition)
+      extends PatternTree(pos) {
+    override def withPos(pos: SourcePosition): QuotePattern =
+      QuotePattern(bindings, body, quotes, patternType)(pos)
+  }
+
   /** Seq(elems)
     *  @param  tpt  The element type of the sequence.
     */
@@ -717,6 +744,65 @@ object Trees {
     override final def withPos(pos: SourcePosition): Inlined = Inlined(expr, caller, bindings)(pos)
   }
 
+  /** A tree representing a quote `'{ body }`.
+    *
+    * @param body
+    *   The tree that was quoted
+    * @param bodyType
+    *   Explicit type of quoted body, which is the source of truth from which we build the type of the quote
+    */
+  final case class Quote(body: TermTree, bodyType: Type)(pos: SourcePosition) extends TermTree(pos) {
+    protected final def calculateType(using Context): TermType =
+      // `Quotes ?=> Expr[bodyType]`
+      defn.ContextFunction1Class.staticRef.appliedTo(
+        List(defn.QuotesClass.staticRef, defn.QuotedExprClass.staticRef.appliedTo(bodyType))
+      )
+    end calculateType
+
+    override final def withPos(pos: SourcePosition): Quote =
+      Quote(body, bodyType)(pos)
+  }
+
+  /** A tree representing a splice `${ expr }`.
+    *
+    * @param expr
+    *   The tree that was spliced
+    */
+  final case class Splice(expr: TermTree, spliceType: Type)(pos: SourcePosition) extends TermTree(pos) {
+    protected final def calculateType(using Context): TermType =
+      spliceType
+
+    override final def withPos(pos: SourcePosition): Splice =
+      Splice(expr, spliceType)(pos)
+  }
+
+  /** A tree representing a pattern splice `${ pattern }`, `$ident` or `$ident(args*)` in a quote pattern.
+    *
+    * Parser will only create `${ pattern }` and `$ident`, hence they will not have args.
+    * While typing, the `$ident(args*)` the args are identified and desugared into a `SplicePattern`
+    * containing them.
+    *
+    * `SplicePattern` can only be contained within a `QuotePattern`.
+    *
+    * @param pattern
+    *   The pattern that was spliced
+    * @param targs
+    *   The type arguments of the splice (the HOAS arguments)
+    * @param args
+    *   The arguments of the splice (the HOAS arguments)
+    * @param spliceType
+    *   The type of the splice, i.e., of this tree
+    */
+  final case class SplicePattern(pattern: PatternTree, targs: List[TypeTree], args: List[TermTree], spliceType: Type)(
+    pos: SourcePosition
+  ) extends TermTree(pos) {
+    protected final def calculateType(using Context): TermType =
+      spliceType
+
+    override final def withPos(pos: SourcePosition): SplicePattern =
+      SplicePattern(pattern, targs, args, spliceType)(pos)
+  }
+
   // --- TypeTrees ------------------------------------------------------------
 
   sealed abstract class TypeArgTree(pos: SourcePosition) extends Tree(pos):
diff --git a/tasty-query/shared/src/main/scala/tastyquery/TypeMaps.scala b/tasty-query/shared/src/main/scala/tastyquery/TypeMaps.scala
index a97ad10..f674eb7 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/TypeMaps.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/TypeMaps.scala
@@ -53,6 +53,8 @@ private[tastyquery] object TypeMaps {
       tp.derivedAnnotatedType(underlying, annot)
     protected def derivedMatchType(tp: MatchType, bound: Type, scrutinee: Type, cases: List[MatchTypeCase]): Type =
       tp.derivedMatchType(bound, scrutinee, cases)
+    protected def derivedFlexibleType(tp: FlexibleType, nonNullableType: Type): Type =
+      tp.derivedFlexibleType(nonNullableType)
     protected def derivedByNameType(tp: ByNameType, restpe: Type): Type =
       tp.derivedByNameType(restpe)
     protected def derivedRepeatedType(tp: RepeatedType, elemType: Type): Type =
@@ -123,6 +125,9 @@ private[tastyquery] object TypeMaps {
         case tp: TypeLambda =>
           mapOverLambda(tp)
 
+        case tp: FlexibleType =>
+          derivedFlexibleType(tp, this(tp.nonNullableType))
+
         case tp: ByNameType =>
           derivedByNameType(tp, this(tp.resultType))
 
diff --git a/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala b/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala
index cf4cf6b..a7d0a0f 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/TypeOps.scala
@@ -114,6 +114,8 @@ private[tastyquery] object TypeOps:
         apply(z, tp.thistpe)
       case tp: AppliedType =>
         tp.args.foldLeft(apply(z, tp.tycon))(this)
+      case tp: FlexibleType =>
+        apply(z, tp.nonNullableType)
       case tp: ByNameType =>
         apply(z, tp.resultType)
       case tp: RepeatedType =>
diff --git a/tasty-query/shared/src/main/scala/tastyquery/Types.scala b/tasty-query/shared/src/main/scala/tastyquery/Types.scala
index 19919f4..9dd72f0 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/Types.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/Types.scala
@@ -58,6 +58,7 @@ import tastyquery.Utils.*
   *          +- AppliedType          `C[T1, ..., Tn]`
   *          +- ByNameType            type of a by-name parameter `=> T`
   *          +- ThisType             `C.this`
+  *          +- FlexibleType         `U?`
   *          +- OrType               `A | B`
   *          +- AndType              `A & B`
   *          +- TypeLambda           `[T1, ..., Tn] => R`
@@ -579,7 +580,8 @@ object Types {
           self.superType.typeParams
         case self: AnnotatedType =>
           self.superType.typeParams
-        case _: SingletonType | _: RefinedType | _: ByNameType | _: RepeatedType | _: MatchType | _: RecType =>
+        case _: SingletonType | _: RefinedType | _: FlexibleType | _: ByNameType | _: RepeatedType | _: MatchType |
+            _: RecType =>
           // These types are always proper types
           Nil
         case _: NothingType | _: AnyKindType =>
@@ -1533,6 +1535,22 @@ object Types {
     override def toString(): String = s"AppliedType($tycon, $args)"
   }
 
+  /** A flexible type `T?`, with `T | Null <: T? <: T`. */
+  final class FlexibleType(val nonNullableType: Type) extends TypeProxy {
+    private val myNullableType: Memo[Type] = uninitializedMemo
+
+    final def nullableType(using Context): Type = memoized(myNullableType) {
+      OrType(nonNullableType, defn.NullType)
+    }
+
+    override def underlying(using Context): Type = nonNullableType
+
+    private[tastyquery] final def derivedFlexibleType(nonNullableType: Type): FlexibleType =
+      if nonNullableType eq this.nonNullableType then this else FlexibleType(nonNullableType)
+
+    override def toString(): String = s"FlexibleType($nonNullableType)"
+  }
+
   /** A by-name parameter type of the form `=> T`. */
   final class ByNameType(val resultType: Type) extends TypeProxy {
     override def underlying(using Context): Type = resultType
diff --git a/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TastyFormat.scala b/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TastyFormat.scala
index 05b8293..185bc48 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TastyFormat.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TastyFormat.scala
@@ -107,10 +107,14 @@ import scala.annotation.switch
   *                  WHILE          Length cond_Term body_Term                        -- while cond do body
   *                  REPEATED       Length elem_Type elem_Term*                       -- Varargs argument of type `elem`
   *                  SELECTouter    Length levels_Nat qual_Term underlying_Type       -- Follow `levels` outer links, starting from `qual`, with given `underlying` type
+  *                  QUOTE          Length body_Term bodyTpe_Type                     -- Quoted expression `'{ body }` of a body typed as `bodyTpe`
+  *                  SPLICE         Length expr_Term tpe_Type                         -- Spliced expression `${ expr }` typed as `tpe`
+  *                  SPLICEPATTERN  Length pat_Term tpe_Type targs_Type* args_Term*   -- Pattern splice `${pat}` or `$pat[targs*](args*)` in a quoted pattern of type `tpe`.
   *    -- patterns:
   *                  BIND           Length boundName_NameRef patType_Type pat_Term    -- name @ pat, wherev `patType` is the type of the bound symbol
   *                  ALTERNATIVE    Length alt_Term*                                  -- alt1 | ... | altn   as a pattern
   *                  UNAPPLY        Length fun_Term ImplicitArg* pat_Type pat_Term*   -- Unapply node `fun(_: pat_Type)(implicitArgs)` flowing into patterns `pat`.
+  *                  QUOTEPATTERN   Length body_Term quotes_Term pat_Type bindings_Term* -- Quote pattern node `'{ bindings*; body }(using quotes)`
   *    -- type trees:
   *                  IDENTtpt              NameRef Type                               -- Used for all type idents
   *                  SELECTtpt             NameRef qual_Term                          -- qual.name
@@ -123,6 +127,8 @@ import scala.annotation.switch
   *                  MATCHtpt       Length bound_Term? sel_Term CaseDef*              -- sel match { CaseDef } where `bound` is optional upper bound of all rhs
   *                  BYNAMEtpt             underlying_Term                            -- => underlying
   *                  SHAREDterm            term_ASTRef                                -- Link to previously serialized term
+  *    -- pickled quote trees:                                                        -- These trees can only appear in pickled quotes. They will never be in a TASTy file.
+  *                  EXPLICITtpt           tpt_Term                                   -- Tag for a type tree that in a context where it is not explicitly known that this tree is a type.
   *                  HOLE           Length idx_Nat tpe_Type arg_Tree*                 -- Splice hole with index `idx`, the type of the hole `tpe`, type and term arguments of the hole `arg`s
   *
   *  CaseDef       = CASEDEF        Length pat_Term rhs_Tree guard_Tree?              -- case pat if guard => rhs
@@ -170,6 +176,7 @@ import scala.annotation.switch
   *                  ORtype         Length left_Type right_Type                       -- lefgt | right
   *                  MATCHtype      Length bound_Type sel_Type case_Type*             -- sel match {cases} with optional upper `bound`
   *                  MATCHCASEtype  Length pat_type rhs_Type                          -- match cases are MATCHCASEtypes or TYPELAMBDAtypes over MATCHCASEtypes
+  *                  FLEXIBLEtype   Length underlying_Type                            -- (underlying)?
   *                  BIND           Length boundName_NameRef bounds_Type Modifier*    -- boundName @ bounds,  for type-variables defined in a type pattern
   *                  BYNAMEtype            underlying_Type                            -- => underlying
   *                  PARAMtype      Length binder_ASTRef paramNum_Nat                 -- A reference to parameter # paramNum in lambda type `binder`
@@ -289,7 +296,7 @@ private[tasties] object TastyFormat:
     * compatibility, but remains backwards compatible, with all
     * preceeding `MinorVersion`.
     */
-  final val MinorVersion: Int = 4
+  final val MinorVersion: Int = 5
 
   /** Natural Number. The `ExperimentalVersion` allows for
     * experimentation with changes to TASTy without committing
@@ -518,6 +525,8 @@ private[tasties] object TastyFormat:
   final val RECtype = 100
   final val SINGLETONtpt = 101
   final val BOUNDED = 102
+  final val EXPLICITtpt = 103
+  final val ELIDED = 104
 
   // Cat. 4:    tag Nat AST
 
@@ -584,14 +593,17 @@ private[tasties] object TastyFormat:
   final val TYPEREFin = 175
   final val SELECTin = 176
   final val EXPORT = 177
-  // final val ??? = 178
-  // final val ??? = 179
+  final val QUOTE = 178
+  final val SPLICE = 179
   final val METHODtype = 180
   final val APPLYsigpoly = 181
+  final val QUOTEPATTERN = 182
+  final val SPLICEPATTERN = 183
 
   final val MATCHtype = 190
   final val MATCHtpt = 191
   final val MATCHCASEtype = 192
+  final val FLEXIBLEtype = 193
 
   final val HOLE = 255
 
@@ -606,7 +618,7 @@ private[tasties] object TastyFormat:
       || firstNatTreeTag <= tag && tag <= RENAMED
       || firstASTTreeTag <= tag && tag <= BOUNDED
       || firstNatASTTreeTag <= tag && tag <= NAMEDARG
-      || firstLengthTreeTag <= tag && tag <= MATCHtpt
+      || firstLengthTreeTag <= tag && tag <= FLEXIBLEtype
       || tag == HOLE
   end isLegalTag
 
@@ -625,7 +637,7 @@ private[tasties] object TastyFormat:
 
   def isTypeTreeTag(tag: Int): Boolean = (tag: @switch) match
     case IDENTtpt | SELECTtpt | SINGLETONtpt | REFINEDtpt | APPLIEDtpt | LAMBDAtpt | TYPEBOUNDStpt | ANNOTATEDtpt |
-        BYNAMEtpt | MATCHtpt | BIND =>
+        BYNAMEtpt | MATCHtpt | EXPLICITtpt | BIND =>
       true
     case _ =>
       false
@@ -768,9 +780,16 @@ private[tasties] object TastyFormat:
     case MATCHCASEtype      => "MATCHCASEtype"
     case MATCHtpt           => "MATCHtpt"
     case PARAMtype          => "PARAMtype"
+    case FLEXIBLEtype       => "FLEXIBLEtype"
     case ANNOTATION         => "ANNOTATION"
     case PRIVATEqualified   => "PRIVATEqualified"
     case PROTECTEDqualified => "PROTECTEDqualified"
+    case EXPLICITtpt        => "EXPLICITtpt"
+    case ELIDED             => "ELIDED"
+    case QUOTE              => "QUOTE"
+    case SPLICE             => "SPLICE"
+    case QUOTEPATTERN       => "QUOTEPATTERN"
+    case SPLICEPATTERN      => "SPLICEPATTERN"
     case HOLE               => "HOLE"
 
     case _ => s"UnknownTag($tag)"
diff --git a/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TastyReader.scala b/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TastyReader.scala
index 06f9f14..4482e11 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TastyReader.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TastyReader.scala
@@ -131,17 +131,21 @@ private[tasties] class TastyReader(val bytes: Array[Byte], start: Int, end: Int,
   def goto(addr: Addr): Unit =
     bp = index(addr)
 
+  /** Is the current read address before the given end address? */
+  def isBefore(end: Addr): Boolean =
+    bp < index(end)
+
   /** Perform `op` until `end` address is reached and collect results in a list. */
   def until[T](end: Addr)(op: => T): List[T] =
     val buf = new mutable.ListBuffer[T]
-    while bp < index(end) do buf += op
+    while isBefore(end) do buf += op
     assert(bp == index(end))
     buf.toList
   end until
 
   /** If before given `end` address, the result of `op`, otherwise `default` */
   def ifBefore[T](end: Addr)(op: => T, default: T): T =
-    if bp < index(end) then op
+    if isBefore(end) then op
     else default
 
   /** Perform `op` while cindition `cond` holds and collect results in a list. */
diff --git a/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TreeUnpickler.scala b/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TreeUnpickler.scala
index d0d4924..89420ea 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TreeUnpickler.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/reader/tasties/TreeUnpickler.scala
@@ -160,7 +160,8 @@ private[tasties] class TreeUnpickler private (
         reader.readNat()
         createSymbols(owner)
       case APPLY | TYPEAPPLY | SUPER | TYPED | ASSIGN | BLOCK | INLINED | LAMBDA | IF | MATCH | TRY | WHILE | REPEATED |
-          ALTERNATIVE | UNAPPLY | APPLIEDtpt | LAMBDAtpt | TYPEBOUNDStpt | ANNOTATEDtpt | MATCHtpt | CASEDEF =>
+          ALTERNATIVE | UNAPPLY | APPLIEDtpt | LAMBDAtpt | TYPEBOUNDStpt | ANNOTATEDtpt | MATCHtpt | CASEDEF | QUOTE |
+          SPLICE | QUOTEPATTERN | SPLICEPATTERN =>
         val end = reader.readEnd()
         reader.until(end)(createSymbols(owner))
       case SELECTin =>
@@ -878,6 +879,19 @@ private[tasties] class TreeUnpickler private (
       val patType = readTrueType()
       val patterns = reader.until(end)(readPattern)
       Unapply(fun, args, patterns)(spn)
+    case QUOTEPATTERN =>
+      val spn = span
+      reader.readByte()
+      val end = reader.readEnd()
+      val body =
+        if reader.nextByte == EXPLICITtpt then
+          reader.readByte()
+          Right(readTypeTree)
+        else Left(readTerm)
+      val quotes = readTerm
+      val patternType = readTrueType()
+      val bindings = reader.until(end)(readTypeTree.asInstanceOf[TypeTreeBind])
+      QuotePattern(bindings, body, quotes, patternType)(spn)
     case SHAREDterm =>
       val spn = span
       reader.readByte()
@@ -1080,6 +1094,32 @@ private[tasties] class TreeUnpickler private (
       val caller = readInlinedCaller(end)
       val bindings = reader.until(end)(readValOrDefDef)
       Inlined(expr, caller, bindings)(spn)
+    case QUOTE =>
+      val spn = span
+      reader.readByte()
+      reader.readEnd() // ignored
+      val body = readTerm
+      val bodyType = readTrueType()
+      Quote(body, bodyType)(spn)
+    case SPLICE =>
+      val spn = span
+      reader.readByte()
+      reader.readEnd() // ignored
+      val expr = readTerm
+      val spliceType = readTrueType()
+      Splice(expr, spliceType)(spn)
+    case SPLICEPATTERN =>
+      val spn = span
+      reader.readByte()
+      val end = reader.readEnd()
+      val pattern = readPattern
+      val spliceType = readTrueType()
+      val targs = reader.collectWhile(reader.isBefore(end) && reader.nextByte == EXPLICITtpt) {
+        reader.readByte()
+        readTypeTree
+      }
+      val args = reader.until(end)(readTerm)
+      SplicePattern(pattern, targs, args, spliceType)(spn)
     case SHAREDterm =>
       val spn = span
       reader.readByte()
@@ -1325,6 +1365,10 @@ private[tasties] class TreeUnpickler private (
       val lambda = readBinderRef[ParamRefBinder]()
       val num = reader.readNat()
       lambda.paramRefs(num)
+    case FLEXIBLEtype =>
+      reader.readByte()
+      reader.readEnd()
+      FlexibleType(readTrueType())
     case REFINEDtype =>
       reader.readByte()
       val end = reader.readEnd()
@@ -1619,7 +1663,10 @@ private[tasties] class TreeUnpickler private (
       reader.readByte()
       ByNameTypeTree(readTypeTree)(spn)
     case BLOCK =>
-      // #284 See QuotesAndSplices.typeQuoteMatching
+      /* #284 See QuotesAndSplices.typeQuoteMatching, but before 3.5.0.
+       * It is unclear whether post-3.5.0 TASTy can still produce this shape,
+       * and therefore whether `TypeBindingsTree` is still relevant at all.
+       */
       val spn = span
       reader.readByte()
       val end = reader.readEnd()


Test patch:
diff --git a/tasty-query/shared/src/test/scala/tastyquery/PositionSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/PositionSuite.scala
index c954077..bf91060 100644
--- a/tasty-query/shared/src/test/scala/tastyquery/PositionSuite.scala
+++ b/tasty-query/shared/src/test/scala/tastyquery/PositionSuite.scala
@@ -186,11 +186,7 @@ class PositionSuite extends RestrictedUnpicklingSuite {
   }
 
   testUnpickle("lambda", "simple_trees.Function") { tree =>
-    assertEquals(
-      collectCode[Lambda](tree),
-      // NoSpan is the expected behavior
-      Nil
-    )
+    assertEquals(collectCode[Lambda](tree), List("(x: Int) => x + 1", "() => ()", "[T] => (x: T) => x", "x => x"))
   }
 
   testUnpickle("return", "simple_trees.Return") { tree =>
diff --git a/tasty-query/shared/src/test/scala/tastyquery/ReadTreeSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/ReadTreeSuite.scala
index bbe4c29..7902736 100644
--- a/tasty-query/shared/src/test/scala/tastyquery/ReadTreeSuite.scala
+++ b/tasty-query/shared/src/test/scala/tastyquery/ReadTreeSuite.scala
@@ -2308,25 +2308,74 @@ class ReadTreeSuite extends RestrictedUnpicklingSuite {
   }
 
   testUnpickle("quotes-and-splices", "simple_trees.QuotesAndSplices$") { tree =>
-    val typeQuoteMatchingDef = findTree(tree) { case dd @ DefDef(SimpleName("typeQuoteMatching"), _, _, _, _) =>
+    val termQuoteBody = findTree(tree) { case dd @ DefDef(SimpleName("termQuote"), _, _, Some(rhs), _) =>
+      rhs
+    }
+    val termQuoteCheck: StructureCheck = {
+      case Quote(Literal(Constant("hello")), TypeRefInternal(_, SimpleTypeName("String"))) =>
+    }
+    assert(containsSubtree(termQuoteCheck)(clue(termQuoteBody)))
+
+    val termSpliceBody = findTree(tree) { case dd @ DefDef(SimpleName("termSplice"), _, _, Some(rhs), _) =>
+      rhs
+    }
+    val termSpliceCheck: StructureCheck = {
+      case Splice(Block(List(_: DefDef), _: Lambda), TypeRefInternal(_, SimpleTypeName("String"))) =>
+    }
+    assert(containsSubtree(termSpliceCheck)(clue(termSpliceBody)))
+
+    val termQuotePatternDef = findTree(tree) { case dd @ DefDef(SimpleName("termQuotePattern"), _, _, _, _) =>
       dd
     }
-    val typeQuoteMatchingCaseDef = findTree(typeQuoteMatchingDef) { case cd: CaseDef =>
+    val termQuotePatternCaseDef = findTree(termQuotePatternDef) { case cd: CaseDef =>
       cd
     }
-    val typeQuoteMatchingCheck: StructureCheck = {
-      case TypeBindingsTree(
-            List(
-              TypeMember(SimpleTypeName("t"), InferredTypeBoundsTree(_), tSym),
-              TypeMember(SimpleTypeName("u"), InferredTypeBoundsTree(_), uSym)
+    val termQuotePatternCheck: StructureCheck = {
+      case QuotePattern(
+            Nil,
+            Left(
+              Apply(
+                Select(
+                  Typed(
+                    SplicePattern(
+                      Bind(SimpleName("a"), WildcardPattern(_), aSym),
+                      Nil,
+                      Nil,
+                      TypeRefInternal(ScalaPackageRef(), SimpleTypeName("Int"))
+                    ),
+                    _
+                  ),
+                  _
+                ),
+                _
+              )
             ),
-            AppliedTypeTree(
-              TypeIdent(SimpleTypeName("Map")),
-              List(TypeWrapper(TypeRefInternal(NoPrefix, tSymRef)), TypeWrapper(TypeRefInternal(NoPrefix, uSymRef)))
-            )
+            Ident(SimpleName("quotes")),
+            _
+          ) =>
+    }
+    assert(containsSubtree(termQuotePatternCheck)(clue(termQuotePatternCaseDef)))
+
+    val typeQuotePatternDef = findTree(tree) { case dd @ DefDef(SimpleName("typeQuotePattern"), _, _, _, _) =>
+      dd
+    }
+    val typeQuotePatternCaseDef = findTree(typeQuotePatternDef) { case cd: CaseDef =>
+      cd
+    }
+    val typeQuotePatternCheck: StructureCheck = {
+      case QuotePattern(
+            List(TypeTreeBind(SimpleTypeName("t"), _, tSym), TypeTreeBind(SimpleTypeName("u"), _, uSym)),
+            Right(
+              AppliedTypeTree(
+                TypeIdent(SimpleTypeName("Map")),
+                List(TypeWrapper(TypeRefInternal(NoPrefix, tSymRef)), TypeWrapper(TypeRefInternal(NoPrefix, uSymRef)))
+              )
+            ),
+            Ident(SimpleName("quotes")),
+            _
           ) if tSymRef == tSym && uSymRef == uSym && tSym != uSym =>
     }
-    assert(containsSubtree(typeQuoteMatchingCheck)(clue(typeQuoteMatchingCaseDef)))
+    assert(containsSubtree(typeQuotePatternCheck)(clue(typeQuotePatternCaseDef)))
   }
 
   testUnpickle("anon-classes-in-constructor", "simple_trees.AnonClassesInCtor") { tree =>
diff --git a/tasty-query/shared/src/test/scala/tastyquery/WholeClasspathSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/WholeClasspathSuite.scala
index b53cc0e..95a8f61 100644
--- a/tasty-query/shared/src/test/scala/tastyquery/WholeClasspathSuite.scala
+++ b/tasty-query/shared/src/test/scala/tastyquery/WholeClasspathSuite.scala
@@ -80,8 +80,8 @@ class WholeClasspathSuite extends UnrestrictedUnpicklingSuite:
     traverseEveryTopLevelClassDef(_ != RuntimeLazyValsClass)(traverser)
 
     tracker.finalize(
-      // As of this writing, there were 15293 successes
-      minExpectedSuccessCount = 15000,
+      // As of this writing, there were 11617 successes
+      minExpectedSuccessCount = 11000,
       errorTitle = "Could not resolve the `symbol` of some trees on the classpath:"
     )
   }
@@ -116,8 +116,8 @@ class WholeClasspathSuite extends UnrestrictedUnpicklingSuite:
     traverseEveryTopLevelClassDef(_ != RuntimeLazyValsClass)(traverser)
 
     tracker.finalize(
-      // As of this writing, there were 44959 successes
-      minExpectedSuccessCount = 44000,
+      // As of this writing, there were 38337 successes
+      minExpectedSuccessCount = 38000,
       errorTitle = "Could not compute the `tpe` of some term trees on the classpath:"
     )
   }
@@ -144,7 +144,10 @@ object WholeClasspathSuite:
       val errors = errorsBuilder.result()
       if errors.nonEmpty then fail(errors.mkString(errorTitle + "\n", "\n", "\n"))
 
-      assert(clue(successCount) >= clue(minExpectedSuccessCount))
+      assert(
+        clue(successCount) >= clue(minExpectedSuccessCount),
+        "There were no failures, but there were not as many successes as expected."
+      )
     end finalize
   end Tracker
 end WholeClasspathSuite
diff --git a/test-sources/src/main/scala/simple_trees/AccessModifiers.scala b/test-sources/src/main/scala/simple_trees/AccessModifiers.scala
index 5ef7824..e8e908b 100644
--- a/test-sources/src/main/scala/simple_trees/AccessModifiers.scala
+++ b/test-sources/src/main/scala/simple_trees/AccessModifiers.scala
@@ -2,7 +2,7 @@ package simple_trees
 
 import scala.annotation.nowarn
 
-@nowarn("msg=The \\[this\\] qualifier will be deprecated")
+@nowarn("msg=Ignoring \\[this\\] qualifier")
 class AccessModifiers(
   localParam: Int,
   private[this] val privateThisParam: Int,
diff --git a/test-sources/src/main/scala/simple_trees/QuotesAndSplices.scala b/test-sources/src/main/scala/simple_trees/QuotesAndSplices.scala
index c46ec97..74508f7 100644
--- a/test-sources/src/main/scala/simple_trees/QuotesAndSplices.scala
+++ b/test-sources/src/main/scala/simple_trees/QuotesAndSplices.scala
@@ -3,7 +3,23 @@ package simple_trees
 import scala.quoted.*
 
 object QuotesAndSplices:
-  def typeQuoteMatching(using quotes: Quotes): Expr[Unit] =
+  def termQuote(using Quotes): Expr[String] =
+    '{ "hello" }
+
+  def termSplice(using Quotes)(s: Expr[String]): Expr[String] =
+    '{ $s + "foo" }
+
+  /* Reading only -- mostly making sure that there isn't a separate TYPESPLICE
+   * tag that we don't know about.
+   */
+  def typeSplice[T: Type](using Quotes)(e: Expr[T]): Expr[T] =
+    '{ $e: T }
+
+  def termQuotePattern(using quotes: Quotes)(e: Expr[Int]): Expr[Int] =
+    e match
+      case '{ ($a: Int) + 1 } => a
+
+  def typeQuotePattern(using quotes: Quotes): Expr[Unit] =
     Type.of[Any] match
       case '[Map[t, u]] => '{ () }
 end QuotesAndSplices