Skip to content
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

Named tuples experimental first implementation #19075

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 12 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
108 changes: 94 additions & 14 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import Decorators.*
import Annotations.Annotation
import NameKinds.{UniqueName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName}
import typer.{Namer, Checking}
import util.{Property, SourceFile, SourcePosition, Chars}
import util.{Property, SourceFile, SourcePosition, SrcPos, Chars}
import config.Feature.{sourceVersion, migrateTo3, enabled}
import config.SourceVersion.*
import collection.mutable.ListBuffer
import collection.mutable
import reporting.*
import annotation.constructorOnly
import printing.Formatting.hl
Expand Down Expand Up @@ -242,7 +242,7 @@ object desugar {

private def elimContextBounds(meth: DefDef, isPrimaryConstructor: Boolean)(using Context): DefDef =
val DefDef(_, paramss, tpt, rhs) = meth
val evidenceParamBuf = ListBuffer[ValDef]()
val evidenceParamBuf = mutable.ListBuffer[ValDef]()

var seenContextBounds: Int = 0
def desugarContextBounds(rhs: Tree): Tree = rhs match
Expand Down Expand Up @@ -1445,22 +1445,102 @@ object desugar {
AppliedTypeTree(
TypeTree(defn.throwsAlias.typeRef).withSpan(op.span), tpt :: excepts :: Nil)

private def checkWellFormedTupleElems(elems: List[Tree])(using Context) =
val seen = mutable.Set[Name]()
for case arg @ NamedArg(name, _) <- elems do
if seen.contains(name) then
report.error(em"Duplicate tuple element name", arg.srcPos)
seen += name
if name.startsWith("_") && name.toString.tail.toIntOption.isDefined then
report.error(
em"$name cannot be used as the name of a tuple element because it is a regular tuple selector",
arg.srcPos)

elems match
case elem :: elems1 =>
val misMatchOpt =
if elem.isInstanceOf[NamedArg]
then elems1.find(!_.isInstanceOf[NamedArg])
else elems1.find(_.isInstanceOf[NamedArg])
for misMatch <- misMatchOpt do
report.error(em"Illegal combination of named and unnamed tuple elements", misMatch.srcPos)
case _ =>
end checkWellFormedTupleElems

/** Translate tuple expressions of arity <= 22
*
* () ==> ()
* (t) ==> t
* (t1, ..., tN) ==> TupleN(t1, ..., tN)
*/
def smallTuple(tree: Tuple)(using Context): Tree = {
val ts = tree.trees
val arity = ts.length
assert(arity <= Definitions.MaxTupleArity)
def tupleTypeRef = defn.TupleType(arity).nn
if (arity == 0)
if (ctx.mode is Mode.Type) TypeTree(defn.UnitType) else unitLiteral
else if (ctx.mode is Mode.Type) AppliedTypeTree(ref(tupleTypeRef), ts)
else Apply(ref(tupleTypeRef.classSymbol.companionModule.termRef), ts)
}
def tuple(tree: Tuple, pt: Type)(using Context): (Tree, Type) =
checkWellFormedTupleElems(tree.trees)
val (adapted, pt1) = adaptTupleElems(tree.trees, pt)
val elems = adapted.mapConserve(desugarTupleElem)
val arity = elems.length
if arity <= Definitions.MaxTupleArity then
def tupleTypeRef = defn.TupleType(arity).nn
val tree1 =
if arity == 0 then
if ctx.mode is Mode.Type then TypeTree(defn.UnitType) else unitLiteral
else if ctx.mode is Mode.Type then AppliedTypeTree(ref(tupleTypeRef), elems)
else Apply(ref(tupleTypeRef.classSymbol.companionModule.termRef), elems)
(tree1.withSpan(tree.span), pt1)
else
(cpy.Tuple(tree)(elems), pt1)

/** When desugaring a pattern, adapt tuple elements `elems` and expected type `pt`
* to each other. This means:
* - If `elems` are named pattern elements, rearrange them to match `pt`.
* This requires all names in `elems` to be also present in `pt`.
* - If `elems` are unnamed elements, drop any tuple element names from `pt`.
*/
def adaptTupleElems(elems: List[Tree], pt: Type)(using Context): (List[Tree], Type) =

def reorderedNamedArgs(selElems: List[Type], wildcardSpan: Span): List[untpd.Tree] =
val nameIdx =
for case (defn.NamedTupleElem(name, _), idx) <- selElems.zipWithIndex yield
(name, idx)
val nameToIdx = nameIdx.toMap[Name, Int]
val reordered = Array.fill[untpd.Tree](selElems.length):
untpd.Ident(nme.WILDCARD).withSpan(wildcardSpan)
for case arg @ NamedArg(name, _) <- elems do
nameToIdx.get(name) match
case Some(idx) =>
if reordered(idx).isInstanceOf[Ident] then
reordered(idx) = arg
else
report.error(em"Duplicate named pattern", arg.srcPos)
case _ =>
report.error(em"No element named `$name` is defined in selector type $pt", arg.srcPos)
reordered.toList

if ctx.mode.is(Mode.Pattern) then
elems match
case (first @ NamedArg(_, _)) :: _ =>
(reorderedNamedArgs(pt.tupleElementTypes.getOrElse(Nil), first.span.startPos), pt)
case _ =>
(elems, pt.dropNamedTupleElems)
else
(elems, pt)
end adaptTupleElems

private def desugarTupleElem(elem: Tree)(using Context): Tree = elem match
case NamedArg(name, arg) =>
locally:
val nameLit = Literal(Constant(name.toString))
if ctx.mode.is(Mode.Type) then
AppliedTypeTree(ref(defn.NamedTuple_ElementTypeRef),
SingletonTypeTree(nameLit) :: arg :: Nil)
else if ctx.mode.is(Mode.Pattern) then
NamedElemPattern(name, arg)
else
Apply(
Select(ref(defn.NamedTuple_ElementModuleRef), nme.apply),
nameLit :: arg :: Nil)
.withSpan(elem.span)
case _ =>
elem

private def isTopLevelDef(stat: Tree)(using Context): Boolean = stat match
case _: ValDef | _: PatDef | _: DefDef | _: Export | _: ExtMethods => true
Expand Down Expand Up @@ -1977,7 +2057,7 @@ object desugar {
* without duplicates
*/
private def getVariables(tree: Tree, shouldAddGiven: Context ?=> Bind => Boolean)(using Context): List[VarInfo] = {
val buf = ListBuffer[VarInfo]()
val buf = mutable.ListBuffer[VarInfo]()
def seenName(name: Name) = buf exists (_._1.name == name)
def add(named: NameTree, t: Tree): Unit =
if (!seenName(named.name) && named.name.isTermName) buf += ((named, t))
Expand Down
25 changes: 25 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,31 @@ trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped]
if id.span == result.span.startPos => Some(result)
case _ => None
end ImpureByNameTypeTree

/** The desugared version of a named tuple element pattern `name = elem`
* (unapply is currently unused)
*/
object NamedElemPattern:

def apply(name: Name, elem: Tree)(using Context): Tree =
Apply(
Block(Nil,
TypeApply(
untpd.Select(untpd.ref(defn.NamedTuple_ElementModuleRef), nme.extract),
SingletonTypeTree(Literal(Constant(name.toString))) :: Nil)),
elem :: Nil)

def unapply(tree: Tree)(using Context): Option[(TermName, Tree)] = tree match
case Apply(
Block(Nil,
TypeApply(
untpd.Select(TypedSplice(namedValue), nme.extract),
SingletonTypeTree(Literal(Constant(name: String))) :: Nil)),
elem :: Nil) if namedValue.symbol == defn.NamedTuple_ElementModuleRef.symbol =>
Some((name.toTermName, elem))
case _ => None

end NamedElemPattern
}

trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
Expand Down
8 changes: 4 additions & 4 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -530,15 +530,15 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
def makeSelfDef(name: TermName, tpt: Tree)(using Context): ValDef =
ValDef(name, tpt, EmptyTree).withFlags(PrivateLocal)

def makeTupleOrParens(ts: List[Tree])(using Context): Tree = ts match {
def makeTupleOrParens(ts: List[Tree])(using Context): Tree = ts match
case (t: NamedArg) :: Nil => Tuple(t :: Nil)
case t :: Nil => Parens(t)
case _ => Tuple(ts)
}

def makeTuple(ts: List[Tree])(using Context): Tree = ts match {
def makeTuple(ts: List[Tree])(using Context): Tree = ts match
case (t: NamedArg) :: Nil => Tuple(t :: Nil)
case t :: Nil => t
case _ => Tuple(ts)
}

def makeAndType(left: Tree, right: Tree)(using Context): AppliedTypeTree =
AppliedTypeTree(ref(defn.andType.typeRef), left :: right :: Nil)
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ object Feature:
val pureFunctions = experimental("pureFunctions")
val captureChecking = experimental("captureChecking")
val into = experimental("into")
val namedTuples = experimental("namedTuples")

val globalOnlyImports: Set[TermName] = Set(pureFunctions, captureChecking)

Expand Down
19 changes: 19 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Comments.Comment
import util.Spans.NoSpan
import config.Feature
import Symbols.requiredModuleRef
import Constants.Constant
import cc.{CaptureSet, RetainingType}
import ast.tpd.ref

Expand Down Expand Up @@ -937,6 +938,7 @@ class Definitions {
def TupleClass(using Context): ClassSymbol = TupleTypeRef.symbol.asClass
@tu lazy val Tuple_cons: Symbol = TupleClass.requiredMethod("*:")
@tu lazy val TupleModule: Symbol = requiredModule("scala.Tuple")

@tu lazy val EmptyTupleClass: Symbol = requiredClass("scala.EmptyTuple")
@tu lazy val EmptyTupleModule: Symbol = requiredModule("scala.EmptyTuple")
@tu lazy val NonEmptyTupleTypeRef: TypeRef = requiredClassRef("scala.NonEmptyTuple")
Expand All @@ -950,6 +952,15 @@ class Definitions {
def TupleXXL_fromIterator(using Context): Symbol = TupleXXLModule.requiredMethod("fromIterator")
def TupleXXL_unapplySeq(using Context): Symbol = TupleXXLModule.requiredMethod(nme.unapplySeq)

@tu lazy val NamedTupleModule = requiredModule("scala.NamedTuple")

def NamedTuple_ElementTypeRef: TypeRef = NamedTupleModule.termRef.select("Element".toTypeName).asInstanceOf
def NamedTuple_ElementModuleRef: TermRef = NamedTupleModule.termRef.select("Element".toTermName).asInstanceOf
// Note: It would be dangerous to expose Element as a symbol, since
// Element.{typeRef/termRef} give the internal view of Element inside NamedTuple
// which reveals the opaque alias. To see it externally, we need the construction
// above. Without this tweak, named-tuples.scala fails -Ycheck after typer.

@tu lazy val RuntimeTupleMirrorTypeRef: TypeRef = requiredClassRef("scala.runtime.TupleMirror")

@tu lazy val RuntimeTuplesModule: Symbol = requiredModule("scala.runtime.Tuples")
Expand Down Expand Up @@ -1302,6 +1313,14 @@ class Definitions {
case ByNameFunction(_) => true
case _ => false

object NamedTupleElem:
def apply(name: Name, tp: Type)(using Context): Type =
AppliedType(NamedTuple_ElementTypeRef, ConstantType(Constant(name.toString)) :: tp :: Nil)
def unapply(t: Type)(using Context): Option[(TermName, Type)] = t match
case AppliedType(tycon, ConstantType(Constant(s: String)) :: tp :: Nil)
if tycon.typeSymbol == NamedTuple_ElementTypeRef.typeSymbol => Some((s.toTermName, tp))
case _ => None

final def isCompiletime_S(sym: Symbol)(using Context): Boolean =
sym.name == tpnme.S && sym.owner == CompiletimeOpsIntModuleClass

Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ object StdNames {
val eqlAny: N = "eqlAny"
val ex: N = "ex"
val extension: N = "extension"
val extract: N = "extract"
val experimental: N = "experimental"
val f: N = "f"
val false_ : N = "false"
Expand Down
30 changes: 29 additions & 1 deletion compiler/src/dotty/tools/dotc/core/TypeUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,12 @@ class TypeUtils {
case tp: AppliedType if defn.isTupleNType(tp) && normalize =>
Some(tp.args) // if normalize is set, use the dealiased tuple
// otherwise rely on the default case below to print unaliased tuples.
case tp: SkolemType =>
recur(tp.underlying, bound)
case tp: SingletonType =>
if tp.termSymbol == defn.EmptyTupleModule then Some(Nil) else None
if tp.termSymbol == defn.EmptyTupleModule then Some(Nil)
else if normalize then recur(tp.widen, bound)
else None
case _ =>
if defn.isTupleClass(tp.typeSymbol) && !normalize then Some(tp.dealias.argInfos)
else None
Expand All @@ -84,6 +88,30 @@ class TypeUtils {
case Some(elems) if elems.length <= Definitions.MaxTupleArity => true
case _ => false

/** Is this type a named tuple element `name = value`? */
def isNamedTupleElem(using Context): Boolean = dropNamedTupleElem ne self

/** Rewrite `name = elem` to `elem` */
def dropNamedTupleElem(using Context) = self match
case defn.NamedTupleElem(_, elem) => elem
case elem => elem

/** Drop all named elements in tuple type */
def dropNamedTupleElems(using Context): Type = self match
case AppliedType(tycon, hd :: tl :: Nil) if tycon.isRef(defn.PairClass) =>
val hd1 = hd.dropNamedTupleElem
val tl1 = tl.dropNamedTupleElems
if (hd1 eq hd) && (tl1 eq tl) then self else AppliedType(tycon, hd1 :: tl1 :: Nil)
case tp @ AppliedType(tycon, args) if defn.isTupleNType(tp) =>
tp.derivedAppliedType(tycon, args.mapConserve(_.dropNamedTupleElem))
case _ =>
if self.termSymbol ne defn.EmptyTupleModule then
val normed = self.widen.normalized.dealias
if normed ne self then
val normed1 = normed.dropNamedTupleElems
if normed1 ne normed then return normed1
self

/** The `*:` equivalent of an instance of a Tuple class */
def toNestedPairs(using Context): Type =
tupleElementTypes match
Expand Down
Loading