-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Minimal enums with precise apply methods #9922
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
+6 −4 | unitTests/src/test/scala/scodec/codecs/DiscriminatorCodecTest.scala |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -350,13 +350,24 @@ trait ConstraintHandling { | |
val tpw = tp.widenSingletons | ||
if (tpw ne tp) && (tpw <:< bound) then tpw else tp | ||
|
||
def widenEnum(tp: Type) = | ||
val tpw = tp.widenEnumCase | ||
if (tpw ne tp) && (tpw <:< bound) then tpw else tp | ||
|
||
def isSingleton(tp: Type): Boolean = tp match | ||
case WildcardType(optBounds) => optBounds.exists && isSingleton(optBounds.bounds.hi) | ||
case _ => isSubTypeWhenFrozen(tp, defn.SingletonType) | ||
|
||
def isEnumCase(tp: Type): Boolean = tp match | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might want to extract the commonalities of isEnumCase and isSingleton into a method def widenWildcard(tp: Type) = tp match
case WildcardType(optBounds) => optBounds
case _ => tp |
||
case WildcardType(optBounds) => optBounds.exists && isEnumCase(optBounds.bounds.hi) | ||
case _ => tp.classSymbol.isAllOf(EnumCase, butNot=JavaDefined) | ||
|
||
val wideInst = | ||
if isSingleton(bound) then inst | ||
else dropSuperTraits(widenOr(widenSingle(inst))) | ||
else | ||
val lub = widenOr(widenSingle(inst)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nicer if dropSuperTraits(widenEnum(widenOr(widenSingle(inst)))) here |
||
val asAdt = if isEnumCase(bound) then lub else widenEnum(lub) | ||
dropSuperTraits(asAdt) | ||
wideInst match | ||
case wideInst: TypeRef if wideInst.symbol.is(Module) => | ||
TermRef(wideInst.prefix, wideInst.symbol.sourceModule) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1146,6 +1146,11 @@ object Types { | |
case _ => this | ||
} | ||
|
||
/** if this type is a reference to a class case of an enum, replace it by its first parent */ | ||
final def widenEnumCase(using Context): Type = this match | ||
case tp: (TypeRef | AppliedType) if tp.classSymbol.isAllOf(EnumCase) => tp.parents.head | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the restriction to TypeRefs and AppliedTypes? What should happen with TypeVars, annotated types, or aliases? if tp.classSymbol.isAllOf(EnumCase) then tp.parents.head else tp |
||
case _ => this | ||
|
||
/** Widen this type and if the result contains embedded union types, replace | ||
* them by their joins. | ||
* "Embedded" means: inside type lambdas, intersections or recursive types, or in prefixes of refined types. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1399,8 +1399,8 @@ object Scanners { | |
|
||
object IndentWidth { | ||
private inline val MaxCached = 40 | ||
private val spaces = Array.tabulate(MaxCached + 1)(new Run(' ', _)) | ||
private val tabs = Array.tabulate(MaxCached + 1)(new Run('\t', _)) | ||
private val spaces = Array.tabulate[Run](MaxCached + 1)(new Run(' ', _)) // TODO: remove new after bootstrap | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact, this gives me pause. Is this really what we want? I find it surprising and unnatural that one needs to add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An alternative design would be more local: Instead of widening enum cases everywhere where they take part in inference we only decide at the application itself. I.e. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want an Array of Run you can write explicitly There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. I mean the worrying thing is that the |
||
private val tabs = Array.tabulate[Run](MaxCached + 1)(new Run('\t', _)) // TODO: remove new after bootstrap | ||
|
||
def Run(ch: Char, n: Int): Run = | ||
if (n <= MaxCached && ch == ' ') spaces(n) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
scala> enum Maybe[+T] { case Something(value: T); case EmptyValue }; import Maybe._ | ||
// defined class Maybe | ||
|
||
scala> List(Something(1)) | ||
val res0: List[Maybe[Int]] = List(Something(1)) | ||
|
||
scala> def listOfSomething[O <: Maybe.Something[_]](listOfSomething: List[O]): listOfSomething.type = listOfSomething | ||
def listOfSomething | ||
[O <: Maybe.Something[?]](listOfSomething: List[O]): listOfSomething.type | ||
|
||
scala> listOfSomething(List(Something(1))) | ||
val res1: List[Maybe.Something[Int]] = List(Something(1)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
enum Foo3[T](x: T) { | ||
case Bar[S, T](y: T) extends Foo3[y.type](y) | ||
} | ||
|
||
val foo: Foo3.Bar[Nothing, 3] = Foo3.Bar(3) | ||
val bar = foo | ||
|
||
def baz[T](f: Foo3[T]): f.type = f | ||
|
||
val qux = baz(bar) // existentials are back in Dotty? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import Nat._ | ||
|
||
inline def toIntMacro(inline nat: Nat): Int = ${ Macros.toIntImpl('nat) } | ||
inline def ZeroMacro: Zero.type = ${ Macros.natZero } | ||
transparent inline def toNatMacro(inline int: Int): Nat = ${ Macros.toNatImpl('int) } | ||
|
||
object Macros: | ||
import quoted._ | ||
|
||
def toIntImpl(nat: Expr[Nat])(using QuoteContext): Expr[Int] = | ||
|
||
def inner(nat: Expr[Nat], acc: Int): Int = nat match | ||
case '{ Succ($nat) } => inner(nat, acc + 1) | ||
case '{ Zero } => acc | ||
|
||
Expr(inner(nat, 0)) | ||
|
||
def natZero(using QuoteContext): Expr[Nat.Zero.type] = '{Zero} | ||
|
||
def toNatImpl(int: Expr[Int])(using QuoteContext): Expr[Nat] = | ||
|
||
// it seems even with the bound that the arg will always widen to Expr[Nat] unless explicit | ||
|
||
def inner[N <: Nat: Type](int: Int, acc: Expr[N]): Expr[Nat] = int match | ||
case 0 => acc | ||
case n => inner[Succ[N]](n - 1, '{Succ($acc)}) | ||
|
||
val Const(i) = int | ||
require(i >= 0) | ||
inner[Zero.type](i, '{Zero}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
enum Nat: | ||
case Zero | ||
case Succ[N <: Nat](n: N) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import Nat._ | ||
|
||
@main def Test: Unit = | ||
assert(toIntMacro(Succ(Succ(Succ(Zero)))) == 3) | ||
assert(toNatMacro(3) == Succ(Succ(Succ(Zero)))) | ||
val zero: Zero.type = ZeroMacro | ||
assert(zero == Zero) | ||
assert(toIntMacro(toNatMacro(3)) == 3) | ||
val n: Succ[Succ[Succ[Zero.type]]] = toNatMacro(3) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,5 +11,7 @@ true | |
|
||
true | ||
|
||
true | ||
|
||
false | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
enum NonEmptyList[+T]: | ||
case Many[+U](head: U, tail: NonEmptyList[U]) extends NonEmptyList[U] | ||
case One [+U](value: U) extends NonEmptyList[U] | ||
|
||
enum Ast: | ||
case Binding(name: String, tpe: String) | ||
case Lambda(args: NonEmptyList[Binding], rhs: Ast) // reference to another case of the enum | ||
case Ident(name: String) | ||
case Apply(fn: Ast, args: NonEmptyList[Ast]) | ||
|
||
import NonEmptyList._ | ||
import Ast._ | ||
|
||
// This example showcases the widening when inferring enum case types. | ||
// With scala 2 case class hierarchies, if One.apply(1) returns One[Int] and Many.apply(2, One(3)) returns Many[Int] | ||
// then the `foldRight` expression below would complain that Many[Binding] is not One[Binding]. With Scala 3 enums, | ||
// .apply on the companion returns the precise class, but type inference will widen to NonEmptyList[Binding] unless | ||
// the precise class is expected. | ||
def Bindings(arg: (String, String), args: (String, String)*): NonEmptyList[Binding] = | ||
def Bind(arg: (String, String)): Binding = | ||
val (name, tpe) = arg | ||
Binding(name, tpe) | ||
|
||
args.foldRight(One[Binding](Bind(arg)))((arg, acc) => Many(Bind(arg), acc)) | ||
|
||
@main def Test: Unit = | ||
val OneOfOne: One[1] = One[1](1) | ||
val True = Lambda(Bindings("x" -> "T", "y" -> "T"), Ident("x")) | ||
val Const = Lambda(One(Binding("x", "T")), Lambda(One(Binding("y", "U")), Ident("x"))) // precise type is forwarded | ||
|
||
assert(OneOfOne.value == 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's nice that we can simplify desugaring in a significant way.