Skip to content

[WIP] Named tuples PR-PR #19532

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

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6c0d6eb
Improvements to tuples: Drop experimental
odersky Dec 13, 2023
4ce52c1
Improvements to Tuples: New methods
odersky Dec 13, 2023
f3a7741
Improvements to tuples: Allow prefix slice in fromArray
odersky Dec 13, 2023
c132083
Improvements to tuples: Rearrange types into a more logical order
odersky Dec 13, 2023
e5def8e
Improvements to tuples: more new types and methods
odersky Dec 13, 2023
f7cf7c0
Add NamedTuple object to library
odersky Dec 2, 2023
467c765
Support for named tuples with new representation
odersky Dec 3, 2023
be02de1
Add doc page
odersky Dec 3, 2023
6b83159
Make NamedTuple covariant in its value type
odersky Dec 3, 2023
6225e0b
Various tweaks
odersky Dec 5, 2023
9651f62
Harden NamedTuple handling against ill-formed NamedTuples
odersky Dec 5, 2023
f93275c
Fix test
odersky Dec 5, 2023
8fe002e
Simplify tupleElementTypes unapply handling
odersky Dec 8, 2023
d687d20
Fix pattern matching for get matches
odersky Dec 8, 2023
82b5fe5
Another fix for named get patterns
odersky Dec 14, 2023
fc0078f
Avoid widening into unreducible types when inferring types
odersky Dec 14, 2023
0693b6b
Fix rebase breakage
odersky Jan 12, 2024
6e3ec86
Make NamedTuples work under new MatchType spec
odersky Jan 12, 2024
c5ac4aa
Avoid TypeError exception in RefinedPrinter
odersky Jan 12, 2024
2354b45
Move named-tuples-strawman.ccala to pending
odersky Jan 12, 2024
9bbe89a
Update check file
odersky Jan 12, 2024
f33e496
Better printing of NamedTuple type trees
odersky Jan 14, 2024
b0ab161
Add FieldsOf type
odersky Jan 14, 2024
19b4b2c
Describe and add tests for source incompabilities
odersky Jan 14, 2024
b3fb14d
Rename NamedTuple.FieldsOf --> NamedTuple.From
odersky Jan 14, 2024
cb9aa46
Implement Fields as a Selectable type member
odersky Feb 5, 2024
5a31ba1
Refactor typedSelect
odersky Feb 5, 2024
9331dd9
Add section on computable field names to reference doc page
odersky Feb 5, 2024
7b15378
Fix fields as a selectable type member
odersky Feb 6, 2024
8ebbf16
Make NamedTuple.From work for named tuple arguments
odersky Feb 6, 2024
fea6fff
Fix NamedArg term/type classification
odersky Feb 7, 2024
8adffb7
Fix rebase breakage
odersky Feb 7, 2024
1ba6a94
Drop unnamed tuple <: named tuple, but allow literal tuple to conform
dwijnand Jan 25, 2024
47facee
Don't force type vars while typing (named) tuples
dwijnand Jan 27, 2024
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
112 changes: 98 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 @@ -248,7 +248,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 @@ -1455,22 +1455,106 @@ object desugar {
AppliedTypeTree(
TypeTree(defn.throwsAlias.typeRef).withSpan(op.span), tpt :: excepts :: Nil)

private def checkWellFormedTupleElems(elems: List[Tree])(using Context): List[Tree] =
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])
mismatchOpt match
case Some(misMatch) =>
report.error(em"Illegal combination of named and unnamed tuple elements", misMatch.srcPos)
elems.mapConserve(dropNamedArg)
case None => elems
case _ => elems
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 =
var elems = checkWellFormedTupleElems(tree.trees)
if ctx.mode.is(Mode.Pattern) then elems = adaptPatternArgs(elems, pt)
val elemValues = elems.mapConserve(dropNamedArg)
val tup =
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), elemValues)
else Apply(ref(tupleTypeRef.classSymbol.companionModule.termRef), elemValues)
tree1.withSpan(tree.span)
else
cpy.Tuple(tree)(elemValues)
var names = elems.collect:
case NamedArg(name, arg) => name
if names.isEmpty then
val pt1 = pt.stripTypeVar match
case p: TypeParamRef => ctx.typerState.constraint.entry(p).hiBound
case _ => NoType
names = pt1.orElse(pt).namedTupleNames
if names.isEmpty || ctx.mode.is(Mode.Pattern) then
tup
else
def namesTuple = inMode(ctx.mode &~ Mode.Pattern | Mode.Type):
tuple(Tuple(
names.map: name =>
SingletonTypeTree(Literal(Constant(name.toString))).withSpan(tree.span)),
WildcardType)
if ctx.mode.is(Mode.Type) then
AppliedTypeTree(ref(defn.NamedTupleTypeRef), namesTuple :: tup :: Nil)
else
TypeApply(
Apply(Select(ref(defn.NamedTupleModule), nme.withNames), tup),
namesTuple :: Nil)

/** When desugaring a list pattern arguments `elems` adapt them and the
* 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, and `pt` is a named tuple, drop all
* tuple element names from `pt`.
*/
def adaptPatternArgs(elems: List[Tree], pt: Type)(using Context): List[Tree] =

def reorderedNamedArgs(wildcardSpan: Span): List[untpd.Tree] =
var selNames = pt.namedTupleElementTypes.map(_(0))
if selNames.isEmpty && pt.classSymbol.is(CaseClass) then
selNames = pt.classSymbol.caseAccessors.map(_.name.asTermName)
val nameToIdx = selNames.zipWithIndex.toMap
val reordered = Array.fill[untpd.Tree](selNames.length):
untpd.Ident(nme.WILDCARD).withSpan(wildcardSpan)
for case arg @ NamedArg(name: TermName, _) <- 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

elems match
case (first @ NamedArg(_, _)) :: _ => reorderedNamedArgs(first.span.startPos)
case _ => elems
end adaptPatternArgs

private def isTopLevelDef(stat: Tree)(using Context): Boolean = stat match
case _: ValDef | _: PatDef | _: DefDef | _: Export | _: ExtMethods => true
Expand Down Expand Up @@ -1989,7 +2073,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
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] =>
def hasNamedArg(args: List[Any]): Boolean = args exists isNamedArg
val isNamedArg: Any => Boolean = (arg: Any) => arg.isInstanceOf[Trees.NamedArg[?]]

def dropNamedArg(arg: Tree) = arg match
case NamedArg(_, arg1) => arg1
case arg => arg

/** Is this pattern node a catch-all (wildcard or variable) pattern? */
def isDefaultCase(cdef: CaseDef): Boolean = cdef match {
case CaseDef(pat, EmptyTree, _) => isWildcardArg(pat)
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/Trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,8 @@ object Trees {
case class NamedArg[+T <: Untyped] private[ast] (name: Name, arg: Tree[T])(implicit @constructorOnly src: SourceFile)
extends Tree[T] {
type ThisTree[+T <: Untyped] = NamedArg[T]
override def isTerm = arg.isTerm
override def isType = arg.isType
}

/** name = arg, outside a parameter list */
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 @@ -529,15 +529,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 @@ -32,6 +32,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
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ object Contexts {
inline def atPhaseNoEarlier[T](limit: Phase)(inline op: Context ?=> T)(using Context): T =
op(using if !limit.exists || limit <= ctx.phase then ctx else ctx.withPhase(limit))

inline private def inMode[T](mode: Mode)(inline op: Context ?=> T)(using ctx: Context): T =
inline def inMode[T](mode: Mode)(inline op: Context ?=> T)(using ctx: Context): T =
op(using if mode != ctx.mode then ctx.fresh.setMode(mode) else ctx)

inline def withMode[T](mode: Mode)(inline op: Context ?=> T)(using ctx: Context): T =
Expand Down
17 changes: 16 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,9 @@ 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")
@tu lazy val NamedTupleTypeRef: TypeRef = NamedTupleModule.termRef.select(tpnme.NamedTuple).asInstanceOf

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

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

object NamedTuple:
def apply(nmes: Type, vals: Type)(using Context): Type =
AppliedType(NamedTupleTypeRef, nmes :: vals :: Nil)
def unapply(t: Type)(using Context): Option[(Type, Type)] = t match
case AppliedType(tycon, nmes :: vals :: Nil) if tycon.typeSymbol == NamedTupleTypeRef.symbol =>
Some((nmes, vals))
case _ => None

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

final def isNamedTuple_From(sym: Symbol)(using Context): Boolean =
sym.name == tpnme.From && sym.owner == NamedTupleModule.moduleClass

private val compiletimePackageAnyTypes: Set[Name] = Set(
tpnme.Equals, tpnme.NotEquals, tpnme.IsConst, tpnme.ToString
)
Expand Down Expand Up @@ -1341,7 +1355,7 @@ class Definitions {
tpnme.Plus, tpnme.Length, tpnme.Substring, tpnme.Matches, tpnme.CharAt
)
private val compiletimePackageOpTypes: Set[Name] =
Set(tpnme.S)
Set(tpnme.S, tpnme.From)
++ compiletimePackageAnyTypes
++ compiletimePackageIntTypes
++ compiletimePackageLongTypes
Expand All @@ -1354,6 +1368,7 @@ class Definitions {
compiletimePackageOpTypes.contains(sym.name)
&& (
isCompiletime_S(sym)
|| isNamedTuple_From(sym)
|| sym.owner == CompiletimeOpsAnyModuleClass && compiletimePackageAnyTypes.contains(sym.name)
|| sym.owner == CompiletimeOpsIntModuleClass && compiletimePackageIntTypes.contains(sym.name)
|| sym.owner == CompiletimeOpsLongModuleClass && compiletimePackageLongTypes.contains(sym.name)
Expand Down
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@ object StdNames {
val EnumValue: N = "EnumValue"
val ExistentialTypeTree: N = "ExistentialTypeTree"
val Flag : N = "Flag"
val Fields: N = "Fields"
val From: N = "From"
val Ident: N = "Ident"
val Import: N = "Import"
val Literal: N = "Literal"
Expand All @@ -374,6 +376,7 @@ object StdNames {
val MirroredMonoType: N = "MirroredMonoType"
val MirroredType: N = "MirroredType"
val Modifiers: N = "Modifiers"
val NamedTuple: N = "NamedTuple"
val NestedAnnotArg: N = "NestedAnnotArg"
val NoFlags: N = "NoFlags"
val NoPrefix: N = "NoPrefix"
Expand Down Expand Up @@ -649,6 +652,7 @@ object StdNames {
val wildcardType: N = "wildcardType"
val withFilter: N = "withFilter"
val withFilterIfRefutable: N = "withFilterIfRefutable$"
val withNames: N = "withNames"
val WorksheetWrapper: N = "WorksheetWrapper"
val wrap: N = "wrap"
val writeReplace: N = "writeReplace"
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2356,7 +2356,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
* @param canConstrain If true, new constraints might be added to simplify the lub.
* @param isSoft If the lub is a union, this determines whether it's a soft union.
*/
def lub(tp1: Type, tp2: Type, canConstrain: Boolean = false, isSoft: Boolean = true): Type = /*>|>*/ trace(s"lub(${tp1.show}, ${tp2.show}, canConstrain=$canConstrain, isSoft=$isSoft)", subtyping, show = true) /*<|<*/ {
def lub(tp1: Type, tp2: Type, canConstrain: Boolean = false, isSoft: Boolean = true): Type = trace(s"lub(${tp1.show}, ${tp2.show}, canConstrain=$canConstrain, isSoft=$isSoft)", subtyping, show = true) {
if (tp1 eq tp2) tp1
else if !tp1.exists || (tp2 eq WildcardType) then tp1
else if !tp2.exists || (tp1 eq WildcardType) then tp2
Expand Down
28 changes: 25 additions & 3 deletions compiler/src/dotty/tools/dotc/core/TypeEval.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import Types.*, Contexts.*, Symbols.*, Constants.*, Decorators.*
import config.Printers.typr
import reporting.trace
import StdNames.tpnme
import Flags.CaseClass
import TypeOps.nestedPairs

object TypeEval:

def tryCompiletimeConstantFold(tp: AppliedType)(using Context): Type = tp.tycon match
case tycon: TypeRef if defn.isCompiletimeAppliedType(tycon.symbol) =>

extension (tp: Type) def fixForEvaluation: Type =
tp.normalized.dealias match
// enable operations for constant singleton terms. E.g.:
Expand Down Expand Up @@ -94,6 +97,22 @@ object TypeEval:
throw TypeError(em"${e.getMessage.nn}")
ConstantType(Constant(result))

def fieldsOf: Option[Type] =
expectArgsNum(1)
val arg = tp.args.head
val cls = arg.classSymbol
if cls.is(CaseClass) then
val fields = cls.caseAccessors
val fieldLabels = fields.map: field =>
ConstantType(Constant(field.name.toString))
val fieldTypes = fields.map(arg.memberInfo)
Some:
defn.NamedTupleTypeRef.appliedTo:
nestedPairs(fieldLabels) :: nestedPairs(fieldTypes) :: Nil
else arg.widenDealias match
case arg @ defn.NamedTuple(_, _) => Some(arg)
case _ => None

def constantFold1[T](extractor: Type => Option[T], op: T => Any): Option[Type] =
expectArgsNum(1)
extractor(tp.args.head).map(a => runConstantOp(op(a)))
Expand Down Expand Up @@ -122,11 +141,14 @@ object TypeEval:
yield runConstantOp(op(a, b, c))

trace(i"compiletime constant fold $tp", typr, show = true) {
val name = tycon.symbol.name
val owner = tycon.symbol.owner
val sym = tycon.symbol
val name = sym.name
val owner = sym.owner
val constantType =
if defn.isCompiletime_S(tycon.symbol) then
if defn.isCompiletime_S(sym) then
constantFold1(natValue, _ + 1)
else if defn.isNamedTuple_From(sym) then
fieldsOf
else if owner == defn.CompiletimeOpsAnyModuleClass then name match
case tpnme.Equals => constantFold2(constValue, _ == _)
case tpnme.NotEquals => constantFold2(constValue, _ != _)
Expand Down
7 changes: 6 additions & 1 deletion compiler/src/dotty/tools/dotc/core/TypeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,12 @@ object TypeOps:
(tp.tp1.dealias, tp.tp2.dealias) match
case (tp1 @ AppliedType(tycon1, args1), tp2 @ AppliedType(tycon2, args2))
if tycon1.typeSymbol == tycon2.typeSymbol && (tycon1 =:= tycon2) =>
mergeRefinedOrApplied(tp1, tp2)
mergeRefinedOrApplied(tp1, tp2) match
case tp: AppliedType if tp.isUnreducibleWild =>
// fall back to or-dominators rather tahn inferring a type that would
// caue an unreducible type error later.
approximateOr(tp1, tp2)
case tp => tp
case (tp1, tp2) =>
approximateOr(tp1, tp2)
case _ =>
Expand Down
Loading