Skip to content

Add an immutable array type #5885

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 13 commits into from
Feb 23, 2019
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,8 @@ object Types {
else if (tp.symbol.isAliasType) tp.underlying.underlyingClassRef(refinementOK)
else NoType
case tp: AppliedType =>
tp.superType.underlyingClassRef(refinementOK)
if (tp.tycon.isLambdaSub) NoType
else tp.superType.underlyingClassRef(refinementOK)
case tp: AnnotatedType =>
tp.underlying.underlyingClassRef(refinementOK)
case tp: RefinedType =>
Expand Down
7 changes: 6 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1737,7 +1737,12 @@ trait Applications extends Compatibility { self: Typer with Dynamic =>
val app =
typed(untpd.Apply(core, untpd.TypedSplice(receiver) :: Nil), pt1, ctx.typerState.ownedVars)(
ctx.addMode(Mode.SynthesizeExtMethodReceiver))
if (!app.symbol.is(Extension))
val appSym =
app match {
case Inlined(call, _, _) => call.symbol
case _ => app.symbol
}
if (!appSym.is(Extension))
ctx.error(em"not an extension method: $methodRef", receiver.sourcePos)
app
}
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ object VarianceChecker {
case tref: TypeParamRef if tref.binder `eq` tl =>
val v = tl.typeParams(tref.paramNum).paramVariance
varianceConforms(variance, v) || { error(tref); false }
case AnnotatedType(_, annot) if annot.symbol == defn.UncheckedVarianceAnnot =>
x
case _ =>
foldOver(x, t)
}
Expand Down
196 changes: 196 additions & 0 deletions library/src-bootstrapped/scala/IArray.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package scala
import reflect.ClassTag

/** An immutable array. An `IArray[T]` has the same representation as an `Array[T]`,
* but it cannot be updated. Unlike regular arrays, immutable arrays are covariant.
*/
opaque type IArray[+T] = Array[_ <: T]

object IArray {

/** Defines extension methods for immutable arrays */
implied arrayOps {

/** The selection operation on an immutable array.
*
* @param arr the immutable array
* @param n the index of the element to select
* @return the element of the array at the given index
*/
inline def (arr: IArray[T]) apply[T] (n: Int): T = arr.asInstanceOf[Array[T]].apply(n)

/** The number of elements in an immutable array
* @param arr the immutable array
*/
inline def (arr: IArray[T]) length[T] : Int = arr.asInstanceOf[Array[T]].length
}

/** An immutable array of length 0.
*/
def empty[T: ClassTag]: IArray[T] = new Array[T](0)

/** An immutable array with given elements.
*/
def apply[T: ClassTag](xs: T*): IArray[T] = Array(xs: _*)
def apply(x: Boolean, xs: Boolean*): IArray[Boolean] = Array(x, xs: _*)
def apply(x: Byte, xs: Byte*): IArray[Byte] = Array(x, xs: _*)
def apply(x: Short, xs: Short*): IArray[Short] = Array(x, xs: _*)
def apply(x: Char, xs: Char*): IArray[Char] = Array(x, xs: _*)
def apply(x: Int, xs: Int*): IArray[Int] = Array(x, xs: _*)
def apply(x: Long, xs: Long*): IArray[Long] = Array(x, xs: _*)
def apply(x: Float, xs: Float*): IArray[Float] = Array(x, xs: _*)
def apply(x: Double, xs: Double*): IArray[Double] = Array(x, xs: _*)
def apply(x: Unit, xs: Unit*): IArray[Unit] = Array(x, xs: _*)

/** Concatenates all arrays into a single immutable array.
*
* @param xss the given immutable arrays
* @return the array created from concatenating `xss`
*/
def concat[T: ClassTag](xss: IArray[T]*): IArray[T] = Array.concat[T](xss.asInstanceOf[Seq[Array[T]]]: _*)

/** Returns an immutable array that contains the results of some element computation a number
* of times. Each element is determined by a separate computation.
*
* @param n the number of elements in the array
* @param elem the element computation
*/
def fill[T: ClassTag](n: Int)(elem: => T): IArray[T] =
Array.fill(n)(elem)

/** Returns a two-dimensional immutable array that contains the results of some element computation a number
* of times. Each element is determined by a separate computation.
*
* @param n1 the number of elements in the 1st dimension
* @param n2 the number of elements in the 2nd dimension
* @param elem the element computation
*/
def fill[T: ClassTag](n1: Int, n2: Int)(elem: => T): IArray[IArray[T]] =
Array.fill(n1, n2)(elem)

/** Returns a three-dimensional immutable array that contains the results of some element computation a number
* of times. Each element is determined by a separate computation.
*
* @param n1 the number of elements in the 1st dimension
* @param n2 the number of elements in the 2nd dimension
* @param n3 the number of elements in the 3nd dimension
* @param elem the element computation
*/
def fill[T: ClassTag](n1: Int, n2: Int, n3: Int)(elem: => T): IArray[IArray[IArray[T]]] =
Array.fill(n1, n2, n3)(elem)

/** Returns a four-dimensional immutable array that contains the results of some element computation a number
* of times. Each element is determined by a separate computation.
*
* @param n1 the number of elements in the 1st dimension
* @param n2 the number of elements in the 2nd dimension
* @param n3 the number of elements in the 3nd dimension
* @param n4 the number of elements in the 4th dimension
* @param elem the element computation
*/
def fill[T: ClassTag](n1: Int, n2: Int, n3: Int, n4: Int)(elem: => T): IArray[IArray[IArray[IArray[T]]]] =
Array.fill(n1, n2, n3, n4)(elem)

/** Returns a five-dimensional immutable array that contains the results of some element computation a number
* of times. Each element is determined by a separate computation.
*
* @param n1 the number of elements in the 1st dimension
* @param n2 the number of elements in the 2nd dimension
* @param n3 the number of elements in the 3nd dimension
* @param n4 the number of elements in the 4th dimension
* @param n5 the number of elements in the 5th dimension
* @param elem the element computation
*/
def fill[T: ClassTag](n1: Int, n2: Int, n3: Int, n4: Int, n5: Int)(elem: => T): IArray[IArray[IArray[IArray[IArray[T]]]]] =
Array.fill(n1, n2, n3, n4, n5)(elem)

/** Returns an immutable array containing values of a given function over a range of integer
* values starting from 0.
*
* @param n The number of elements in the array
* @param f The function computing element values
*/
def tabulate[T: ClassTag](n: Int)(f: Int => T): IArray[T] =
Array.tabulate(n)(f)

/** Returns a two-dimensional immutable array containing values of a given function
* over ranges of integer values starting from `0`.
*
* @param n1 the number of elements in the 1st dimension
* @param n2 the number of elements in the 2nd dimension
* @param f The function computing element values
*/
def tabulate[T: ClassTag](n1: Int, n2: Int)(f: (Int, Int) => T): IArray[IArray[T]] =
Array.tabulate(n1, n2)(f)

/** Returns a three-dimensional immutable array containing values of a given function
* over ranges of integer values starting from `0`.
*
* @param n1 the number of elements in the 1st dimension
* @param n2 the number of elements in the 2nd dimension
* @param n3 the number of elements in the 3rd dimension
* @param f The function computing element values
*/
def tabulate[T: ClassTag](n1: Int, n2: Int, n3: Int)(f: (Int, Int, Int) => T): IArray[IArray[IArray[T]]] =
Array.tabulate(n1, n2, n3)(f)

/** Returns a four-dimensional immutable array containing values of a given function
* over ranges of integer values starting from `0`.
*
* @param n1 the number of elements in the 1st dimension
* @param n2 the number of elements in the 2nd dimension
* @param n3 the number of elements in the 3rd dimension
* @param n4 the number of elements in the 4th dimension
* @param f The function computing element values
*/
def tabulate[T: ClassTag](n1: Int, n2: Int, n3: Int, n4: Int)(f: (Int, Int, Int, Int) => T): IArray[IArray[IArray[IArray[T]]]] =
Array.tabulate(n1, n2, n3, n4)(f)

/** Returns a five-dimensional immutable array containing values of a given function
* over ranges of integer values starting from `0`.
*
* @param n1 the number of elements in the 1st dimension
* @param n2 the number of elements in the 2nd dimension
* @param n3 the number of elements in the 3rd dimension
* @param n4 the number of elements in the 4th dimension
* @param n5 the number of elements in the 5th dimension
* @param f The function computing element values
*/
def tabulate[T: ClassTag](n1: Int, n2: Int, n3: Int, n4: Int, n5: Int)(f: (Int, Int, Int, Int, Int) => T): IArray[IArray[IArray[IArray[IArray[T]]]]] =
Array.tabulate(n1, n2, n3, n4, n5)(f)

/** Returns an immutable array containing a sequence of increasing integers in a range.
*
* @param start the start value of the array
* @param end the end value of the array, exclusive (in other words, this is the first value '''not''' returned)
* @return the immutable array with values in range `start, start + 1, ..., end - 1`
* up to, but excluding, `end`.
*/
def range(start: Int, end: Int): IArray[Int] = Array.range(start, end)

/** Returns an immutable array containing equally spaced values in some integer interval.
*
* @param start the start value of the array
* @param end the end value of the array, exclusive (in other words, this is the first value '''not''' returned)
* @param step the increment value of the array (may not be zero)
* @return the immutable array with values in `start, start + step, ...` up to, but excluding `end`
*/
def range(start: Int, end: Int, step: Int): IArray[Int] = Array.range(start, end, step)

/** Returns an immutable array containing repeated applications of a function to a start value.
*
* @param start the start value of the array
* @param len the number of elements returned by the array
* @param f the function that is repeatedly applied
* @return the immutable array returning `len` values in the sequence `start, f(start), f(f(start)), ...`
*/
def iterate[T: ClassTag](start: T, len: Int)(f: T => T): IArray[T] = Array.iterate(start, len)(f)

/** Returns a decomposition of the array into a sequence. This supports
* a pattern match like `{ case IArray(x,y,z) => println('3 elements')}`.
*
* @param x the selector value
* @return sequence wrapped in a [[scala.Some]], if `x` is a Seq, otherwise `None`
*/
def unapplySeq[T](x: IArray[T]) = Array.unapplySeq[T](x.asInstanceOf[Array[T]])
}
15 changes: 15 additions & 0 deletions tests/neg/alloc-abstract.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class Test[T] {
type U <: T

type Foo[T] = Array[T]

new T // error: not a class type
new T() // error: not a class type
new U // error: not a class type
new U() // error: not a class type
new IArray[String] // error: not a class type
new IArray[String]() // error: not a class type
new IArray[String](10) // error: not a class type // error: too mamy arguments

new Foo[String](10) // ok
}
15 changes: 15 additions & 0 deletions tests/neg/iarrays.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
object Test {

// Can't allocate an IArray
new IArray[String](10) // error: not a class type // error: too many arguments

val xs = IArray(1, 2, 3)

// Can't have a wildcard IArray
val ys: IArray[_] = xs

// Can't update an IArray
xs(0) = 1 // error: value update is not a member
xs(1) += 1 // error: value += is not a member

}
10 changes: 10 additions & 0 deletions tests/neg/iarrays1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
object Test {

// Can't allocate an IArray

val xs = IArray(1, 2, 3)

// Can't have a wildcard IArray
val ys: IArray[_] = xs // error: unreducible application

}
2 changes: 1 addition & 1 deletion tests/neg/parser-stability-21.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
class x0[x1[]] // error
extends x1[ // error
extends x1[
// error
3 changes: 3 additions & 0 deletions tests/pos/covariant-opaque.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import annotation.unchecked.uncheckedVariance

opaque type O[+T] = Array[T @uncheckedVariance]
73 changes: 73 additions & 0 deletions tests/run/iarrays.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import reflect.ClassTag
object Test extends App {

val xs = IArray(1, 2, 3)

def f[T](ys: IArray[T]) = {
Copy link
Contributor

@julienrf julienrf Feb 9, 2019

Choose a reason for hiding this comment

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

What type does this method erase to? f(ys: Array[AnyRef])? Or f(ys: AnyRef)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it's Object. Just what Array[T] erases to.

assert(ys.length == 3)
var sum = 0
for (i <- 0 until ys.length)
sum += xs(i)
assert(sum == 6)
}

f(xs)

var sum = 0
for (i <- 0 until xs.length)
sum += xs(i)
assert(sum == 6)

def reduce[T](xs: IArray[T], z: T, op: (T, T) => T) = {
var acc = z
for (i <- 0 until xs.length)
acc = op(acc, xs(i))
acc
}

def reduce2[T <: AnyRef](xs: IArray[T], z: T, op: (T, T) => T) = {
var acc = z
for (i <- 0 until xs.length)
acc = op(acc, xs(i))
acc
}

def flatten[T: ClassTag](ys: IArray[IArray[T]]) = {
var len = 0
for (i <- 0 until ys.length) len += ys(i).length
val flat = new Array[T](len)
var k = 0
for (i <- 0 until ys.length) {
for (j <- 0 until ys(i).length) {
flat(k) = ys(i)(j)
k += 1
}
}
IArray(flat: _*)
}

val ys = IArray.concat(xs, xs, xs)
assert(reduce(ys, 0, _ + _) == 18)

val ss = IArray("a", "b", "c")
assert(reduce2(ss, "", _ ++ _) == "abc")

val zss = IArray.fill(2, 3)(1)
val zs = flatten(zss)
assert(reduce(zs, 0, _ + _) == 6)

val is = IArray.iterate(0, 4)(_ + 1)
assert(reduce(is, 0, _ + _) == 6)

val IArray(1, 2, 3) = xs

val as: IArray[Any] = IArray(1, "hello")
assert(as(as.length - 1) == "hello")
assert(reduce(as, 0, (x, y) => x.toString ++ y.toString) == "01hello")

// Check that representation of IArray and Array is the same
val bs: IArray[Double] = IArray(1.0, 2.0)
val cs: Array[Double] = bs.asInstanceOf[Array[Double]]
cs(1) = 3.0
assert(bs(1) == 3.0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we also check that apply and update don’t box elements?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's harder. I believe there are ways to test it, but I don't know them. The fact that apply is an inline method means it is literally replaced by its right-hand side, so by the rules of inlining there can't be any boxing. I verified the code after erasure by looking at it to make sure that's the case.

}