From 96b1b83ff166be70cd8c5b1e2774580e52af4a3a Mon Sep 17 00:00:00 2001 From: Matt Bovel Date: Thu, 13 Nov 2025 19:56:23 +0000 Subject: [PATCH 1/4] Strip synthetic ascriptions from inlined calls --- compiler/src/dotty/tools/dotc/ast/TreeInfo.scala | 10 ++++++++++ compiler/src/dotty/tools/dotc/inlines/Inliner.scala | 2 +- tests/run/i24420-inline-local-ref.check | 1 + tests/run/i24420-inline-local-ref.scala | 12 ++++++++++++ tests/run/i24420-transparent-inline-local-ref.check | 1 + tests/run/i24420-transparent-inline-local-ref.scala | 12 ++++++++++++ 6 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/run/i24420-inline-local-ref.check create mode 100644 tests/run/i24420-inline-local-ref.scala create mode 100644 tests/run/i24420-transparent-inline-local-ref.check create mode 100644 tests/run/i24420-transparent-inline-local-ref.scala diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 8c6303d07b9a..bb25c8dd1a6c 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -831,6 +831,16 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => } } + /** Strip `Typed` nodes, recursing into `Block` nodes. */ + def stripSyntheticTyped(tree: Tree)(using Context): Tree = + unsplice(tree) match + case Typed(expr, _) if tree.span.isSynthetic => + stripSyntheticTyped(expr) + case Block(stats, expr) => + cpy.Block(tree)(stats, stripSyntheticTyped(expr)) + case _ => + tree + /** The type and term arguments of a possibly curried call, in the order they are given */ def allArgss(tree: Tree): List[List[Tree]] = @tailrec diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index 356e5ad40fdd..a9c74dc6af64 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -790,7 +790,7 @@ class Inliner(val call: tpd.Tree)(using Context): } // Run a typing pass over the inlined tree. See InlineTyper for details. - val expansion1 = inlineTyper.typed(expansion)(using inlineCtx) + val expansion1 = inlineTyper.typed(stripSyntheticTyped(expansion))(using inlineCtx) if (ctx.settings.verbose.value) { inlining.println(i"to inline = $rhsToInline") diff --git a/tests/run/i24420-inline-local-ref.check b/tests/run/i24420-inline-local-ref.check new file mode 100644 index 000000000000..d00491fd7e5b --- /dev/null +++ b/tests/run/i24420-inline-local-ref.check @@ -0,0 +1 @@ +1 diff --git a/tests/run/i24420-inline-local-ref.scala b/tests/run/i24420-inline-local-ref.scala new file mode 100644 index 000000000000..31d491ad74a9 --- /dev/null +++ b/tests/run/i24420-inline-local-ref.scala @@ -0,0 +1,12 @@ +inline def f(): Long = + 1L + +inline def g(): Long = + inline val x = f() + x + +inline def h(): Long = + inline if g() > 0L then 1L else 0L + +@main def Test: Unit = + println(h()) diff --git a/tests/run/i24420-transparent-inline-local-ref.check b/tests/run/i24420-transparent-inline-local-ref.check new file mode 100644 index 000000000000..d00491fd7e5b --- /dev/null +++ b/tests/run/i24420-transparent-inline-local-ref.check @@ -0,0 +1 @@ +1 diff --git a/tests/run/i24420-transparent-inline-local-ref.scala b/tests/run/i24420-transparent-inline-local-ref.scala new file mode 100644 index 000000000000..bf7c2ffddf81 --- /dev/null +++ b/tests/run/i24420-transparent-inline-local-ref.scala @@ -0,0 +1,12 @@ +transparent inline def f(): Long = + 1L + +transparent inline def g(): Long = + inline val x = f() + x + +transparent inline def h(): Long = + inline if g() > 0L then 1L else 0L + +@main def Test: Unit = + println(h()) From 5d54e68b81306215d82e88571491c48eca73f55d Mon Sep 17 00:00:00 2001 From: Matt Bovel Date: Fri, 14 Nov 2025 15:27:32 +0000 Subject: [PATCH 2/4] Only strip synthetic ascriptions inside inlined defs --- compiler/src/dotty/tools/dotc/ast/TreeInfo.scala | 2 ++ compiler/src/dotty/tools/dotc/inlines/Inliner.scala | 5 +++-- tests/run/i24420-inline-local-ref.check | 1 - tests/run/i24420-inline-local-ref.scala | 2 +- tests/run/i24420-transparent-inline-local-ref.check | 1 - tests/run/i24420-transparent-inline-local-ref.scala | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 tests/run/i24420-inline-local-ref.check delete mode 100644 tests/run/i24420-transparent-inline-local-ref.check diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index bb25c8dd1a6c..af6ff6450f31 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -836,6 +836,8 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => unsplice(tree) match case Typed(expr, _) if tree.span.isSynthetic => stripSyntheticTyped(expr) + case tree @ Inlined(call, bindings, expr) => + cpy.Inlined(tree)(call, bindings, stripSyntheticTyped(expr)) case Block(stats, expr) => cpy.Block(tree)(stats, stripSyntheticTyped(expr)) case _ => diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index a9c74dc6af64..71b8dba6d2f0 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -790,7 +790,7 @@ class Inliner(val call: tpd.Tree)(using Context): } // Run a typing pass over the inlined tree. See InlineTyper for details. - val expansion1 = inlineTyper.typed(stripSyntheticTyped(expansion))(using inlineCtx) + val expansion1 = inlineTyper.typed(expansion)(using inlineCtx) if (ctx.settings.verbose.value) { inlining.println(i"to inline = $rhsToInline") @@ -1056,7 +1056,8 @@ class Inliner(val call: tpd.Tree)(using Context): val meth = tree.symbol if meth.isAllOf(DeferredInline) then errorTree(tree, em"Deferred inline ${meth.showLocated} cannot be invoked") - else if Inlines.needsInlining(tree) then Inlines.inlineCall(simplify(tree, pt, locked)) + else if Inlines.needsInlining(tree) then + stripSyntheticTyped(Inlines.inlineCall(simplify(tree, pt, locked))) else tree override def typedUnadapted(tree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree = diff --git a/tests/run/i24420-inline-local-ref.check b/tests/run/i24420-inline-local-ref.check deleted file mode 100644 index d00491fd7e5b..000000000000 --- a/tests/run/i24420-inline-local-ref.check +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/tests/run/i24420-inline-local-ref.scala b/tests/run/i24420-inline-local-ref.scala index 31d491ad74a9..84828de9f443 100644 --- a/tests/run/i24420-inline-local-ref.scala +++ b/tests/run/i24420-inline-local-ref.scala @@ -9,4 +9,4 @@ inline def h(): Long = inline if g() > 0L then 1L else 0L @main def Test: Unit = - println(h()) + assert(h() == 1L) diff --git a/tests/run/i24420-transparent-inline-local-ref.check b/tests/run/i24420-transparent-inline-local-ref.check deleted file mode 100644 index d00491fd7e5b..000000000000 --- a/tests/run/i24420-transparent-inline-local-ref.check +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/tests/run/i24420-transparent-inline-local-ref.scala b/tests/run/i24420-transparent-inline-local-ref.scala index bf7c2ffddf81..7d8180680130 100644 --- a/tests/run/i24420-transparent-inline-local-ref.scala +++ b/tests/run/i24420-transparent-inline-local-ref.scala @@ -9,4 +9,4 @@ transparent inline def h(): Long = inline if g() > 0L then 1L else 0L @main def Test: Unit = - println(h()) + assert(h() == 1L) From 0036b59c3add9a523640f43c59fb4a6ef6578e7f Mon Sep 17 00:00:00 2001 From: Matt Bovel Date: Fri, 14 Nov 2025 16:53:58 +0000 Subject: [PATCH 3/4] Only remove ascriptions that are safe to remove --- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 12 --------- .../dotty/tools/dotc/inlines/Inliner.scala | 24 +++++++++++++++++- tests/pos/i24412.scala | 13 ++++++++++ tests/run/i24420-inline-val.scala | 25 +++++++++++++++++++ 4 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 tests/pos/i24412.scala create mode 100644 tests/run/i24420-inline-val.scala diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index af6ff6450f31..8c6303d07b9a 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -831,18 +831,6 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => } } - /** Strip `Typed` nodes, recursing into `Block` nodes. */ - def stripSyntheticTyped(tree: Tree)(using Context): Tree = - unsplice(tree) match - case Typed(expr, _) if tree.span.isSynthetic => - stripSyntheticTyped(expr) - case tree @ Inlined(call, bindings, expr) => - cpy.Inlined(tree)(call, bindings, stripSyntheticTyped(expr)) - case Block(stats, expr) => - cpy.Block(tree)(stats, stripSyntheticTyped(expr)) - case _ => - tree - /** The type and term arguments of a possibly curried call, in the order they are given */ def allArgss(tree: Tree): List[List[Tree]] = @tailrec diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index 71b8dba6d2f0..1cb4ff8d9436 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -1057,9 +1057,31 @@ class Inliner(val call: tpd.Tree)(using Context): if meth.isAllOf(DeferredInline) then errorTree(tree, em"Deferred inline ${meth.showLocated} cannot be invoked") else if Inlines.needsInlining(tree) then - stripSyntheticTyped(Inlines.inlineCall(simplify(tree, pt, locked))) + safeStripTyped(Inlines.inlineCall(simplify(tree, pt, locked))) else tree + /** Strips ascriptions, recursing into `Inlined` and `Block` nodes. + * + * Only strips `Typed` nodes that are safe to remove, i.e. where the + * expression type is a subtype of the expected type, and not `Nothing` or + * `Null`. + */ + private def safeStripTyped(tree: Tree)(using Context): Tree = + def canRemoveTyped(expr: Tree, pt: Type): Boolean = + (expr.tpe frozen_<:< pt) + && !expr.tpe.isNothingType + && !expr.tpe.isNullType + + unsplice(tree) match + case Typed(expr, _) if canRemoveTyped(expr, tree.tpe) => + safeStripTyped(expr) + case tree @ Inlined(call, bindings, expr) => + cpy.Inlined(tree)(call, bindings, safeStripTyped(expr)) + case Block(stats, expr) => + cpy.Block(tree)(stats, safeStripTyped(expr)) + case _ => + tree + override def typedUnadapted(tree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree = super.typedUnadapted(tree, pt, locked) match case member: MemberDef => member.setDefTree diff --git a/tests/pos/i24412.scala b/tests/pos/i24412.scala new file mode 100644 index 000000000000..3b9ec8d5a579 --- /dev/null +++ b/tests/pos/i24412.scala @@ -0,0 +1,13 @@ +object test { + import scala.compiletime.erasedValue + + inline def contains[T <: Tuple, E]: Boolean = inline erasedValue[T] match { + case _: EmptyTuple => false + case _: (_ *: tail) => contains[tail, E] + } + inline def check[T <: Tuple]: Unit = { + inline if contains[T, Long] && false then ??? + } + + check[(String, Double)] +} diff --git a/tests/run/i24420-inline-val.scala b/tests/run/i24420-inline-val.scala new file mode 100644 index 000000000000..ec201d948a96 --- /dev/null +++ b/tests/run/i24420-inline-val.scala @@ -0,0 +1,25 @@ +inline def f1(): Long = + 1L + +inline def f2(): Long = + inline val x = f1() + 1L + x + +inline def f3(): Long = + inline val x = f1() + x + +inline def g1(): Boolean = + true + +inline def g2(): Long = + inline if g1() then 1L else 2L + +inline def g3(): Long = + inline if f1() > 0L then 1L else 2L + +@main def Test: Unit = + assert(f2() == 2L) + assert(f3() == 1L) + assert(g2() == 1L) + assert(g3() == 1L) From 1099559ba5c38df0683ed4ffea2c7352a82ba75d Mon Sep 17 00:00:00 2001 From: Matt Bovel Date: Fri, 14 Nov 2025 16:59:27 +0000 Subject: [PATCH 4/4] Check inline val rhs already during inlining --- .../dotty/tools/dotc/inlines/Inliner.scala | 4 +++- .../tools/dotc/transform/InlineVals.scala | 16 +++++++------- .../neg/i24412-non-constant-inline-val.check | 22 +++++++++++++++++++ .../neg/i24412-non-constant-inline-val.scala | 9 ++++++++ 4 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 tests/neg/i24412-non-constant-inline-val.check create mode 100644 tests/neg/i24412-non-constant-inline-val.scala diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index 1cb4ff8d9436..285d48f619ee 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -932,7 +932,9 @@ class Inliner(val call: tpd.Tree)(using Context): sym.info = rhs.tpe untpd.cpy.ValDef(vdef)(vdef.name, untpd.TypeTree(rhs.tpe), untpd.TypedSplice(rhs)) else vdef - super.typedValDef(vdef1, sym) + val typedVdef = super.typedValDef(vdef1, sym) + transform.InlineVals.checkInlineConformant(typedVdef.asInstanceOf[ValDef]) + typedVdef override def typedApply(tree: untpd.Apply, pt: Type)(using Context): Tree = val locked = ctx.typerState.ownedVars diff --git a/compiler/src/dotty/tools/dotc/transform/InlineVals.scala b/compiler/src/dotty/tools/dotc/transform/InlineVals.scala index c300352a162e..44fe3687438c 100644 --- a/compiler/src/dotty/tools/dotc/transform/InlineVals.scala +++ b/compiler/src/dotty/tools/dotc/transform/InlineVals.scala @@ -6,12 +6,12 @@ import core.* import Contexts.*, Decorators.*, Symbols.*, Flags.*, Types.* import MegaPhase.MiniPhase import inlines.Inlines -import ast.tpd +import ast.tpd.* /** Check that `tree.rhs` can be right hand-side of an `inline` value definition. */ class InlineVals extends MiniPhase: - import ast.tpd.* + import InlineVals.checkInlineConformant override def phaseName: String = InlineVals.name @@ -27,8 +27,12 @@ class InlineVals extends MiniPhase: checkInlineConformant(tree) tree +object InlineVals: + val name: String = "inlineVals" + val description: String = "check right hand-sides of an `inline val`s" + /** Check that `tree.rhs` can be right hand-side of an `inline` value definition. */ - private def checkInlineConformant(tree: ValDef)(using Context): Unit = { + def checkInlineConformant(tree: ValDef)(using Context): Unit = { if tree.symbol.is(Inline, butNot = DeferredOrTermParamOrAccessor) && !Inlines.inInlineMethod then @@ -38,7 +42,7 @@ class InlineVals extends MiniPhase: case tp: ConstantType => if !isPureExpr(rhs) then def details = - if enclosingInlineds.nonEmpty || rhs.isInstanceOf[tpd.Inlined] + if enclosingInlineds.nonEmpty || rhs.isInstanceOf[Inlined] then i" but was: $rhs" else "" report.error(em"inline value must be pure$details", rhs.srcPos) @@ -55,7 +59,3 @@ class InlineVals extends MiniPhase: else report.error(em"inline value must contain a literal constant value.\n\nTo inline more complex types consider using `inline def`", rhs) } - -object InlineVals: - val name: String = "inlineVals" - val description: String = "check right hand-sides of an `inline val`s" diff --git a/tests/neg/i24412-non-constant-inline-val.check b/tests/neg/i24412-non-constant-inline-val.check new file mode 100644 index 000000000000..a410c879d63a --- /dev/null +++ b/tests/neg/i24412-non-constant-inline-val.check @@ -0,0 +1,22 @@ +-- Error: tests/neg/i24412-non-constant-inline-val.scala:9:3 ----------------------------------------------------------- +9 | g() // error // error + | ^^^ + | inline value must have a literal constant type + |--------------------------------------------------------------------------------------------------------------------- + |Inline stack trace + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + |This location contains code that was inlined from i24412-non-constant-inline-val.scala:5 +5 | inline val x = f() + | ^^^ + --------------------------------------------------------------------------------------------------------------------- +-- Error: tests/neg/i24412-non-constant-inline-val.scala:9:3 ----------------------------------------------------------- +9 | g() // error // error + | ^^^ + | Cannot reduce `inline if` because its condition is not a constant value: x > 0 + |--------------------------------------------------------------------------------------------------------------------- + |Inline stack trace + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + |This location contains code that was inlined from i24412-non-constant-inline-val.scala:6 +6 | inline if x > 0 then 1 else 0 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --------------------------------------------------------------------------------------------------------------------- diff --git a/tests/neg/i24412-non-constant-inline-val.scala b/tests/neg/i24412-non-constant-inline-val.scala new file mode 100644 index 000000000000..330637001da7 --- /dev/null +++ b/tests/neg/i24412-non-constant-inline-val.scala @@ -0,0 +1,9 @@ +inline def f(): Int = + Math.random().toInt + +inline def g(): Int = + inline val x = f() + inline if x > 0 then 1 else 0 + +@main def Test: Unit = + g() // error // error