Skip to content

Use unitialized for wildcard initializers #11231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions bench/tests/Vector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import scala.annotation.unchecked.uncheckedVariance
import scala.compat.Platform
import scala.collection.generic._
import scala.collection.mutable.Builder
import compiletime.uninitialized

/** Companion object to the Vector class
*/
Expand Down Expand Up @@ -741,13 +742,13 @@ final class VectorBuilder[A]() extends Builder[A,Vector[A]] with VectorPointer[A


private[immutable] trait VectorPointer[T] {
private[immutable] var depth: Int = _
private[immutable] var display0: Array[AnyRef] = _
private[immutable] var display1: Array[AnyRef] = _
private[immutable] var display2: Array[AnyRef] = _
private[immutable] var display3: Array[AnyRef] = _
private[immutable] var display4: Array[AnyRef] = _
private[immutable] var display5: Array[AnyRef] = _
private[immutable] var depth: Int = uninitialized
private[immutable] var display0: Array[AnyRef] = uninitialized
private[immutable] var display1: Array[AnyRef] = uninitialized
private[immutable] var display2: Array[AnyRef] = uninitialized
private[immutable] var display3: Array[AnyRef] = uninitialized
private[immutable] var display4: Array[AnyRef] = uninitialized
private[immutable] var display5: Array[AnyRef] = uninitialized

// used
private[immutable] final def initFrom[U](that: VectorPointer[U]): Unit = initFrom(that, that.depth)
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,9 @@ class Definitions {
@tu lazy val CompiletimePackageObject: Symbol = requiredModule("scala.compiletime.package")
@tu lazy val Compiletime_codeOf: Symbol = CompiletimePackageObject.requiredMethod("codeOf")
@tu lazy val Compiletime_erasedValue : Symbol = CompiletimePackageObject.requiredMethod("erasedValue")
@tu lazy val Compiletime_uninitialized: Symbol = CompiletimePackageObject.requiredMethod("uninitialized")
@tu lazy val Compiletime_error : Symbol = CompiletimePackageObject.requiredMethod(nme.error)
@tu lazy val Compiletime_requireConst: Symbol = CompiletimePackageObject.requiredMethod("requireConst")
@tu lazy val Compiletime_requireConst : Symbol = CompiletimePackageObject.requiredMethod("requireConst")
@tu lazy val Compiletime_constValue : Symbol = CompiletimePackageObject.requiredMethod("constValue")
@tu lazy val Compiletime_constValueOpt: Symbol = CompiletimePackageObject.requiredMethod("constValueOpt")
@tu lazy val Compiletime_summonFrom : Symbol = CompiletimePackageObject.requiredMethod("summonFrom")
Expand Down
8 changes: 7 additions & 1 deletion compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3201,7 +3201,8 @@ object Parsers {

/** PatDef ::= ids [‘:’ Type] ‘=’ Expr
* | Pattern2 [‘:’ Type] ‘=’ Expr
* VarDef ::= PatDef | id {`,' id} `:' Type `=' `_'
* VarDef ::= PatDef
* | id {`,' id} `:' Type `=' `_' (deprecated in 3.1)
* ValDcl ::= id {`,' id} `:' Type
* VarDcl ::= id {`,' id} `:' Type
*/
Expand All @@ -3224,9 +3225,14 @@ object Parsers {
val rhs =
if tpt.isEmpty || in.token == EQUALS then
accept(EQUALS)
val rhsOffset = in.offset
subExpr() match
case rhs0 @ Ident(name) if placeholderParams.nonEmpty && name == placeholderParams.head.name
&& !tpt.isEmpty && mods.is(Mutable) && lhs.forall(_.isInstanceOf[Ident]) =>
if sourceVersion.isAtLeast(`3.1`) then
deprecationWarning(
em"""`= _` has been deprecated; use `= uninitialized` instead.
|`uninitialized` needs to be imported from scala.compiletime.""", rhsOffset)
placeholderParams = placeholderParams.tail
atSpan(rhs0.span) { Ident(nme.WILDCARD) }
case rhs0 => rhs0
Expand Down
24 changes: 22 additions & 2 deletions compiler/src/dotty/tools/dotc/transform/PruneErasedDefs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Symbols._
import Types._
import typer.RefChecks
import MegaPhase.MiniPhase
import StdNames.nme
import ast.tpd

/** This phase makes all erased term members of classes private so that they cannot
Expand All @@ -18,6 +19,10 @@ import ast.tpd
* The phase also replaces all expressions that appear in an erased context by
* default values. This is necessary so that subsequent checking phases such
* as IsInstanceOfChecker don't give false negatives.
* Finally, the phase replaces `compiletime.uninitialized` on the right hand side
* of a mutable field definition by `_`. This avoids a "is declared erased, but is
* in fact used" error in Erasure and communicates to Constructors that the
* variable does not have an initializer.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uninitialized is not erased anymore and the transformation has nothing to do with pruning erased definitions. It is also unrelated to erased in general as this operation does produce a value at runtime (even though it is elided). This should be done somewhere else. FirstTransform might be a good candidate as we are transforming var x: T = uninitialized to the canonical form that erasure understands.

*/
class PruneErasedDefs extends MiniPhase with SymTransformer { thisTransform =>
import tpd._
Expand All @@ -38,10 +43,25 @@ class PruneErasedDefs extends MiniPhase with SymTransformer { thisTransform =>
cpy.Apply(tree)(tree.fun, tree.args.map(trivialErasedTree))
else tree

private def hasUninitializedRHS(tree: ValOrDefDef)(using Context): Boolean =
def recur(rhs: Tree): Boolean = rhs match
case rhs: RefTree =>
rhs.symbol == defn.Compiletime_uninitialized
&& tree.symbol.is(Mutable) && tree.symbol.owner.isClass
case closureDef(ddef) if defn.isContextFunctionType(tree.tpt.tpe.dealias) =>
recur(ddef.rhs)
case _ =>
false
recur(tree.rhs)

override def transformValDef(tree: ValDef)(using Context): Tree =
if (tree.symbol.isEffectivelyErased && !tree.rhs.isEmpty)
val sym = tree.symbol
if tree.symbol.isEffectivelyErased && !tree.rhs.isEmpty then
cpy.ValDef(tree)(rhs = trivialErasedTree(tree))
else tree
else if hasUninitializedRHS(tree) then
cpy.ValDef(tree)(rhs = cpy.Ident(tree.rhs)(nme.WILDCARD).withType(tree.tpt.tpe))
else
tree

override def transformDefDef(tree: DefDef)(using Context): Tree =
if (tree.symbol.isEffectivelyErased && !tree.rhs.isEmpty)
Expand Down
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/dotc/CompilationTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class CompilationTests {
aggregateTests(
compileFilesInDir("tests/neg", defaultOptions),
compileFilesInDir("tests/neg-tailcall", defaultOptions),
compileFilesInDir("tests/neg-strict", defaultOptions.and("-source", "3.1", "-Xfatal-warnings")),
compileFilesInDir("tests/neg-strict", defaultOptions.and("-source", "3.1", "-deprecation", "-Xfatal-warnings")),
compileFilesInDir("tests/neg-no-kind-polymorphism", defaultOptions and "-Yno-kind-polymorphism"),
compileFilesInDir("tests/neg-custom-args/deprecation", defaultOptions.and("-Xfatal-warnings", "-deprecation")),
compileFilesInDir("tests/neg-custom-args/fatal-warnings", defaultOptions.and("-Xfatal-warnings")),
Expand Down
6 changes: 2 additions & 4 deletions docs/docs/internals/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,14 +384,12 @@ TypeDcl ::= id [TypeParamClause] {FunParamClause} TypeBounds
[‘=’ Type]

Def ::= ‘val’ PatDef
| ‘var’ VarDef
| ‘var’ PatDef
| ‘def’ DefDef
| ‘type’ {nl} TypeDcl
| TmplDef
PatDef ::= ids [‘:’ Type] ‘=’ Expr
| Pattern2 [‘:’ Type] ‘=’ Expr PatDef(_, pats, tpe?, expr)
VarDef ::= PatDef
| ids ‘:’ Type ‘=’ ‘_’
| Pattern2 [‘:’ Type] ‘=’ Expr PatDef(_, pats, tpe?, expr)
DefDef ::= DefSig [‘:’ Type] ‘=’ Expr DefDef(_, name, tparams, vparamss, tpe, expr)
| ‘this’ DefParamClause DefParamClauses ‘=’ ConstrExpr DefDef(_, <init>, Nil, vparamss, EmptyTree, expr | Block)

Expand Down
18 changes: 18 additions & 0 deletions docs/docs/reference/dropped-features/wildcard-init.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
layout: doc-page
title: "Dropped: wildcard initializer"
---

The syntax
```scala
var x: A = _
```
that was used to indicate an uninitialized field, has been dropped.
At its place there is a special value `uninitialized` in the `scala.compiletime` package. To get an uninitialized field, you now write
```scala
import scala.compiletime.uninitialized

var x: A = uninitialized
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, we also support

  var a = uninitialized[A]

This seems to be leaking implementation details.

Copy link
Contributor Author

@odersky odersky Feb 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. We it's better to define it like this:

@compileTimeOnly("`uninitialized` can only be used as the right hand side of a mutable field definition")
def uninitialized: Nothing = ???

```
To enable cross-compilation, `_` is still supported, but it will be dropped in a future 3.x version.

4 changes: 1 addition & 3 deletions docs/docs/reference/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,14 +374,12 @@ DefSig ::= id [DefTypeParamClause] DefParamClauses
TypeDcl ::= id [TypeParamClause] {FunParamClause} TypeBounds [‘=’ Type]

Def ::= ‘val’ PatDef
| ‘var’ VarDef
| ‘var’ PatDef
| ‘def’ DefDef
| ‘type’ {nl} TypeDcl
| TmplDef
PatDef ::= ids [‘:’ Type] ‘=’ Expr
| Pattern2 [‘:’ Type] ‘=’ Expr
VarDef ::= PatDef
| ids ‘:’ Type ‘=’ ‘_’
DefDef ::= DefSig [‘:’ Type] ‘=’ Expr
| ‘this’ DefParamClause DefParamClauses ‘=’ ConstrExpr

Expand Down
2 changes: 2 additions & 0 deletions docs/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ sidebar:
url: docs/reference/dropped-features/nonlocal-returns.html
- title: "[this] Qualifier"
url: docs/reference/dropped-features/this-qualifier.html
- title: Wildcard initializers
url: docs/reference/dropped-features/wildcard-init.html
- title: Syntax Summary
url: docs/reference/syntax.html
- title: Contributing
Expand Down
14 changes: 12 additions & 2 deletions library/src/scala/compiletime/package.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package scala

import scala.quoted._
import annotation.compileTimeOnly

package object compiletime {

Expand All @@ -18,6 +17,17 @@ package object compiletime {
*/
erased def erasedValue[T]: T = ???

/** Used as the initializer of a mutable class or object field, like this:
*
* var x: T = uninitialized
*
* This signifies that the field is not initialized on its own. It is still initialized
* as part of the bulk initialization of the object it belongs to, which assigns zero
* values such as `null`, `0`, `0.0`, `false` to all object fields.
*/
@compileTimeOnly("`uninitialized` can only be used as the right hand side of a mutable field definition")
def uninitialized: Nothing = ???

/** The error method is used to produce user-defined compile errors during inline expansion.
* If an inline expansion results in a call error(msgStr) the compiler produces an error message containing the given msgStr.
*
Expand Down
2 changes: 1 addition & 1 deletion tests/fuzzy/471d33abf565d5dd3691679237f148638f4ff115.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def i10(i2: i4): i3 = new i4(i5)
object i10 {
def main(i12: Array[String]): Unit = {
val i10: Array[String] = null
var i2 = _
var i2 = compiletime.uninitialized
def i3(i2: Int) = i2
}
object i0 {
Expand Down
23 changes: 12 additions & 11 deletions tests/init/crash/i2468.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

import compiletime.uninitialized
object Test {

class A {
private[this] var x: String = _
private[this] var x: String = uninitialized
}

class B {
Expand All @@ -11,16 +12,16 @@ object Test {
}

class C {
private[this] var x1: Int = _
private[this] var x2: Unit = _
private[this] var x3: Char = _
private[this] var x4: Boolean = _
private[this] var x5: Float = _
private[this] var x6: Double = _
private[this] var x7: Char = _
private[this] var x8: Byte = _
private[this] var x9: AnyVal = _
private[this] var x10: D = _
private[this] var x1: Int = uninitialized
private[this] var x2: Unit = uninitialized
private[this] var x3: Char = uninitialized
private[this] var x4: Boolean = uninitialized
private[this] var x5: Float = uninitialized
private[this] var x6: Double = uninitialized
private[this] var x7: Char = uninitialized
private[this] var x8: Byte = uninitialized
private[this] var x9: AnyVal = uninitialized
private[this] var x10: D = uninitialized
}

class D(x: Int) extends AnyVal
Expand Down
2 changes: 1 addition & 1 deletion tests/init/crash/opassign.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ object opassign {
}

class Ref {
var x: Int = _
var x: Int = compiletime.uninitialized
}
val r = new Ref
r.x += 1
Expand Down
2 changes: 1 addition & 1 deletion tests/neg-strict/i1050.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ object Tiark3 {
def brand(x: Any): p.L = x // error: underlying not concrete
}
trait V extends U {
type X = B with A
type X = B & A
def p2: X = ???
}
val v = new V {}
Expand Down
10 changes: 10 additions & 0 deletions tests/neg-strict/i11225.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import compiletime.uninitialized

class Memo[A](x: => A):
private var cached: A = _ // error
private var known: Boolean = false
def force =
if !known then
known = true
cached = x
cached
4 changes: 2 additions & 2 deletions tests/neg-strict/nullless.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ object bar {
type U = UU
}
final lazy val nothing: Nothing = nothing
final lazy val sub: S2 with Sub = nothing
final lazy val box : Box[S2 with Sub] = new Box(nothing)
final lazy val sub: S2 & Sub = nothing
final lazy val box : Box[S2 & Sub] = new Box(nothing)
def upcast(t: box.v.M2): box.v.M2 = t // error // error under -strict
}
def main(args : Array[String]) : Unit = {
Expand Down
45 changes: 45 additions & 0 deletions tests/neg/i11225.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
-- Error: tests/neg/i11225.scala:5:16 ----------------------------------------------------------------------------------
5 | val x1: Int = uninitialized // error
| ^^^^^^^^^^^^^
| `uninitialized` can only be used as the right hand side of a mutable field definition
-- Error: tests/neg/i11225.scala:6:28 ----------------------------------------------------------------------------------
6 | var x2: Int = if ??? then uninitialized else uninitialized // error // error
| ^^^^^^^^^^^^^
| `uninitialized` can only be used as the right hand side of a mutable field definition
-- Error: tests/neg/i11225.scala:6:47 ----------------------------------------------------------------------------------
6 | var x2: Int = if ??? then uninitialized else uninitialized // error // error
| ^^^^^^^^^^^^^
| `uninitialized` can only be used as the right hand side of a mutable field definition
-- Error: tests/neg/i11225.scala:9:28 ----------------------------------------------------------------------------------
9 | var x5: () => Int = () => uninitialized // error
| ^^^^^^^^^^^^^
| `uninitialized` can only be used as the right hand side of a mutable field definition
-- Error: tests/neg/i11225.scala:10:18 ---------------------------------------------------------------------------------
10 | var x6: Int = { uninitialized } // error
| ^^^^^^^^^^^^^
| `uninitialized` can only be used as the right hand side of a mutable field definition
-- Error: tests/neg/i11225.scala:13:22 ---------------------------------------------------------------------------------
13 | var cached: Int = uninitialized // error
| ^^^^^^^^^^^^^
| `uninitialized` can only be used as the right hand side of a mutable field definition
-- Error: tests/neg/i11225.scala:14:30 ---------------------------------------------------------------------------------
14 | cached = if x then 1 else uninitialized // error
| ^^^^^^^^^^^^^
| `uninitialized` can only be used as the right hand side of a mutable field definition
-- Error: tests/neg/i11225.scala:17:4 ----------------------------------------------------------------------------------
17 | uninitialized // error
| ^^^^^^^^^^^^^
| `uninitialized` can only be used as the right hand side of a mutable field definition
-- Error: tests/neg/i11225.scala:18:4 ----------------------------------------------------------------------------------
18 | uninitialized // error
| ^^^^^^^^^^^^^
| `uninitialized` can only be used as the right hand side of a mutable field definition
-- Error: tests/neg/i11225.scala:23:4 ----------------------------------------------------------------------------------
23 | uninitialized // error
| ^^^^^^^^^^^^^
| `uninitialized` can only be used as the right hand side of a mutable field definition
-- Error: tests/neg/i11225.scala:30:16 ---------------------------------------------------------------------------------
30 | var x7: Int = uni // error
| ^^^
| `uninitialized` can only be used as the right hand side of a mutable field definition
| This location contains code that was inlined from i11225.scala:25
30 changes: 30 additions & 0 deletions tests/neg/i11225.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import compiletime.uninitialized

class Test:

val x1: Int = uninitialized // error
var x2: Int = if ??? then uninitialized else uninitialized // error // error
var x3: Int = if true then uninitialized else 1 // ok
var x4: Int = if false then uninitialized else 1 // ok
var x5: () => Int = () => uninitialized // error
var x6: Int = { uninitialized } // error

def f(x: Boolean) =
var cached: Int = uninitialized // error
cached = if x then 1 else uninitialized // error

var c: Int =
uninitialized // error
uninitialized // error
2

var d: Int =
println("pseudo init")
uninitialized // error

transparent inline def uni = uninitialized

inline def g(inline x: Int): Unit = ()
def f2 = g(uninitialized) // this one is ok since `uninitialized` is inlined away

var x7: Int = uni // error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be ok as the code we end up with is

  var x7: Int = uni  // ok
// after inlining
  var x7: Int = uninitialized // still ok

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise, we would need to make g(uninitialized) to be consistent with inlining/erased rules.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an error since we end up with Inlined(uninitialized). I think that's OK. Ideally we would not allow the inline def of uni in the first place.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it would be inconsistent with var x4: Int = if false then uninitialized else 1 // ok, since inlined away and def f2 = g(uninitialized) // this one is ok since uninitialized is inlined away. We would have some special inlining rules that only apply uninitialized which will confuse everyone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need not overthink this. The intention is that you should not use uninitialized except as the RHS of a field definition. For all practical purposes that's what this PR achieves. We don't need to split hairs beyond that,

2 changes: 1 addition & 1 deletion tests/neg/i8427.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
trait T

object Test {
var t: T = _
var t: T = compiletime.uninitialized
def main(args: Array[String]) = println("hi")
}
2 changes: 1 addition & 1 deletion tests/neg/refinedSubtyping.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Test3 {
type U1 = C { type T <: B }
type U2 = C { type T <: A }

var x: T2 = _
var x: T2 = compiletime.uninitialized
val y1: U1 = ???
val y2: U2 = ???

Expand Down
2 changes: 1 addition & 1 deletion tests/neg/t11437.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

class Regress {
var v: Int = _
var v: Int = compiletime.uninitialized
def f = 42
var w: Int = (_) // error: not default value syntax
}
Loading