From 8514b17b5aa62d3931ff62dcc27f039fb7279ca9 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 23 Nov 2023 16:20:45 +0100 Subject: [PATCH 01/15] First implementation of named tuples --- .../src/dotty/tools/dotc/ast/Desugar.scala | 40 ++++-- compiler/src/dotty/tools/dotc/ast/untpd.scala | 8 +- .../dotty/tools/dotc/core/Definitions.scala | 19 +++ .../dotty/tools/dotc/parsing/Parsers.scala | 115 ++++++++------- .../tools/dotc/printing/PlainPrinter.scala | 13 +- .../tools/dotc/printing/RefinedPrinter.scala | 7 +- .../src/dotty/tools/dotc/typer/Typer.scala | 135 +++++++++--------- docs/_docs/reference/syntax.md | 14 +- library/src/scala/NamedTuple.scala | 13 ++ library/src/scala/Tuple.scala | 11 ++ library/src/scala/runtime/LazyVals.scala | 2 +- tests/neg/depfuns.scala | 4 +- tests/neg/i7247.scala | 2 - tests/neg/named-tuples.check | 28 ++++ tests/neg/named-tuples.scala | 16 +++ tests/pos/i7247.scala | 2 + tests/pos/named-tuples-strawman.scala | 48 +++++++ tests/pos/named-tuples.check | 5 + tests/pos/named-tuples.scala | 18 +++ tests/run/named-tuples.check | 6 + tests/run/named-tuples.scala | 43 ++++++ 21 files changed, 400 insertions(+), 149 deletions(-) create mode 100644 library/src/scala/NamedTuple.scala delete mode 100644 tests/neg/i7247.scala create mode 100644 tests/neg/named-tuples.check create mode 100644 tests/neg/named-tuples.scala create mode 100644 tests/pos/i7247.scala create mode 100644 tests/pos/named-tuples-strawman.scala create mode 100644 tests/pos/named-tuples.check create mode 100644 tests/pos/named-tuples.scala create mode 100644 tests/run/named-tuples.check create mode 100644 tests/run/named-tuples.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index a08d6da650c9..b7fc163d3fd4 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1451,16 +1451,36 @@ object desugar { * (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)(using Context): Tree = + val elems = tree.trees.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) + else + cpy.Tuple(tree)(elems) + + private def desugarTupleElem(elem: untpd.Tree)(using Context): untpd.Tree = elem match + case NamedArg(name, arg) => + val nameLit = untpd.Literal(Constant(name.toString)) + if ctx.mode.is(Mode.Type) then + untpd.AppliedTypeTree(untpd.ref(defn.Tuple_NamedValueType), + untpd.SingletonTypeTree(nameLit) :: arg :: Nil) + else if ctx.mode.is(Mode.Pattern) then + untpd.Apply( + untpd.Block(Nil, + untpd.TypeApply(untpd.ref(defn.Tuple_NamedValue_extract), + untpd.SingletonTypeTree(nameLit) :: Nil)), + arg :: Nil) + else + untpd.Apply(untpd.ref(defn.Tuple_NamedValue_apply), nameLit :: arg :: Nil) + case _ => + elem private def isTopLevelDef(stat: Tree)(using Context): Boolean = stat match case _: ValDef | _: PatDef | _: DefDef | _: Export | _: ExtMethods => true diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 817ff5c6c9fa..81bb24e2064b 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -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) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index c500953f49bf..befb6e1ca301 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -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 @@ -937,6 +938,16 @@ 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 TupleNamedValueModule: Symbol = requiredModule("scala.Tuple.NamedValue") + @tu lazy val Tuple_NamedValue_apply: Symbol = TupleNamedValueModule.requiredMethod("apply") + @tu lazy val Tuple_NamedValue_extract: Symbol = TupleNamedValueModule.requiredMethod("extract") + + def Tuple_NamedValueType: TypeRef = TupleModule.termRef.select("NamedValue".toTypeName).asInstanceOf + // Note: It would be dangerous to expose NamedValue as a symbol, since + // NamedValue.typeRef gives the internal view of NamedValue inside Tuple + // 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 EmptyTupleClass: Symbol = requiredClass("scala.EmptyTuple") @tu lazy val EmptyTupleModule: Symbol = requiredModule("scala.EmptyTuple") @tu lazy val NonEmptyTupleTypeRef: TypeRef = requiredClassRef("scala.NonEmptyTuple") @@ -1302,6 +1313,14 @@ class Definitions { case ByNameFunction(_) => true case _ => false + object NamedTupleElem: + def apply(name: Name, tp: Type)(using Context): Type = + AppliedType(Tuple_NamedValueType, 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 == Tuple_NamedValueType.typeSymbol => Some((s.toTermName, tp)) + case _ => None + final def isCompiletime_S(sym: Symbol)(using Context): Boolean = sym.name == tpnme.S && sym.owner == CompiletimeOpsIntModuleClass diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 306656b137c8..ba74ed2528e6 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -634,6 +634,14 @@ object Parsers { ts.toList else leading :: Nil + def maybeNamed(op: () => Tree): () => Tree = () => + if isIdent && in.lookahead.token == EQUALS then + atSpan(in.offset): + val name = ident() + in.nextToken() + NamedArg(name, op()) + else op() + def inSepRegion[T](f: Region => Region)(op: => T): T = val cur = in.currentRegion in.currentRegion = f(cur) @@ -1528,6 +1536,7 @@ object Parsers { val start = in.offset var imods = Modifiers() var erasedArgs: ListBuffer[Boolean] = ListBuffer() + def functionRest(params: List[Tree]): Tree = val paramSpan = Span(start, in.lastOffset) atSpan(start, in.offset) { @@ -1574,61 +1583,51 @@ object Parsers { Function(params, resultType) } - var isValParamList = false + def convertToElem(t: Tree): Tree = t match + case ByNameTypeTree(t1) => + syntaxError(ByNameParameterNotSupported(t), t.span) + t1 + case ValDef(name, tpt, _) => NamedArg(name, convertToElem(tpt)) + case _ => t val t = - if (in.token == LPAREN) { + if in.token == LPAREN then in.nextToken() - if (in.token == RPAREN) { + if in.token == RPAREN then in.nextToken() functionRest(Nil) - } - else { + else val paramStart = in.offset def addErased() = - erasedArgs.addOne(isErasedKw) - if isErasedKw then { in.skipToken(); } + erasedArgs += isErasedKw + if isErasedKw then in.nextToken() addErased() - val ts = in.currentRegion.withCommasExpected { + var ts = in.currentRegion.withCommasExpected: funArgType() match case Ident(name) if name != tpnme.WILDCARD && in.isColon => - isValParamList = true - def funParam(start: Offset, mods: Modifiers) = { - atSpan(start) { + def funParam(start: Offset, mods: Modifiers) = + atSpan(start): addErased() typedFunParam(in.offset, ident(), imods) - } - } commaSeparatedRest( typedFunParam(paramStart, name.toTermName, imods), () => funParam(in.offset, imods)) case t => - def funParam() = { - addErased() - funArgType() - } + def funParam() = + addErased() + funArgType() commaSeparatedRest(t, funParam) - } accept(RPAREN) - if isValParamList || in.isArrow || isPureArrow then + if in.isArrow || isPureArrow || erasedArgs.contains(true) then functionRest(ts) - else { - val ts1 = ts.mapConserve { t => - if isByNameType(t) then - syntaxError(ByNameParameterNotSupported(t), t.span) - stripByNameType(t) - else - t - } - val tuple = atSpan(start) { makeTupleOrParens(ts1) } + else + val tuple = atSpan(start): + makeTupleOrParens(ts.mapConserve(convertToElem)) infixTypeRest( refinedTypeRest( withTypeRest( annotTypeRest( simpleTypeRest(tuple))))) - } - } - } else if (in.token == LBRACKET) { val start = in.offset val tparams = typeParamClause(ParamOwner.TypeParam) @@ -1913,6 +1912,7 @@ object Parsers { * | Singleton `.' id * | Singleton `.' type * | ‘(’ ArgTypes ‘)’ + * | ‘(’ NamesAndTypes ‘)’ * | Refinement * | TypeSplice -- deprecated syntax (since 3.0.0) * | SimpleType1 TypeArgs @@ -1921,7 +1921,7 @@ object Parsers { def simpleType1() = simpleTypeRest { if in.token == LPAREN then atSpan(in.offset) { - makeTupleOrParens(inParensWithCommas(argTypes(namedOK = false, wildOK = true))) + makeTupleOrParens(inParensWithCommas(argTypes(namedOK = false, wildOK = true, tupleOK = true))) } else if in.token == LBRACE then atSpan(in.offset) { RefinedTypeTree(EmptyTree, refinement(indentOK = false)) } @@ -2004,32 +2004,31 @@ object Parsers { /** ArgTypes ::= Type {`,' Type} * | NamedTypeArg {`,' NamedTypeArg} * NamedTypeArg ::= id `=' Type + * NamesAndTypes ::= NameAndType {‘,’ NameAndType} + * NameAndType ::= id ':' Type */ - def argTypes(namedOK: Boolean, wildOK: Boolean): List[Tree] = { - - def argType() = { + def argTypes(namedOK: Boolean, wildOK: Boolean, tupleOK: Boolean): List[Tree] = + def argType() = val t = typ() - if (wildOK) t else rejectWildcardType(t) - } + if wildOK then t else rejectWildcardType(t) - def namedTypeArg() = { + def namedArgType() = val name = ident() accept(EQUALS) NamedArg(name.toTypeName, argType()) - } - if (namedOK && in.token == IDENTIFIER) - in.currentRegion.withCommasExpected { - argType() match { - case Ident(name) if in.token == EQUALS => - in.nextToken() - commaSeparatedRest(NamedArg(name, argType()), () => namedTypeArg()) - case firstArg => - commaSeparatedRest(firstArg, () => argType()) - } - } - else commaSeparated(() => argType()) - } + def namedElem() = + val name = ident() + acceptColon() + NamedArg(name, argType()) + + if namedOK && isIdent && in.lookahead.token == EQUALS then + commaSeparated(() => namedArgType()) + else if tupleOK && isIdent && in.lookahead.isColon then + commaSeparated(() => namedElem()) + else + commaSeparated(() => argType()) + end argTypes def paramTypeOf(core: () => Tree): Tree = if in.token == ARROW || isPureArrow(nme.PUREARROW) then @@ -2075,7 +2074,7 @@ object Parsers { * NamedTypeArgs ::= `[' NamedTypeArg {`,' NamedTypeArg} `]' */ def typeArgs(namedOK: Boolean, wildOK: Boolean): List[Tree] = - inBracketsWithCommas(argTypes(namedOK, wildOK)) + inBracketsWithCommas(argTypes(namedOK, wildOK, tupleOK = false)) /** Refinement ::= `{' RefineStatSeq `}' */ @@ -2651,7 +2650,9 @@ object Parsers { } /** ExprsInParens ::= ExprInParens {`,' ExprInParens} + * | NamedExprInParens {‘,’ NamedExprInParens} * Bindings ::= Binding {`,' Binding} + * NamedExprInParens ::= id '=' ExprInParens */ def exprsInParensOrBindings(): List[Tree] = if in.token == RPAREN then Nil @@ -2661,7 +2662,7 @@ object Parsers { if isErasedKw then isFormalParams = true if isFormalParams then binding(Modifiers()) else - val t = exprInParens() + val t = maybeNamed(exprInParens)() if t.isInstanceOf[ValDef] then isFormalParams = true t commaSeparatedRest(exprOrBinding(), exprOrBinding) @@ -3017,7 +3018,7 @@ object Parsers { * | Literal * | Quoted * | XmlPattern - * | `(' [Patterns] `)' + * | `(' [Patterns | NamedPatterns] `)' * | SimplePattern1 [TypeArgs] [ArgumentPatterns] * | ‘given’ RefinedType * SimplePattern1 ::= SimpleRef @@ -3068,9 +3069,13 @@ object Parsers { p /** Patterns ::= Pattern [`,' Pattern] + * NamedPatterns ::= NamedPattern {‘,’ NamedPattern} + * NamedPattern ::= id '=' Pattern */ def patterns(location: Location = Location.InPattern): List[Tree] = - commaSeparated(() => pattern(location)) + val pat = () => pattern(location) + commaSeparated( // TODO: Drop the distinction once we allow named argument patterns + if location == Location.InPattern then maybeNamed(pat) else pat) def patternsOpt(location: Location = Location.InPattern): List[Tree] = if (in.token == RPAREN) Nil else patterns(location) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 7fed5bc97f35..c2a30567ec8e 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -68,7 +68,8 @@ class PlainPrinter(_ctx: Context) extends Printer { homogenize(tp.ref) case tp @ AppliedType(tycon, args) => if (defn.isCompiletimeAppliedType(tycon.typeSymbol)) tp.tryCompiletimeConstantFold - else tycon.dealias.appliedTo(args) + else if !tycon.typeSymbol.isOpaqueAlias then tycon.dealias.appliedTo(args) + else tp case tp: NamedType => tp.reduceProjection case _ => @@ -120,10 +121,12 @@ class PlainPrinter(_ctx: Context) extends Printer { } (keyword ~ refinementNameString(rt) ~ toTextRHS(rt.refinedInfo)).close - protected def argText(arg: Type, isErased: Boolean = false): Text = keywordText("erased ").provided(isErased) ~ (homogenizeArg(arg) match { - case arg: TypeBounds => "?" ~ toText(arg) - case arg => toText(arg) - }) + protected def argText(arg: Type, isErased: Boolean = false): Text = + keywordText("erased ").provided(isErased) + ~ homogenizeArg(arg).match + case arg: TypeBounds => "?" ~ toText(arg) + case defn.NamedTupleElem(name, arg) => toText(name) ~ ": " ~ argText(arg, isErased) + case arg => toText(arg) /** Pretty-print comma-separated type arguments for a constructor to be inserted among parentheses or brackets * (hence with `GlobalPrec` precedence). diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 8ad1188a3e7e..1b9009c732b3 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -234,7 +234,10 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { def appliedText(tp: Type): Text = tp match case tp @ AppliedType(tycon, args) => tp.tupleElementTypesUpTo(200, normalize = false) match - case Some(types) if types.size >= 2 && !printDebug => toTextTuple(types) + case Some(types @ (defn.NamedTupleElem(_, _) :: _)) if !printDebug => + toTextTuple(types) + case Some(types) if types.size >= 2 && !printDebug => + toTextTuple(types) case _ => val tsym = tycon.typeSymbol if tycon.isRepeatedParam then toTextLocal(args.head) ~ "*" @@ -485,7 +488,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { exprText ~ colon ~ toText(tpt) } case NamedArg(name, arg) => - toText(name) ~ " = " ~ toText(arg) + toText(name) ~ (if name.isTermName && arg.isType then " : " else " = ") ~ toText(arg) case Assign(lhs, rhs) => changePrec(GlobalPrec) { toTextLocal(lhs) ~ " = " ~ toText(rhs) } case block: Block => diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 194d84d0dd0d..ce53b2208201 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -705,54 +705,64 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // There's a second trial where we try to instantiate all type variables in `qual.tpe.widen`, // but that is done only after we search for extension methods or conversions. typedSelect(tree, pt, qual) - else if qual.tpe.isSmallGenericTuple then - val elems = qual.tpe.widenTermRefExpr.tupleElementTypes.getOrElse(Nil) - typedSelect(tree, pt, qual.cast(defn.tupleType(elems))) else - val tree1 = tryExtensionOrConversion( - tree, pt, IgnoredProto(pt), qual, ctx.typerState.ownedVars, this, inSelect = true) - .orElse { - if ctx.gadt.isNarrowing then - // try GADT approximation if we're trying to select a member - // Member lookup cannot take GADTs into account b/c of cache, so we - // approximate types based on GADT constraints instead. For an example, - // see MemberHealing in gadt-approximation-interaction.scala. - val wtp = qual.tpe.widen - gadts.println(i"Trying to heal member selection by GADT-approximating $wtp") - val gadtApprox = Inferencing.approximateGADT(wtp) - gadts.println(i"GADT-approximated $wtp ~~ $gadtApprox") - val qual1 = qual.cast(gadtApprox) - val tree1 = cpy.Select(tree0)(qual1, selName) - val checkedType1 = accessibleType(selectionType(tree1, qual1), superAccess = false) - if checkedType1.exists then - gadts.println(i"Member selection healed by GADT approximation") - finish(tree1, qual1, checkedType1) - else if qual1.tpe.isSmallGenericTuple then - gadts.println(i"Tuple member selection healed by GADT approximation") - typedSelect(tree, pt, qual1) - else - tryExtensionOrConversion(tree1, pt, IgnoredProto(pt), qual1, ctx.typerState.ownedVars, this, inSelect = true) - else EmptyTree - } - if !tree1.isEmpty then - tree1 - else if canDefineFurther(qual.tpe.widen) then - typedSelect(tree, pt, qual) - else if qual.tpe.derivesFrom(defn.DynamicClass) - && selName.isTermName && !isDynamicExpansion(tree) - then - val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName) - if pt.isInstanceOf[FunOrPolyProto] || pt == LhsProto then - assignType(tree2, TryDynamicCallType) - else - typedDynamicSelect(tree2, Nil, pt) + val tupleElems = qual.tpe.widenTermRefExpr.tupleElementTypes.getOrElse(Nil) + val nameIdx = tupleElems.indexWhere: + case defn.NamedTupleElem(name, _) => name == selName + case _ => false + if nameIdx >= 0 then + typed( + untpd.Apply( + untpd.Select(untpd.TypedSplice(qual), nme.apply), + untpd.Literal(Constant(nameIdx))), + pt) + else if qual.tpe.isSmallGenericTuple then + typedSelect(tree, pt, qual.cast(defn.tupleType(tupleElems))) else - assignType(tree, - rawType match - case rawType: NamedType => - inaccessibleErrorType(rawType, superAccess, tree.srcPos) - case _ => - notAMemberErrorType(tree, qual, pt)) + val tree1 = tryExtensionOrConversion( + tree, pt, IgnoredProto(pt), qual, ctx.typerState.ownedVars, this, inSelect = true) + .orElse { + if ctx.gadt.isNarrowing then + // try GADT approximation if we're trying to select a member + // Member lookup cannot take GADTs into account b/c of cache, so we + // approximate types based on GADT constraints instead. For an example, + // see MemberHealing in gadt-approximation-interaction.scala. + val wtp = qual.tpe.widen + gadts.println(i"Trying to heal member selection by GADT-approximating $wtp") + val gadtApprox = Inferencing.approximateGADT(wtp) + gadts.println(i"GADT-approximated $wtp ~~ $gadtApprox") + val qual1 = qual.cast(gadtApprox) + val tree1 = cpy.Select(tree0)(qual1, selName) + val checkedType1 = accessibleType(selectionType(tree1, qual1), superAccess = false) + if checkedType1.exists then + gadts.println(i"Member selection healed by GADT approximation") + finish(tree1, qual1, checkedType1) + else if qual1.tpe.isSmallGenericTuple then + gadts.println(i"Tuple member selection healed by GADT approximation") + typedSelect(tree, pt, qual1) + else + tryExtensionOrConversion(tree1, pt, IgnoredProto(pt), qual1, ctx.typerState.ownedVars, this, inSelect = true) + else EmptyTree + } + if !tree1.isEmpty then + tree1 + else if canDefineFurther(qual.tpe.widen) then + typedSelect(tree, pt, qual) + else if qual.tpe.derivesFrom(defn.DynamicClass) + && selName.isTermName && !isDynamicExpansion(tree) + then + val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName) + if pt.isInstanceOf[FunOrPolyProto] || pt == LhsProto then + assignType(tree2, TryDynamicCallType) + else + typedDynamicSelect(tree2, Nil, pt) + else + assignType(tree, + rawType match + case rawType: NamedType => + inaccessibleErrorType(rawType, superAccess, tree.srcPos) + case _ => + notAMemberErrorType(tree, qual, pt)) end typedSelect def typedSelect(tree: untpd.Select, pt: Type)(using Context): Tree = { @@ -3057,37 +3067,32 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } /** Translate tuples of all arities */ - def typedTuple(tree: untpd.Tuple, pt: Type)(using Context): Tree = { - val arity = tree.trees.length - if (arity <= Definitions.MaxTupleArity) - typed(desugar.smallTuple(tree).withSpan(tree.span), pt) - else { - val pts = - pt.tupleElementTypes match - case Some(types) if types.size == arity => types - case _ => List.fill(arity)(defn.AnyType) - val elems = tree.trees.lazyZip(pts).map( + def typedTuple(tree: untpd.Tuple, pt: Type)(using Context): Tree = + val tree1 = desugar.tuple(tree) + if tree1 ne tree then typed(tree1, pt) + else + val arity = tree.trees.length + val pts = pt.tupleElementTypes match + case Some(types) if types.size == arity => types + case _ => List.fill(arity)(defn.AnyType) + val elems = tree.trees.lazyZip(pts).map: if ctx.mode.is(Mode.Type) then typedType(_, _, mapPatternBounds = true) - else typed(_, _)) - if (ctx.mode.is(Mode.Type)) + else typed(_, _) + if ctx.mode.is(Mode.Type) then elems.foldRight(TypeTree(defn.EmptyTupleModule.termRef): Tree)((elemTpt, elemTpts) => AppliedTypeTree(TypeTree(defn.PairClass.typeRef), List(elemTpt, elemTpts))) .withSpan(tree.span) - else { + else val tupleXXLobj = untpd.ref(defn.TupleXXLModule.termRef) val app = untpd.cpy.Apply(tree)(tupleXXLobj, elems.map(untpd.TypedSplice(_))) .withSpan(tree.span) val app1 = typed(app, if ctx.mode.is(Mode.Pattern) then pt else defn.TupleXXLClass.typeRef) - if (ctx.mode.is(Mode.Pattern)) app1 - else { + if ctx.mode.is(Mode.Pattern) then app1 + else val elemTpes = elems.lazyZip(pts).map((elem, pt) => TypeComparer.widenInferred(elem.tpe, pt, widenUnions = true)) val resTpe = TypeOps.nestedPairs(elemTpes) app1.cast(resTpe) - } - } - } - } /** Retrieve symbol attached to given tree */ protected def retrieveSym(tree: untpd.Tree)(using Context): Symbol = tree.removeAttachment(SymOfTree) match { diff --git a/docs/_docs/reference/syntax.md b/docs/_docs/reference/syntax.md index bf2c27d57863..cebee26c34e6 100644 --- a/docs/_docs/reference/syntax.md +++ b/docs/_docs/reference/syntax.md @@ -198,7 +198,7 @@ SimpleType ::= SimpleLiteral | id | Singleton ‘.’ id | Singleton ‘.’ ‘type’ - | ‘(’ Types ‘)’ + | ‘(’ Types | NamesAndTypes ‘)’ | Refinement | SimpleType1 TypeArgs | SimpleType1 ‘#’ id @@ -216,6 +216,8 @@ Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> TypeBounds ::= [‘>:’ Type] [‘<:’ Type] TypeParamBounds ::= TypeBounds {‘:’ Type} Types ::= Type {‘,’ Type} +NamesAndTypes ::= NameAndType {‘,’ NameAndType} +NameAndType ::= id ':' Type ``` ### Expressions @@ -263,7 +265,7 @@ SimpleExpr ::= SimpleRef | quoteId -- only inside splices | ‘new’ ConstrApp {‘with’ ConstrApp} [TemplateBody] | ‘new’ TemplateBody - | ‘(’ ExprsInParens ‘)’ + | ‘(’ [ExprsInParens] ‘)’ | SimpleExpr ‘.’ id | SimpleExpr ‘.’ MatchClause | SimpleExpr TypeArgs @@ -279,8 +281,9 @@ ExprSplice ::= spliceId | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern ExprsInParens ::= ExprInParens {‘,’ ExprInParens} -ExprInParens ::= PostfixExpr ‘:’ Type - | Expr + | NamedExprInParens {‘,’ NamedExprInParens} +ExprInParens ::= (PostfixExpr ‘:’ Type | Expr) +NamedExprInParens ::= id '=' ExprInParens ParArgumentExprs ::= ‘(’ [ExprsInParens] ‘)’ | ‘(’ ‘using’ ExprsInParens ‘)’ | ‘(’ [ExprsInParens ‘,’] PostfixExpr ‘*’ ‘)’ @@ -331,6 +334,9 @@ SimplePattern1 ::= SimpleRef PatVar ::= varid | ‘_’ Patterns ::= Pattern {‘,’ Pattern} +NamedPatterns ::= NamedPattern {‘,’ NamedPattern} +NamedPattern ::= id '=' Pattern + ArgumentPatterns ::= ‘(’ [Patterns] ‘)’ | ‘(’ [Patterns ‘,’] PatVar ‘*’ ‘)’ ``` diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala new file mode 100644 index 000000000000..2f76138a9988 --- /dev/null +++ b/library/src/scala/NamedTuple.scala @@ -0,0 +1,13 @@ +package scala + +import annotation.experimental + +@experimental +object NamedTuple: + + type DropNames[T <: Tuple] = T match + case Tuple.NamedValue[_, x] *: xs => x *: DropNames[xs] + case _ => T + + extension [T <: Tuple](x: T) def dropNames: DropNames[T] = + x.asInstanceOf // named and unnamed tuples have the same runtime representation diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 6993b8202082..adcff517ca5c 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -241,6 +241,17 @@ object Tuple { */ type Union[T <: Tuple] = Fold[T, Nothing, [x, y] =>> x | y] + opaque type NamedValue[name <: String & Singleton, A] >: A = A + + object NamedValue: + def apply[S <: String & Singleton, A](name: S, x: A): NamedValue[name.type, A] = x + def extract[S <: String & Singleton]: NameExtractor[S] = NameExtractor[S]() + extension [S <: String & Singleton, A](named: NamedValue[S, A]) def value: A = named + + class NameExtractor[S <: String & Singleton]: + def unapply[A](x: NamedValue[S, A]): Some[A] = Some(x) + end NamedValue + /** Empty tuple */ def apply(): EmptyTuple = EmptyTuple diff --git a/library/src/scala/runtime/LazyVals.scala b/library/src/scala/runtime/LazyVals.scala index ea369539d021..e38e016f5182 100644 --- a/library/src/scala/runtime/LazyVals.scala +++ b/library/src/scala/runtime/LazyVals.scala @@ -9,7 +9,7 @@ import scala.annotation.* */ object LazyVals { @nowarn - private[this] val unsafe: sun.misc.Unsafe = { + private val unsafe: sun.misc.Unsafe = { def throwInitializationException() = throw new ExceptionInInitializerError( new IllegalStateException("Can't find instance of sun.misc.Unsafe") diff --git a/tests/neg/depfuns.scala b/tests/neg/depfuns.scala index ac96915a78b5..989aa72be820 100644 --- a/tests/neg/depfuns.scala +++ b/tests/neg/depfuns.scala @@ -1,5 +1,7 @@ +import language.experimental.erasedDefinitions + object Test { - type T = (x: Int) + type T = (erased x: Int) } // error: `=>' expected diff --git a/tests/neg/i7247.scala b/tests/neg/i7247.scala deleted file mode 100644 index 9172f90fad07..000000000000 --- a/tests/neg/i7247.scala +++ /dev/null @@ -1,2 +0,0 @@ -val x = "foo" match - case _: (a *: (b: Any)) => ??? // error \ No newline at end of file diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check new file mode 100644 index 000000000000..69515689e488 --- /dev/null +++ b/tests/neg/named-tuples.check @@ -0,0 +1,28 @@ +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:12:25 ------------------------------------------------------ +12 | val y: (String, Int) = person // error + | ^^^^^^ + | Found: (person : (name: String, age: Int)) + | Required: (String, Int) + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:13:20 ------------------------------------------------------ +13 | val _: NameOnly = person // error + | ^^^^^^ + | Found: (person : (name: String, age: Int)) + | Required: NameOnly + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:14:18 ------------------------------------------------------ +14 | val _: Person = nameOnly // error + | ^^^^^^^^ + | Found: (nameOnly : (name: String)) + | Required: Person + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:16:36 ------------------------------------------------------ +16 | val _: (age: Int, name: String) = person // error + | ^^^^^^ + | Found: (person : (name: String, age: Int)) + | Required: (age: Int, name: String) + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/named-tuples.scala b/tests/neg/named-tuples.scala new file mode 100644 index 000000000000..19ddb9caeb05 --- /dev/null +++ b/tests/neg/named-tuples.scala @@ -0,0 +1,16 @@ + +type Person = (name: String, age: Int) +val person = (name = "Bob", age = 33): (name: String, age: Int) + +type NameOnly = (name: String) + +val nameOnly = (name = "Louis") + + + +def Test = + val y: (String, Int) = person // error + val _: NameOnly = person // error + val _: Person = nameOnly // error + + val _: (age: Int, name: String) = person // error diff --git a/tests/pos/i7247.scala b/tests/pos/i7247.scala new file mode 100644 index 000000000000..3514f20c47fe --- /dev/null +++ b/tests/pos/i7247.scala @@ -0,0 +1,2 @@ +val x = "foo" match + case _: (a *: (b: Any)) => ??? // error, now OK since (b: Any) is a named tuple \ No newline at end of file diff --git a/tests/pos/named-tuples-strawman.scala b/tests/pos/named-tuples-strawman.scala new file mode 100644 index 000000000000..c207b2541e62 --- /dev/null +++ b/tests/pos/named-tuples-strawman.scala @@ -0,0 +1,48 @@ +object Test: + + object Named: + opaque type Named[name <: String & Singleton, A] >: A = A + def apply[S <: String & Singleton, A](name: S, x: A): Named[name.type, A] = x + extension [name <: String & Singleton, A](named: Named[name, A]) def value: A = named + import Named.* + + type DropNames[T <: Tuple] = T match + case Named[_, x] *: xs => x *: DropNames[xs] + case _ => T + + extension [T <: Tuple](x: T) def dropNames: DropNames[T] = + x.asInstanceOf // named and unnamed tuples have the same runtime representation + + val name = "hi" + val named = Named(name, 33) // ok, but should be rejectd + + inline val name2 = "hi" + val named2 = Named(name2, 33) // ok, but should be rejectd + val _: Named["hi", Int] = named2 + + var x = (Named("name", "Bob"), Named("age", 33)) + + val y: (String, Int) = x.dropNames + + x = y + + val z = y.dropNames + + type PersonInfo = (Named["name", String], Named["age", Int]) + type AddressInfo = (Named["city", String], Named["zip", Int]) + + val ok1: (Named["name", String], Named["age", Int]) = x + val ok2: PersonInfo = y + //val err1: (Named["bad", String], Named["age", Int]) = x // error + val err2: (Named["bad", String], Named["age", Int]) = x.dropNames // ok + val ok3: (Named["bad", String], Named["age", Int]) = y // ok + + val addr = (Named("city", "Lausanne"), Named("zip", 1003)) + val _: AddressInfo = addr + + type CombinedInfo = Tuple.Concat[PersonInfo, AddressInfo] + + val combined: CombinedInfo = x ++ addr + +// val person = (name = "Bob", age = 33): (name: String, age: Int) +// person.age diff --git a/tests/pos/named-tuples.check b/tests/pos/named-tuples.check new file mode 100644 index 000000000000..5458409fd895 --- /dev/null +++ b/tests/pos/named-tuples.check @@ -0,0 +1,5 @@ +(Bob,33) +33 +Bob +33 +no match diff --git a/tests/pos/named-tuples.scala b/tests/pos/named-tuples.scala new file mode 100644 index 000000000000..9d289b709647 --- /dev/null +++ b/tests/pos/named-tuples.scala @@ -0,0 +1,18 @@ +type P = (Int, Int) +type Person = (name: String, age: Int) +val person = (name = "Bob", age = 33): (name: String, age: Int)/* +val person2: (name: String, age: Int) = person + +@main def Test = + println(person) + println(person.age) + println(person2.name) + person match + case p @ (name = "Bob", age = _) => println(p.age) + person match + case p @ (name = "Peter", age = _) => println(p.age) + case p @ (name = "Bob", age = 0) => println(p.age) + case _ => println("no match") + + +*/ \ No newline at end of file diff --git a/tests/run/named-tuples.check b/tests/run/named-tuples.check new file mode 100644 index 000000000000..629414f6530d --- /dev/null +++ b/tests/run/named-tuples.check @@ -0,0 +1,6 @@ +(Bob,33) +33 +Bob +(Bob,33,Lausanne,1003) +33 +no match diff --git a/tests/run/named-tuples.scala b/tests/run/named-tuples.scala new file mode 100644 index 000000000000..71a90f6dfd1a --- /dev/null +++ b/tests/run/named-tuples.scala @@ -0,0 +1,43 @@ +import NamedTuple.dropNames + +type Person = (name: String, age: Int) +val bob = (name = "Bob", age = 33): (name: String, age: Int) +val person2: (name: String, age: Int) = bob + +type Uni = (uni: Double) +val uni = (uni = 1.0) +val _: Uni = uni + +type AddressInfo = (city: String, zip: Int) +val addr = (city = "Lausanne", zip = 1003) +val _: AddressInfo = addr + +type CombinedInfo = Tuple.Concat[Person, AddressInfo] +val bobWithAddr = bob ++ addr +val _: CombinedInfo = bobWithAddr +val _: CombinedInfo = bob ++ addr + +@main def Test = + println(bob) + println(bob.age) + println(person2.name) + println(bobWithAddr) + bob match + case p @ (name = "Bob", age = _) => println(p.age) + bob match + case p @ (name = "Peter", age = _) => println(p.age) + case p @ (name = "Bob", age = 0) => println(p.age) + case _ => println("no match") + + val x = bob.age + assert(x == 33) + + val y: (String, Int) = bob.dropNames + + def ageOf(person: Person) = person.age + + assert(ageOf(bob) == 33) + assert(ageOf((name = "anon", age = 22)) == 22) + assert(ageOf(("anon", 11)) == 11) + + From e0c2669e93a94b293caba287fc18baa439047a9f Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 24 Nov 2023 18:51:44 +0100 Subject: [PATCH 02/15] Make named tuples an experimental feature --- .../src/dotty/tools/dotc/config/Feature.scala | 1 + .../dotty/tools/dotc/parsing/Parsers.scala | 4 +- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- .../test/dotc/pos-test-pickling.blacklist | 1 + library/src/scala/Tuple.scala | 2 + .../runtime/stdLibPatches/language.scala | 7 ++++ tests/{pos => neg}/i7247.scala | 0 tests/neg/named-tuples.check | 28 ++++++------- tests/neg/named-tuples.scala | 13 +++--- tests/pos/named-tuples.check | 5 --- tests/pos/named-tuples.scala | 18 --------- tests/pos/named-tuples1.scala | 14 +++++++ .../stdlibExperimentalDefinitions.scala | 10 ++++- tests/run/named-tuples.check | 3 ++ tests/run/named-tuples.scala | 40 +++++++++++++++++++ 15 files changed, 101 insertions(+), 47 deletions(-) rename tests/{pos => neg}/i7247.scala (100%) delete mode 100644 tests/pos/named-tuples.check delete mode 100644 tests/pos/named-tuples.scala create mode 100644 tests/pos/named-tuples1.scala diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index fa262a5880ff..29b60a6eacf5 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -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) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index ba74ed2528e6..ea8ebfcddee4 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -635,7 +635,7 @@ object Parsers { else leading :: Nil def maybeNamed(op: () => Tree): () => Tree = () => - if isIdent && in.lookahead.token == EQUALS then + if isIdent && in.lookahead.token == EQUALS && in.featureEnabled(Feature.namedTuples) then atSpan(in.offset): val name = ident() in.nextToken() @@ -2024,7 +2024,7 @@ object Parsers { if namedOK && isIdent && in.lookahead.token == EQUALS then commaSeparated(() => namedArgType()) - else if tupleOK && isIdent && in.lookahead.isColon then + else if tupleOK && isIdent && in.lookahead.isColon && in.featureEnabled(Feature.namedTuples) then commaSeparated(() => namedElem()) else commaSeparated(() => argType()) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index ce53b2208201..2ee8d31809e1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -710,7 +710,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val nameIdx = tupleElems.indexWhere: case defn.NamedTupleElem(name, _) => name == selName case _ => false - if nameIdx >= 0 then + if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then typed( untpd.Apply( untpd.Select(untpd.TypedSplice(qual), nme.apply), diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index eb4b861eb324..67ee566de076 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -61,6 +61,7 @@ i17149.scala tuple-fold.scala mt-redux-norm.perspective.scala i18211.scala +named-tuples1.scala # Opaque type i5720.scala diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index adcff517ca5c..95febb272ae9 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -241,8 +241,10 @@ object Tuple { */ type Union[T <: Tuple] = Fold[T, Nothing, [x, y] =>> x | y] + @experimental opaque type NamedValue[name <: String & Singleton, A] >: A = A + @experimental object NamedValue: def apply[S <: String & Singleton, A](name: S, x: A): NamedValue[name.type, A] = x def extract[S <: String & Singleton]: NameExtractor[S] = NameExtractor[S]() diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index c2a12cec2ecc..cf664f8eb468 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -91,6 +91,13 @@ object language: @compileTimeOnly("`into` can only be used at compile time in import statements") object into + /** Experimental support for named tuples. + * + * @see [[https://dotty.epfl.ch/docs/reference/experimental/into-modifier]] + */ + @compileTimeOnly("`namedTuples` can only be used at compile time in import statements") + object namedTuples + /** Was needed to add support for relaxed imports of extension methods. * The language import is no longer needed as this is now a standard feature since SIP was accepted. * @see [[http://dotty.epfl.ch/docs/reference/contextual/extension-methods]] diff --git a/tests/pos/i7247.scala b/tests/neg/i7247.scala similarity index 100% rename from tests/pos/i7247.scala rename to tests/neg/i7247.scala diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check index 69515689e488..9aa9640fb50f 100644 --- a/tests/neg/named-tuples.check +++ b/tests/neg/named-tuples.check @@ -1,28 +1,28 @@ --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:12:25 ------------------------------------------------------ -12 | val y: (String, Int) = person // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:13:25 ------------------------------------------------------ +13 | val y: (String, Int) = person // error | ^^^^^^ - | Found: (person : (name: String, age: Int)) + | Found: (Test.person : (name: String, age: Int)) | Required: (String, Int) | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:13:20 ------------------------------------------------------ -13 | val _: NameOnly = person // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:14:20 ------------------------------------------------------ +14 | val _: NameOnly = person // error | ^^^^^^ - | Found: (person : (name: String, age: Int)) - | Required: NameOnly + | Found: (Test.person : (name: String, age: Int)) + | Required: Test.NameOnly | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:14:18 ------------------------------------------------------ -14 | val _: Person = nameOnly // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:15:18 ------------------------------------------------------ +15 | val _: Person = nameOnly // error | ^^^^^^^^ - | Found: (nameOnly : (name: String)) - | Required: Person + | Found: (Test.nameOnly : (name: String)) + | Required: Test.Person | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:16:36 ------------------------------------------------------ -16 | val _: (age: Int, name: String) = person // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:17:36 ------------------------------------------------------ +17 | val _: (age: Int, name: String) = person // error | ^^^^^^ - | Found: (person : (name: String, age: Int)) + | Found: (Test.person : (name: String, age: Int)) | Required: (age: Int, name: String) | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/named-tuples.scala b/tests/neg/named-tuples.scala index 19ddb9caeb05..05fc8858bc98 100644 --- a/tests/neg/named-tuples.scala +++ b/tests/neg/named-tuples.scala @@ -1,14 +1,15 @@ +import annotation.experimental +import language.experimental.namedTuples -type Person = (name: String, age: Int) -val person = (name = "Bob", age = 33): (name: String, age: Int) +@experimental object Test: -type NameOnly = (name: String) + type Person = (name: String, age: Int) + val person = (name = "Bob", age = 33): (name: String, age: Int) -val nameOnly = (name = "Louis") + type NameOnly = (name: String) + val nameOnly = (name = "Louis") - -def Test = val y: (String, Int) = person // error val _: NameOnly = person // error val _: Person = nameOnly // error diff --git a/tests/pos/named-tuples.check b/tests/pos/named-tuples.check deleted file mode 100644 index 5458409fd895..000000000000 --- a/tests/pos/named-tuples.check +++ /dev/null @@ -1,5 +0,0 @@ -(Bob,33) -33 -Bob -33 -no match diff --git a/tests/pos/named-tuples.scala b/tests/pos/named-tuples.scala deleted file mode 100644 index 9d289b709647..000000000000 --- a/tests/pos/named-tuples.scala +++ /dev/null @@ -1,18 +0,0 @@ -type P = (Int, Int) -type Person = (name: String, age: Int) -val person = (name = "Bob", age = 33): (name: String, age: Int)/* -val person2: (name: String, age: Int) = person - -@main def Test = - println(person) - println(person.age) - println(person2.name) - person match - case p @ (name = "Bob", age = _) => println(p.age) - person match - case p @ (name = "Peter", age = _) => println(p.age) - case p @ (name = "Bob", age = 0) => println(p.age) - case _ => println("no match") - - -*/ \ No newline at end of file diff --git a/tests/pos/named-tuples1.scala b/tests/pos/named-tuples1.scala new file mode 100644 index 000000000000..f0b7c8f70dce --- /dev/null +++ b/tests/pos/named-tuples1.scala @@ -0,0 +1,14 @@ +import annotation.experimental +import language.experimental.namedTuples +import NamedTuple.dropNames + +@main def Test = + val bob = (name = "Bob", age = 33): (name: String, age: Int) + val persons = List( + bob, + (name = "Bill", age = 40), + (name = "Lucy", age = 45) + ) + val ages = persons.map(_.age) + // pickling failure: matchtype is reduced after pickling, unreduced before. + assert(ages.sum == 118) diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index a01c71724b0e..28788c24958e 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -97,7 +97,15 @@ val experimentalDefinitionInLibrary = Set( "scala.Tuple$.Helpers$", "scala.Tuple$.Helpers$.ReverseImpl", "scala.Tuple$.Reverse", - "scala.runtime.Tuples$.reverse" + "scala.runtime.Tuples$.reverse", + + // New feature: named tuples + "scala.NamedTuple", + "scala.NamedTuple$", + "scala.Tuple$.NamedValue", + "scala.Tuple$.NamedValue$", + + ) diff --git a/tests/run/named-tuples.check b/tests/run/named-tuples.check index 629414f6530d..c53a2f52ff09 100644 --- a/tests/run/named-tuples.check +++ b/tests/run/named-tuples.check @@ -4,3 +4,6 @@ Bob (Bob,33,Lausanne,1003) 33 no match +Bob is younger than Bill +Bob is younger than Lucy +Bill is younger than Lucy diff --git a/tests/run/named-tuples.scala b/tests/run/named-tuples.scala index 71a90f6dfd1a..0c7b9dc75a11 100644 --- a/tests/run/named-tuples.scala +++ b/tests/run/named-tuples.scala @@ -1,3 +1,5 @@ +import annotation.experimental +import language.experimental.namedTuples import NamedTuple.dropNames type Person = (name: String, age: Int) @@ -40,4 +42,42 @@ val _: CombinedInfo = bob ++ addr assert(ageOf((name = "anon", age = 22)) == 22) assert(ageOf(("anon", 11)) == 11) + val persons = List( + bob, + (name = "Bill", age = 40), + (name = "Lucy", age = 45) + ) + for + p <- persons + q <- persons + if p.age < q.age + do + println(s"${p.name} is younger than ${q.name}") + + //persons.select(_.age, _.name) + //persons.join(addresses).withCommon(_.name) + + def minMax(elems: Int*): (min: Int, max: Int) = + var min = elems(0) + var max = elems(0) + for elem <- elems do + if elem < min then min = elem + if elem > max then max = elem + (min = min, max = max) + + val mm = minMax(1, 3, 400, -3, 10) + assert(mm.min == -3) + assert(mm.max == 400) + + val name1 = bob(0).value + val age1 = bob(1).value + +// should the .value above be inferred or maybe tuple indexing should strip names? +// But then we could not do this: + + def swap[A, B](x: (A, B)): (B, A) = (x(1), x(0)) + + val bobS = swap(bob) + val _: (age: Int, name: String) = bobS + From 3d31d20584d90831c07cacd4563de1738e1ab486 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 25 Nov 2023 18:43:56 +0100 Subject: [PATCH 03/15] Generalize tupleElems - Always follow skolems to underlying types - Also follow other singletons to underlying types if normalize is true --- compiler/src/dotty/tools/dotc/core/TypeUtils.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index 5df9379cb606..4e7b6dae310b 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -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 @@ -84,6 +88,9 @@ 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 = defn.NamedTupleElem.unapply(self).isDefined + /** The `*:` equivalent of an instance of a Tuple class */ def toNestedPairs(using Context): Type = tupleElementTypes match From 640c671c3e43998b5d49292866be595325190aec Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 26 Nov 2023 10:46:06 +0100 Subject: [PATCH 04/15] Complete implementation of pattern matching for named tuples --- .../src/dotty/tools/dotc/ast/Desugar.scala | 89 +++++++++++++++---- .../dotty/tools/dotc/core/Definitions.scala | 12 ++- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../dotty/tools/dotc/typer/Applications.scala | 3 +- .../src/dotty/tools/dotc/typer/Checking.scala | 9 ++ .../src/dotty/tools/dotc/typer/Typer.scala | 11 ++- tests/neg/i7751.scala | 2 +- tests/neg/named-tuples.check | 24 +++++ tests/neg/named-tuples.scala | 17 ++++ tests/run/named-tuples.scala | 15 +++- 10 files changed, 153 insertions(+), 30 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index b7fc163d3fd4..8d752620422c 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1445,14 +1445,26 @@ object desugar { AppliedTypeTree( TypeTree(defn.throwsAlias.typeRef).withSpan(op.span), tpt :: excepts :: Nil) + private def checkMismatched(elems: List[Tree])(using Context) = 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 _ => + /** Translate tuple expressions of arity <= 22 * * () ==> () * (t) ==> t * (t1, ..., tN) ==> TupleN(t1, ..., tN) */ - def tuple(tree: Tuple)(using Context): Tree = - val elems = tree.trees.mapConserve(desugarTupleElem) + def tuple(tree: Tuple, pt: Type)(using Context): Tree = + checkMismatched(tree.trees) + val adapted = adaptTupleElems(tree.trees, pt) + val elems = adapted.mapConserve(desugarTupleElem) val arity = elems.length if arity <= Definitions.MaxTupleArity then def tupleTypeRef = defn.TupleType(arity).nn @@ -1465,20 +1477,67 @@ object desugar { else cpy.Tuple(tree)(elems) - private def desugarTupleElem(elem: untpd.Tree)(using Context): untpd.Tree = elem match + def adaptTupleElems(elems: List[Tree], pt: Type)(using Context): List[Tree] = + + 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", arg.srcPos) + reordered.toList + + def convertedToNamedArgs(selElems: List[Type]): List[untpd.Tree] = + elems.lazyZip(selElems).map: + case (arg, defn.NamedTupleElem(name, _)) => + arg match + case NamedArg(_, _) => arg // can arise for malformed elements + case _ => NamedArg(name, arg) + case (arg, _) => + arg + + pt.tupleElementTypes match + case Some(selElems @ (firstSelElem :: _)) if ctx.mode.is(Mode.Pattern) => + elems match + case (first @ NamedArg(_, _)) :: _ => + reorderedNamedArgs(selElems, first.span.startPos) + case _ => + if firstSelElem.isNamedTupleElem + then convertedToNamedArgs(selElems) + else elems + case _ => + elems + end adaptTupleElems + + private def desugarTupleElem(elem: Tree)(using Context): Tree = elem match case NamedArg(name, arg) => - val nameLit = untpd.Literal(Constant(name.toString)) - if ctx.mode.is(Mode.Type) then - untpd.AppliedTypeTree(untpd.ref(defn.Tuple_NamedValueType), - untpd.SingletonTypeTree(nameLit) :: arg :: Nil) - else if ctx.mode.is(Mode.Pattern) then - untpd.Apply( - untpd.Block(Nil, - untpd.TypeApply(untpd.ref(defn.Tuple_NamedValue_extract), - untpd.SingletonTypeTree(nameLit) :: Nil)), - arg :: Nil) - else - untpd.Apply(untpd.ref(defn.Tuple_NamedValue_apply), nameLit :: arg :: Nil) + locally: + val nameLit = Literal(Constant(name.toString)) + if ctx.mode.is(Mode.Type) then + AppliedTypeTree(ref(defn.Tuple_NamedValueTypeRef), + SingletonTypeTree(nameLit) :: arg :: Nil) + else if ctx.mode.is(Mode.Pattern) then + Apply( + Block(Nil, + TypeApply( + untpd.Select(untpd.ref(defn.Tuple_NamedValueModuleRef), nme.extract), + SingletonTypeTree(nameLit) :: Nil)), + arg :: Nil) + else + Apply( + Select(ref(defn.Tuple_NamedValueModuleRef), nme.apply), + nameLit :: arg :: Nil) + .withSpan(elem.span) case _ => elem diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index befb6e1ca301..259f1f389033 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -938,13 +938,11 @@ 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 TupleNamedValueModule: Symbol = requiredModule("scala.Tuple.NamedValue") - @tu lazy val Tuple_NamedValue_apply: Symbol = TupleNamedValueModule.requiredMethod("apply") - @tu lazy val Tuple_NamedValue_extract: Symbol = TupleNamedValueModule.requiredMethod("extract") - def Tuple_NamedValueType: TypeRef = TupleModule.termRef.select("NamedValue".toTypeName).asInstanceOf + def Tuple_NamedValueTypeRef: TypeRef = TupleModule.termRef.select("NamedValue".toTypeName).asInstanceOf + def Tuple_NamedValueModuleRef: TermRef = TupleModule.termRef.select("NamedValue".toTermName).asInstanceOf // Note: It would be dangerous to expose NamedValue as a symbol, since - // NamedValue.typeRef gives the internal view of NamedValue inside Tuple + // NamedValue.{typeRef/termRef} give the internal view of NamedValue inside Tuple // which reveals the opaque alias. To see it externally, we need the construction // above. Without this tweak, named-tuples.scala fails -Ycheck after typer. @@ -1315,10 +1313,10 @@ class Definitions { object NamedTupleElem: def apply(name: Name, tp: Type)(using Context): Type = - AppliedType(Tuple_NamedValueType, ConstantType(Constant(name.toString)) :: tp :: Nil) + AppliedType(Tuple_NamedValueTypeRef, 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 == Tuple_NamedValueType.typeSymbol => Some((s.toTermName, tp)) + if tycon.typeSymbol == Tuple_NamedValueTypeRef.typeSymbol => Some((s.toTermName, tp)) case _ => None final def isCompiletime_S(sym: Symbol)(using Context): Boolean = diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 253a45ffd7a8..9d97b02ea8fc 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -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" diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 004b21ce4fb5..03824a398ed5 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1447,6 +1447,7 @@ trait Applications extends Compatibility { val dummyArg = dummyTreeOfType(ownType) val unapplyApp = typedExpr(untpd.TypedSplice(Apply(unapplyFn, dummyArg :: Nil))) + def unapplyImplicits(unapp: Tree): List[Tree] = { val res = List.newBuilder[Tree] def loop(unapp: Tree): Unit = unapp match { @@ -1481,7 +1482,7 @@ trait Applications extends Compatibility { else tryWithTypeTest(Typed(result, TypeTree(ownType)), selType) case tp => val unapplyErr = if (tp.isError) unapplyFn else notAnExtractor(unapplyFn) - val typedArgsErr = args mapconserve (typed(_, defn.AnyType)) + val typedArgsErr = args.mapconserve(typed(_, defn.AnyType)) cpy.UnApply(tree)(unapplyErr, Nil, typedArgsErr) withType unapplyErr.tpe } } diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 3b57ada3fc77..07085082cf3f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -851,6 +851,15 @@ object Checking { templ.parents.find(_.tpe.derivesFrom(defn.PolyFunctionClass)) match case Some(parent) => report.error(s"`PolyFunction` marker trait is reserved for compiler generated refinements", parent.srcPos) case None => + + /** Check that `tp` is not a tuple containing a mixture of named and unnamed elements */ + def checkTupleWF(tp: Type, pos: SrcPos, kind: String = "")(using Context): Unit = + tp.tupleElementTypes match + case Some(elem :: elems1) + if elem.isNamedTupleElem && elems1.exists(!_.isNamedTupleElem) + || !elem.isNamedTupleElem && elems1.exists(_.isNamedTupleElem) => + report.error(em"Illegal combination of named and unnamed tuple elements in$kind type $tp", pos) + case _ => } trait Checking { diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 2ee8d31809e1..d674ec541988 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -712,9 +712,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case _ => false if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then typed( - untpd.Apply( - untpd.Select(untpd.TypedSplice(qual), nme.apply), - untpd.Literal(Constant(nameIdx))), + untpd.Select( + untpd.Apply( + untpd.Select(untpd.TypedSplice(qual), nme.apply), + untpd.Literal(Constant(nameIdx))), + nme.value), pt) else if qual.tpe.isSmallGenericTuple then typedSelect(tree, pt, qual.cast(defn.tupleType(tupleElems))) @@ -1801,6 +1803,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val selType = rawSelectorTpe match case c: ConstantType if tree.isInline => c case otherTpe => otherTpe.widen + checkTupleWF(selType, tree.selector.srcPos, " expression's") /** Does `tree` has the same shape as the given match type? * We only support typed patterns with empty guards, but @@ -3068,7 +3071,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer /** Translate tuples of all arities */ def typedTuple(tree: untpd.Tuple, pt: Type)(using Context): Tree = - val tree1 = desugar.tuple(tree) + val tree1 = desugar.tuple(tree, pt) if tree1 ne tree then typed(tree1, pt) else val arity = tree.trees.length diff --git a/tests/neg/i7751.scala b/tests/neg/i7751.scala index 978ed860574f..18070cfd0551 100644 --- a/tests/neg/i7751.scala +++ b/tests/neg/i7751.scala @@ -1,3 +1,3 @@ import language.`3.3` -val a = Some(a=a,)=> // error // error +val a = Some(a=a,)=> // error // error // error val a = Some(x=y,)=> diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check index 9aa9640fb50f..90cd52c69971 100644 --- a/tests/neg/named-tuples.check +++ b/tests/neg/named-tuples.check @@ -26,3 +26,27 @@ | Required: (age: Int, name: String) | | longer explanation available when compiling with `-explain` +-- Error: tests/neg/named-tuples.scala:20:10 --------------------------------------------------------------------------- +20 | case (name = n, age = a) => () // error // error + | ^^^^^^^^ + | No element named `name` is defined +-- Error: tests/neg/named-tuples.scala:20:20 --------------------------------------------------------------------------- +20 | case (name = n, age = a) => () // error // error + | ^^^^^^^ + | No element named `age` is defined +-- Error: tests/neg/named-tuples.scala:25:9 ---------------------------------------------------------------------------- +25 | person ++ (1, 2) match // error + | ^^^^^^^^^^^^^^^^ + | Illegal combination of named and unnamed tuple elements in expression's type (name: String, age: Int, Int, Int) +-- Error: tests/neg/named-tuples.scala:28:17 --------------------------------------------------------------------------- +28 | val bad = ("", age = 10) // error + | ^^^^^^^^ + | Illegal combination of named and unnamed tuple elements +-- Error: tests/neg/named-tuples.scala:31:20 --------------------------------------------------------------------------- +31 | case (name = n, age) => () // error + | ^^^ + | Illegal combination of named and unnamed tuple elements +-- Error: tests/neg/named-tuples.scala:32:16 --------------------------------------------------------------------------- +32 | case (name, age = a) => () // error + | ^^^^^^^ + | Illegal combination of named and unnamed tuple elements diff --git a/tests/neg/named-tuples.scala b/tests/neg/named-tuples.scala index 05fc8858bc98..0a040d46c64e 100644 --- a/tests/neg/named-tuples.scala +++ b/tests/neg/named-tuples.scala @@ -15,3 +15,20 @@ import language.experimental.namedTuples val _: Person = nameOnly // error val _: (age: Int, name: String) = person // error + + ("Ives", 2) match + case (name = n, age = a) => () // error // error + + val pp = person ++ (1, 2) // ok, but should also be error + val qq = ("a", true) ++ (1, 2) + + person ++ (1, 2) match // error + case _ => + + val bad = ("", age = 10) // error + + person match + case (name = n, age) => () // error + case (name, age = a) => () // error + + diff --git a/tests/run/named-tuples.scala b/tests/run/named-tuples.scala index 0c7b9dc75a11..b63628713859 100644 --- a/tests/run/named-tuples.scala +++ b/tests/run/named-tuples.scala @@ -1,4 +1,3 @@ -import annotation.experimental import language.experimental.namedTuples import NamedTuple.dropNames @@ -76,8 +75,20 @@ val _: CombinedInfo = bob ++ addr // But then we could not do this: def swap[A, B](x: (A, B)): (B, A) = (x(1), x(0)) - val bobS = swap(bob) val _: (age: Int, name: String) = bobS + val silly = bob match + case (name, age) => name.length + age + + assert(silly == 36) + + val minors = persons.filter: + case (age = a) => a < 18 + case _ => false + assert(minors.isEmpty) + + bob match + case (age = 33, name = "Bob") => () + case _ => assert(false) From 582ebd09934ece5ce24d626cccf97c2a0a95b6d7 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 26 Nov 2023 11:56:10 +0100 Subject: [PATCH 05/15] Switch handling of regular tuple patterns When matching a regular tuple pattern against a named tuple selector we adapted the tuple to be named, which could hide errors. We now adapt the selector to be unnamed instead. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 36 ++++++------------- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 25 +++++++++++++ .../src/dotty/tools/dotc/core/TypeUtils.scala | 23 +++++++++++- .../src/dotty/tools/dotc/typer/Typer.scala | 4 +-- tests/neg/named-tuples-2.check | 8 +++++ tests/neg/named-tuples-2.scala | 6 ++++ tests/neg/named-tuples.scala | 2 -- tests/run/named-tuples.scala | 5 ++- 8 files changed, 77 insertions(+), 32 deletions(-) create mode 100644 tests/neg/named-tuples-2.check create mode 100644 tests/neg/named-tuples-2.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 8d752620422c..9189a2b1471d 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1461,9 +1461,9 @@ object desugar { * (t) ==> t * (t1, ..., tN) ==> TupleN(t1, ..., tN) */ - def tuple(tree: Tuple, pt: Type)(using Context): Tree = + def tuple(tree: Tuple, pt: Type)(using Context): (Tree, Type) = checkMismatched(tree.trees) - val adapted = adaptTupleElems(tree.trees, pt) + val (adapted, pt1) = adaptTupleElems(tree.trees, pt) val elems = adapted.mapConserve(desugarTupleElem) val arity = elems.length if arity <= Definitions.MaxTupleArity then @@ -1473,11 +1473,11 @@ object desugar { 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) + (tree1.withSpan(tree.span), pt1) else - cpy.Tuple(tree)(elems) + (cpy.Tuple(tree)(elems), pt1) - def adaptTupleElems(elems: List[Tree], pt: Type)(using Context): List[Tree] = + def adaptTupleElems(elems: List[Tree], pt: Type)(using Context): (List[Tree], Type) = def reorderedNamedArgs(selElems: List[Type], wildcardSpan: Span): List[untpd.Tree] = val nameIdx = @@ -1497,26 +1497,15 @@ object desugar { report.error(em"No element named `$name` is defined", arg.srcPos) reordered.toList - def convertedToNamedArgs(selElems: List[Type]): List[untpd.Tree] = - elems.lazyZip(selElems).map: - case (arg, defn.NamedTupleElem(name, _)) => - arg match - case NamedArg(_, _) => arg // can arise for malformed elements - case _ => NamedArg(name, arg) - case (arg, _) => - arg - pt.tupleElementTypes match - case Some(selElems @ (firstSelElem :: _)) if ctx.mode.is(Mode.Pattern) => + case Some(selElems) if ctx.mode.is(Mode.Pattern) => elems match case (first @ NamedArg(_, _)) :: _ => - reorderedNamedArgs(selElems, first.span.startPos) + (reorderedNamedArgs(selElems, first.span.startPos), pt) case _ => - if firstSelElem.isNamedTupleElem - then convertedToNamedArgs(selElems) - else elems + (elems, pt.dropNamedTupleElems) case _ => - elems + (elems, pt) end adaptTupleElems private def desugarTupleElem(elem: Tree)(using Context): Tree = elem match @@ -1527,12 +1516,7 @@ object desugar { AppliedTypeTree(ref(defn.Tuple_NamedValueTypeRef), SingletonTypeTree(nameLit) :: arg :: Nil) else if ctx.mode.is(Mode.Pattern) then - Apply( - Block(Nil, - TypeApply( - untpd.Select(untpd.ref(defn.Tuple_NamedValueModuleRef), nme.extract), - SingletonTypeTree(nameLit) :: Nil)), - arg :: Nil) + NamedElemPattern(name, arg) else Apply( Select(ref(defn.Tuple_NamedValueModuleRef), nme.apply), diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 5ded0e1262e4..fdf9b95f49c7 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -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.Tuple_NamedValueModuleRef), 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.Tuple_NamedValueModuleRef.symbol => + Some((name.toTermName, elem)) + case _ => None + + end NamedElemPattern } trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index 4e7b6dae310b..2454ebddf6c5 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -89,7 +89,28 @@ class TypeUtils { case _ => false /** Is this type a named tuple element `name = value`? */ - def isNamedTupleElem(using Context): Boolean = defn.NamedTupleElem.unapply(self).isDefined + 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 = diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index d674ec541988..40abed3e1457 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -3071,8 +3071,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer /** Translate tuples of all arities */ def typedTuple(tree: untpd.Tuple, pt: Type)(using Context): Tree = - val tree1 = desugar.tuple(tree, pt) - if tree1 ne tree then typed(tree1, pt) + val (tree1, pt1) = desugar.tuple(tree, pt) + if (tree1 ne tree) || (pt1 ne pt) then typed(tree1, pt1) else val arity = tree.trees.length val pts = pt.tupleElementTypes match diff --git a/tests/neg/named-tuples-2.check b/tests/neg/named-tuples-2.check new file mode 100644 index 000000000000..876aa9103f9c --- /dev/null +++ b/tests/neg/named-tuples-2.check @@ -0,0 +1,8 @@ +-- Error: tests/neg/named-tuples-2.scala:5:9 --------------------------------------------------------------------------- +5 | case (name, age) => () // error + | ^ + | this case is unreachable since type (name: String, age: Int, married: Boolean) is not a subclass of class Tuple2 +-- Error: tests/neg/named-tuples-2.scala:6:9 --------------------------------------------------------------------------- +6 | case (n, a, m, x) => () // error + | ^ + | this case is unreachable since type (name: String, age: Int, married: Boolean) is not a subclass of class Tuple4 diff --git a/tests/neg/named-tuples-2.scala b/tests/neg/named-tuples-2.scala new file mode 100644 index 000000000000..0507891e0549 --- /dev/null +++ b/tests/neg/named-tuples-2.scala @@ -0,0 +1,6 @@ +import language.experimental.namedTuples +def Test = + val person = (name = "Bob", age = 33, married = true) + person match + case (name, age) => () // error + case (n, a, m, x) => () // error diff --git a/tests/neg/named-tuples.scala b/tests/neg/named-tuples.scala index 0a040d46c64e..785d631a77a1 100644 --- a/tests/neg/named-tuples.scala +++ b/tests/neg/named-tuples.scala @@ -30,5 +30,3 @@ import language.experimental.namedTuples person match case (name = n, age) => () // error case (name, age = a) => () // error - - diff --git a/tests/run/named-tuples.scala b/tests/run/named-tuples.scala index b63628713859..3a1e88785978 100644 --- a/tests/run/named-tuples.scala +++ b/tests/run/named-tuples.scala @@ -90,5 +90,8 @@ val _: CombinedInfo = bob ++ addr assert(minors.isEmpty) bob match - case (age = 33, name = "Bob") => () + case bob1 @ (age = 33, name = "Bob") => + val x: Person = bob1 // bob1 still has type Person with the unswapped elements case _ => assert(false) + + From b88d4c6db4361fee02aa4c35177e181529653fcb Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 26 Nov 2023 14:30:49 +0100 Subject: [PATCH 06/15] Refine handling of pattern binders for large tuples --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 40abed3e1457..ee12c042def5 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2447,7 +2447,13 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // wrt to operand order for `&`, we include the explicit subtype test here. // See also #5649. then body1.tpe - else pt & body1.tpe + else body1.tpe match + case btpe: TypeRef + if btpe.symbol == defn.TupleXXLClass && pt.tupleElementTypes.isDefined => + // leave the original tuple type; don't mix with & TupleXXL which would only obscure things + pt + case _ => + pt & body1.tpe val sym = newPatternBoundSymbol(name, symTp, tree.span) if (pt == defn.ImplicitScrutineeTypeRef || tree.mods.is(Given)) sym.setFlag(Given) if (ctx.mode.is(Mode.InPatternAlternative)) From e588c5a9986fb74739e014e8332da1f6517195b9 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 26 Nov 2023 14:31:10 +0100 Subject: [PATCH 07/15] Add large tuple tests --- tests/run/named-tuples-xxl.check | 6 +++ tests/run/named-tuples-xxl.scala | 92 ++++++++++++++++++++++++++++++++ tests/run/named-tuples.scala | 2 + 3 files changed, 100 insertions(+) create mode 100644 tests/run/named-tuples-xxl.check create mode 100644 tests/run/named-tuples-xxl.scala diff --git a/tests/run/named-tuples-xxl.check b/tests/run/named-tuples-xxl.check new file mode 100644 index 000000000000..ee5f60bec756 --- /dev/null +++ b/tests/run/named-tuples-xxl.check @@ -0,0 +1,6 @@ +(0,0,0,0,0,0,0,0,0,0,Bob,0,33,0,0,0,0,0,0,0,0,0,0,0) +(0,0,0,0,0,0,0,0,0,0,Bob,0,33,0,0,0,0,0,0,0,0,0,0,0) +(0,0,0,0,0,0,0,0,0,0,Bob,0,33,0,0,0,0,0,0,0,0,0,0,0) +Bob is younger than Bill +Bob is younger than Lucy +Bill is younger than Lucy diff --git a/tests/run/named-tuples-xxl.scala b/tests/run/named-tuples-xxl.scala new file mode 100644 index 000000000000..d3606ae5b6bd --- /dev/null +++ b/tests/run/named-tuples-xxl.scala @@ -0,0 +1,92 @@ +import language.experimental.namedTuples +import NamedTuple.dropNames + +type Person = ( + x0: Int, x1: Int, x2: Int, x3: Int, x4: Int, x5: Int, x6: Int, x7: Int, x8: Int, x9: Int, + name: String, y1: Int, age: Int, y2: Int, + z0: Int, z1: Int, z2: Int, z3: Int, z4: Int, z5: Int, z6: Int, z7: Int, z8: Int, z9: Int) + +val bob = ( + x0 = 0, x1 = 0, x2 = 0, x3 = 0, x4 = 0, x5 = 0, x6 = 0, x7 = 0, x8 = 0, x9 = 0, + name = "Bob", y1 = 0, age = 33, y2 = 0, + z0 = 0, z1 = 0, z2 = 0, z3 = 0, z4 = 0, z5 = 0, z6 = 0, z7 = 0, z8 = 0, z9 = 0) + +val person2: Person = bob + + +type AddressInfo = (city: String, zip: Int) +val addr = (city = "Lausanne", zip = 1003) + +type CombinedInfo = Tuple.Concat[Person, AddressInfo] +val bobWithAddr = bob ++ addr +val _: CombinedInfo = bobWithAddr +val _: CombinedInfo = bob ++ addr + +@main def Test = + assert(bob.name == "Bob") + assert(bob.age == 33) + bob match + case p @ (name = "Bob", age = a) => // !!! spurious unreachable case warning + val x = p + println(x) + assert(p.age == 33) + assert(a == 33) + case _ => + assert(false) + bob match + case p @ (name = "Peter", age = _) => assert(false) + case p @ (name = "Bob", age = 0) => assert(false) + case _ => + bob match + case b @ (x0 = 0, x1 = 0, x2 = 0, x3 = 0, x4 = 0, x5 = 0, x6 = 0, x7 = 0, x8 = 0, x9 = 0, + name = "Bob", y1 = 0, age = 33, y2 = 0, + z0 = 0, z1 = 0, z2 = 0, z3 = 0, z4 = 0, z5 = 0, z6 = 0, z7 = 0, z8 = 0, z9 = 0) + => // !!! spurious unreachable case warning + println(bob) + println(b) + case _ => assert(false) + + val x = bob.age + assert(x == 33) + + val y: ( + Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, + String, Int, Int, Int, + Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) + = bob.dropNames + + def ageOf(person: Person) = person.age + + assert(ageOf(bob) == 33) + + val persons = List( + bob, + (x0 = 0, x1 = 0, x2 = 0, x3 = 0, x4 = 0, x5 = 0, x6 = 0, x7 = 0, x8 = 0, x9 = 0, + name = "Bill", y1 = 0, age = 40, y2 = 0, + z0 = 0, z1 = 0, z2 = 0, z3 = 0, z4 = 0, z5 = 0, z6 = 0, z7 = 0, z8 = 0, z9 = 0), + (x0 = 0, x1 = 0, x2 = 0, x3 = 0, x4 = 0, x5 = 0, x6 = 0, x7 = 0, x8 = 0, x9 = 0, + name = "Lucy", y1 = 0, age = 45, y2 = 0, + z0 = 0, z1 = 0, z2 = 0, z3 = 0, z4 = 0, z5 = 0, z6 = 0, z7 = 0, z8 = 0, z9 = 0), + ) + for + p <- persons + q <- persons + if p.age < q.age + do + println(s"${p.name} is younger than ${q.name}") + + val name1 = bob(10).value + val age1 = bob(12).value + + val minors = persons.filter: + case (age = a) => a < 18 + case _ => false + + assert(minors.isEmpty) + + bob match + case bob1 @ (age = 33, name = "Bob") => + val x: Person = bob1 // bob1 still has type Person with the unswapped elements + case _ => assert(false) + + diff --git a/tests/run/named-tuples.scala b/tests/run/named-tuples.scala index 3a1e88785978..f547578c7017 100644 --- a/tests/run/named-tuples.scala +++ b/tests/run/named-tuples.scala @@ -25,6 +25,8 @@ val _: CombinedInfo = bob ++ addr println(bobWithAddr) bob match case p @ (name = "Bob", age = _) => println(p.age) + bob match + case p @ (name = "Bob", age = age) => assert(age == 33) bob match case p @ (name = "Peter", age = _) => println(p.age) case p @ (name = "Bob", age = 0) => println(p.age) From f8845175985e32916b48daf0377657a64a65a2b6 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 26 Nov 2023 16:05:44 +0100 Subject: [PATCH 08/15] More checks and neg tests --- .../src/dotty/tools/dotc/ast/Desugar.scala | 24 ++++++-- .../dotty/tools/dotc/parsing/Parsers.scala | 17 +++--- tests/neg/named-tuples.check | 56 ++++++++++++------- tests/neg/named-tuples.scala | 5 ++ tests/neg/namedTypeParams.check | 16 +++--- 5 files changed, 77 insertions(+), 41 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 9189a2b1471d..065ddc1941e6 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -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 @@ -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 @@ -1445,7 +1445,18 @@ object desugar { AppliedTypeTree( TypeTree(defn.throwsAlias.typeRef).withSpan(op.span), tpt :: excepts :: Nil) - private def checkMismatched(elems: List[Tree])(using Context) = elems match + 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] @@ -1454,6 +1465,7 @@ object desugar { 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 * @@ -1462,7 +1474,7 @@ object desugar { * (t1, ..., tN) ==> TupleN(t1, ..., tN) */ def tuple(tree: Tuple, pt: Type)(using Context): (Tree, Type) = - checkMismatched(tree.trees) + checkWellFormedTupleElems(tree.trees) val (adapted, pt1) = adaptTupleElems(tree.trees, pt) val elems = adapted.mapConserve(desugarTupleElem) val arity = elems.length @@ -2040,7 +2052,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)) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index ea8ebfcddee4..221bb78474d3 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1587,7 +1587,8 @@ object Parsers { case ByNameTypeTree(t1) => syntaxError(ByNameParameterNotSupported(t), t.span) t1 - case ValDef(name, tpt, _) => NamedArg(name, convertToElem(tpt)) + case ValDef(name, tpt, _) => + NamedArg(name, convertToElem(tpt)).withSpan(t.span) case _ => t val t = @@ -2013,14 +2014,16 @@ object Parsers { if wildOK then t else rejectWildcardType(t) def namedArgType() = - val name = ident() - accept(EQUALS) - NamedArg(name.toTypeName, argType()) + atSpan(in.offset): + val name = ident() + accept(EQUALS) + NamedArg(name.toTypeName, argType()) def namedElem() = - val name = ident() - acceptColon() - NamedArg(name, argType()) + atSpan(in.offset): + val name = ident() + acceptColon() + NamedArg(name, argType()) if namedOK && isIdent && in.lookahead.token == EQUALS then commaSeparated(() => namedArgType()) diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check index 90cd52c69971..f7319e7ad1e4 100644 --- a/tests/neg/named-tuples.check +++ b/tests/neg/named-tuples.check @@ -1,52 +1,68 @@ --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:13:25 ------------------------------------------------------ -13 | val y: (String, Int) = person // error +-- Error: tests/neg/named-tuples.scala:9:19 ---------------------------------------------------------------------------- +9 | val illformed = (_2 = 2) // error + | ^^^^^^ + | _2 cannot be used as the name of a tuple element because it is a regular tuple selector +-- Error: tests/neg/named-tuples.scala:10:20 --------------------------------------------------------------------------- +10 | type Illformed = (_1: Int) // error + | ^^^^^^^ + | _1 cannot be used as the name of a tuple element because it is a regular tuple selector +-- Error: tests/neg/named-tuples.scala:11:40 --------------------------------------------------------------------------- +11 | val illformed2 = (name = "", age = 0, name = true) // error + | ^^^^^^^^^^^ + | Duplicate tuple element name +-- Error: tests/neg/named-tuples.scala:12:45 --------------------------------------------------------------------------- +12 | type Illformed2 = (name: String, age: Int, name: Boolean) // error + | ^^^^^^^^^^^^^ + | Duplicate tuple element name +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:18:25 ------------------------------------------------------ +18 | val y: (String, Int) = person // error | ^^^^^^ | Found: (Test.person : (name: String, age: Int)) | Required: (String, Int) | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:14:20 ------------------------------------------------------ -14 | val _: NameOnly = person // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:19:20 ------------------------------------------------------ +19 | val _: NameOnly = person // error | ^^^^^^ | Found: (Test.person : (name: String, age: Int)) | Required: Test.NameOnly | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:15:18 ------------------------------------------------------ -15 | val _: Person = nameOnly // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:20:18 ------------------------------------------------------ +20 | val _: Person = nameOnly // error | ^^^^^^^^ | Found: (Test.nameOnly : (name: String)) | Required: Test.Person | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:17:36 ------------------------------------------------------ -17 | val _: (age: Int, name: String) = person // error +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:22:36 ------------------------------------------------------ +22 | val _: (age: Int, name: String) = person // error | ^^^^^^ | Found: (Test.person : (name: String, age: Int)) | Required: (age: Int, name: String) | | longer explanation available when compiling with `-explain` --- Error: tests/neg/named-tuples.scala:20:10 --------------------------------------------------------------------------- -20 | case (name = n, age = a) => () // error // error +-- Error: tests/neg/named-tuples.scala:25:10 --------------------------------------------------------------------------- +25 | case (name = n, age = a) => () // error // error | ^^^^^^^^ | No element named `name` is defined --- Error: tests/neg/named-tuples.scala:20:20 --------------------------------------------------------------------------- -20 | case (name = n, age = a) => () // error // error +-- Error: tests/neg/named-tuples.scala:25:20 --------------------------------------------------------------------------- +25 | case (name = n, age = a) => () // error // error | ^^^^^^^ | No element named `age` is defined --- Error: tests/neg/named-tuples.scala:25:9 ---------------------------------------------------------------------------- -25 | person ++ (1, 2) match // error +-- Error: tests/neg/named-tuples.scala:30:9 ---------------------------------------------------------------------------- +30 | person ++ (1, 2) match // error | ^^^^^^^^^^^^^^^^ | Illegal combination of named and unnamed tuple elements in expression's type (name: String, age: Int, Int, Int) --- Error: tests/neg/named-tuples.scala:28:17 --------------------------------------------------------------------------- -28 | val bad = ("", age = 10) // error +-- Error: tests/neg/named-tuples.scala:33:17 --------------------------------------------------------------------------- +33 | val bad = ("", age = 10) // error | ^^^^^^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:31:20 --------------------------------------------------------------------------- -31 | case (name = n, age) => () // error +-- Error: tests/neg/named-tuples.scala:36:20 --------------------------------------------------------------------------- +36 | case (name = n, age) => () // error | ^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:32:16 --------------------------------------------------------------------------- -32 | case (name, age = a) => () // error +-- Error: tests/neg/named-tuples.scala:37:16 --------------------------------------------------------------------------- +37 | case (name, age = a) => () // error | ^^^^^^^ | Illegal combination of named and unnamed tuple elements diff --git a/tests/neg/named-tuples.scala b/tests/neg/named-tuples.scala index 785d631a77a1..1f78d7eb157b 100644 --- a/tests/neg/named-tuples.scala +++ b/tests/neg/named-tuples.scala @@ -6,6 +6,11 @@ import language.experimental.namedTuples type Person = (name: String, age: Int) val person = (name = "Bob", age = 33): (name: String, age: Int) + val illformed = (_2 = 2) // error + type Illformed = (_1: Int) // error + val illformed2 = (name = "", age = 0, name = true) // error + type Illformed2 = (name: String, age: Int, name: Boolean) // error + type NameOnly = (name: String) val nameOnly = (name = "Louis") diff --git a/tests/neg/namedTypeParams.check b/tests/neg/namedTypeParams.check index 3f6f9f7913e8..5e0672f20f25 100644 --- a/tests/neg/namedTypeParams.check +++ b/tests/neg/namedTypeParams.check @@ -24,16 +24,16 @@ 19 | f[X = Int, String](1, "") // error // error | ^ | '=' expected, but ']' found --- Error: tests/neg/namedTypeParams.scala:6:8 -------------------------------------------------------------------------- +-- Error: tests/neg/namedTypeParams.scala:6:4 -------------------------------------------------------------------------- 6 | f[X = Int, Y = Int](1, 2) // error: experimental // error: experimental - | ^^^ - | Named type arguments are experimental, - | they must be enabled with a `experimental.namedTypeArguments` language import or setting --- Error: tests/neg/namedTypeParams.scala:6:17 ------------------------------------------------------------------------- + | ^^^^^^^ + | Named type arguments are experimental, + | they must be enabled with a `experimental.namedTypeArguments` language import or setting +-- Error: tests/neg/namedTypeParams.scala:6:13 ------------------------------------------------------------------------- 6 | f[X = Int, Y = Int](1, 2) // error: experimental // error: experimental - | ^^^ - | Named type arguments are experimental, - | they must be enabled with a `experimental.namedTypeArguments` language import or setting + | ^^^^^^^ + | Named type arguments are experimental, + | they must be enabled with a `experimental.namedTypeArguments` language import or setting -- [E006] Not Found Error: tests/neg/namedTypeParams.scala:11:11 ------------------------------------------------------- 11 | val x: C[T = Int] = // error: ']' expected, but `=` found // error | ^ From 603ae9d29a18db7a47c1046f705f49ecc4106183 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 26 Nov 2023 18:29:37 +0100 Subject: [PATCH 09/15] Tighten checks for named patterns Also disallow checking a named pattern against a top type like Any or Tuple. The names in a named pattern _must_ be statically visible in the selector type. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 25 +++++++------ tests/neg/named-tuples.check | 36 +++++++++++-------- tests/neg/named-tuples.scala | 5 +++ 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 065ddc1941e6..1f43585bc511 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1489,6 +1489,12 @@ object desugar { 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] = @@ -1506,18 +1512,17 @@ object desugar { else report.error(em"Duplicate named pattern", arg.srcPos) case _ => - report.error(em"No element named `$name` is defined", arg.srcPos) + report.error(em"No element named `$name` is defined in selector type $pt", arg.srcPos) reordered.toList - pt.tupleElementTypes match - case Some(selElems) if ctx.mode.is(Mode.Pattern) => - elems match - case (first @ NamedArg(_, _)) :: _ => - (reorderedNamedArgs(selElems, first.span.startPos), pt) - case _ => - (elems, pt.dropNamedTupleElems) - case _ => - (elems, pt) + 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 diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check index f7319e7ad1e4..8e1e117a832e 100644 --- a/tests/neg/named-tuples.check +++ b/tests/neg/named-tuples.check @@ -42,27 +42,35 @@ | Required: (age: Int, name: String) | | longer explanation available when compiling with `-explain` --- Error: tests/neg/named-tuples.scala:25:10 --------------------------------------------------------------------------- -25 | case (name = n, age = a) => () // error // error +-- Error: tests/neg/named-tuples.scala:24:17 --------------------------------------------------------------------------- +24 | val (name = x, agee = y) = person // error + | ^^^^^^^^ + | No element named `agee` is defined in selector type (name: String, age: Int) +-- Error: tests/neg/named-tuples.scala:27:10 --------------------------------------------------------------------------- +27 | case (name = n, age = a) => () // error // error | ^^^^^^^^ - | No element named `name` is defined --- Error: tests/neg/named-tuples.scala:25:20 --------------------------------------------------------------------------- -25 | case (name = n, age = a) => () // error // error + | No element named `name` is defined in selector type (String, Int) +-- Error: tests/neg/named-tuples.scala:27:20 --------------------------------------------------------------------------- +27 | case (name = n, age = a) => () // error // error | ^^^^^^^ - | No element named `age` is defined --- Error: tests/neg/named-tuples.scala:30:9 ---------------------------------------------------------------------------- -30 | person ++ (1, 2) match // error + | No element named `age` is defined in selector type (String, Int) +-- Error: tests/neg/named-tuples.scala:32:9 ---------------------------------------------------------------------------- +32 | person ++ (1, 2) match // error | ^^^^^^^^^^^^^^^^ | Illegal combination of named and unnamed tuple elements in expression's type (name: String, age: Int, Int, Int) --- Error: tests/neg/named-tuples.scala:33:17 --------------------------------------------------------------------------- -33 | val bad = ("", age = 10) // error +-- Error: tests/neg/named-tuples.scala:35:17 --------------------------------------------------------------------------- +35 | val bad = ("", age = 10) // error | ^^^^^^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:36:20 --------------------------------------------------------------------------- -36 | case (name = n, age) => () // error +-- Error: tests/neg/named-tuples.scala:38:20 --------------------------------------------------------------------------- +38 | case (name = n, age) => () // error | ^^^ | Illegal combination of named and unnamed tuple elements --- Error: tests/neg/named-tuples.scala:37:16 --------------------------------------------------------------------------- -37 | case (name, age = a) => () // error +-- Error: tests/neg/named-tuples.scala:39:16 --------------------------------------------------------------------------- +39 | case (name, age = a) => () // error | ^^^^^^^ | Illegal combination of named and unnamed tuple elements +-- Error: tests/neg/named-tuples.scala:42:10 --------------------------------------------------------------------------- +42 | case (age = x) => // error + | ^^^^^^^ + | No element named `age` is defined in selector type Tuple diff --git a/tests/neg/named-tuples.scala b/tests/neg/named-tuples.scala index 1f78d7eb157b..2fe8a9bd8c27 100644 --- a/tests/neg/named-tuples.scala +++ b/tests/neg/named-tuples.scala @@ -21,6 +21,8 @@ import language.experimental.namedTuples val _: (age: Int, name: String) = person // error + val (name = x, agee = y) = person // error + ("Ives", 2) match case (name = n, age = a) => () // error // error @@ -35,3 +37,6 @@ import language.experimental.namedTuples person match case (name = n, age) => () // error case (name, age = a) => () // error + + (??? : Tuple) match + case (age = x) => // error \ No newline at end of file From 8579f42a57949ca4c2c9a83a3b631cc550e3c105 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 27 Nov 2023 19:12:00 +0100 Subject: [PATCH 10/15] Reorganize NamedTuple operations - Now all operations are in the NamedTuple class. - Tuple is not longer affected. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 4 ++-- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 4 ++-- .../dotty/tools/dotc/core/Definitions.scala | 20 ++++++++++--------- library/src/scala/NamedTuple.scala | 15 ++++++++++++-- library/src/scala/Tuple.scala | 13 ------------ 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 1f43585bc511..573d82020b7f 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1530,13 +1530,13 @@ object desugar { locally: val nameLit = Literal(Constant(name.toString)) if ctx.mode.is(Mode.Type) then - AppliedTypeTree(ref(defn.Tuple_NamedValueTypeRef), + 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.Tuple_NamedValueModuleRef), nme.apply), + Select(ref(defn.NamedTuple_ElementModuleRef), nme.apply), nameLit :: arg :: Nil) .withSpan(elem.span) case _ => diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index fdf9b95f49c7..016ecc27ea77 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -491,7 +491,7 @@ trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] Apply( Block(Nil, TypeApply( - untpd.Select(untpd.ref(defn.Tuple_NamedValueModuleRef), nme.extract), + untpd.Select(untpd.ref(defn.NamedTuple_ElementModuleRef), nme.extract), SingletonTypeTree(Literal(Constant(name.toString))) :: Nil)), elem :: Nil) @@ -501,7 +501,7 @@ trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] TypeApply( untpd.Select(TypedSplice(namedValue), nme.extract), SingletonTypeTree(Literal(Constant(name: String))) :: Nil)), - elem :: Nil) if namedValue.symbol == defn.Tuple_NamedValueModuleRef.symbol => + elem :: Nil) if namedValue.symbol == defn.NamedTuple_ElementModuleRef.symbol => Some((name.toTermName, elem)) case _ => None diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 259f1f389033..8915d08cbd1d 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -939,13 +939,6 @@ class Definitions { @tu lazy val Tuple_cons: Symbol = TupleClass.requiredMethod("*:") @tu lazy val TupleModule: Symbol = requiredModule("scala.Tuple") - def Tuple_NamedValueTypeRef: TypeRef = TupleModule.termRef.select("NamedValue".toTypeName).asInstanceOf - def Tuple_NamedValueModuleRef: TermRef = TupleModule.termRef.select("NamedValue".toTermName).asInstanceOf - // Note: It would be dangerous to expose NamedValue as a symbol, since - // NamedValue.{typeRef/termRef} give the internal view of NamedValue inside Tuple - // 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 EmptyTupleClass: Symbol = requiredClass("scala.EmptyTuple") @tu lazy val EmptyTupleModule: Symbol = requiredModule("scala.EmptyTuple") @tu lazy val NonEmptyTupleTypeRef: TypeRef = requiredClassRef("scala.NonEmptyTuple") @@ -959,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") @@ -1313,10 +1315,10 @@ class Definitions { object NamedTupleElem: def apply(name: Name, tp: Type)(using Context): Type = - AppliedType(Tuple_NamedValueTypeRef, ConstantType(Constant(name.toString)) :: tp :: Nil) + 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 == Tuple_NamedValueTypeRef.typeSymbol => Some((s.toTermName, tp)) + if tycon.typeSymbol == NamedTuple_ElementTypeRef.typeSymbol => Some((s.toTermName, tp)) case _ => None final def isCompiletime_S(sym: Symbol)(using Context): Boolean = diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index 2f76138a9988..2eca72016b69 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -1,13 +1,24 @@ package scala - import annotation.experimental @experimental object NamedTuple: + opaque type Element[name <: String & Singleton, A] >: A = A + + object Element: + def apply[S <: String & Singleton, A](name: S, x: A): Element[name.type, A] = x + def extract[S <: String & Singleton]: ValueExtractor[S] = ValueExtractor[S]() + extension [S <: String & Singleton, A](named: Element[S, A]) def value: A = named + + class ValueExtractor[S <: String & Singleton]: + def unapply[A](x: Element[S, A]): Some[A] = Some(x) + end Element + type DropNames[T <: Tuple] = T match - case Tuple.NamedValue[_, x] *: xs => x *: DropNames[xs] + case Element[_, x] *: xs => x *: DropNames[xs] case _ => T extension [T <: Tuple](x: T) def dropNames: DropNames[T] = x.asInstanceOf // named and unnamed tuples have the same runtime representation +end NamedTuple diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 95febb272ae9..6993b8202082 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -241,19 +241,6 @@ object Tuple { */ type Union[T <: Tuple] = Fold[T, Nothing, [x, y] =>> x | y] - @experimental - opaque type NamedValue[name <: String & Singleton, A] >: A = A - - @experimental - object NamedValue: - def apply[S <: String & Singleton, A](name: S, x: A): NamedValue[name.type, A] = x - def extract[S <: String & Singleton]: NameExtractor[S] = NameExtractor[S]() - extension [S <: String & Singleton, A](named: NamedValue[S, A]) def value: A = named - - class NameExtractor[S <: String & Singleton]: - def unapply[A](x: NamedValue[S, A]): Some[A] = Some(x) - end NamedValue - /** Empty tuple */ def apply(): EmptyTuple = EmptyTuple From ba9d99b2aae1f44792ae0537b7da128619e0e3f7 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 27 Nov 2023 20:01:33 +0100 Subject: [PATCH 11/15] Add docs --- docs/_docs/internals/syntax.md | 11 +- .../reference/experimental/named-tuples.md | 158 ++++++++++++++++++ docs/_docs/reference/syntax.md | 10 +- docs/sidebar.yml | 1 + 4 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 docs/_docs/reference/experimental/named-tuples.md diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index aa8cd15f00a0..ba1213517944 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -193,7 +193,7 @@ SimpleType ::= SimpleLiteral SimpleType1 ::= id Ident(name) | Singleton ‘.’ id Select(t, name) | Singleton ‘.’ ‘type’ SingletonTypeTree(p) - | ‘(’ Types ‘)’ Tuple(ts) + | ‘(’ [Types | NamesAndTypes] ‘)’ Tuple(ts) | Refinement RefinedTypeTree(EmptyTree, refinement) | TypeSplice -- deprecated syntax | SimpleType1 TypeArgs AppliedTypeTree(t, args) @@ -212,6 +212,8 @@ Refinement ::= :<<< [RefineDef] {semi [RefineDef]} >>> TypeBounds ::= [‘>:’ Type] [‘<:’ Type] TypeBoundsTree(lo, hi) TypeParamBounds ::= TypeBounds {‘:’ Type} ContextBounds(typeBounds, tps) Types ::= Type {‘,’ Type} +NamesAndTypes ::= NameAndType {‘,’ NameAndType} +NameAndType ::= id ':' Type ``` ### Expressions @@ -280,8 +282,10 @@ TypeSplice ::= spliceId | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted type pattern -- deprecated syntax | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted type pattern -- deprecated syntax ExprsInParens ::= ExprInParens {‘,’ ExprInParens} + | NamedExprInParens {‘,’ NamedExprInParens} ExprInParens ::= PostfixExpr ‘:’ Type -- normal Expr allows only RefinedType here | Expr +NamedExprInParens ::= id '=' ExprInParens ParArgumentExprs ::= ‘(’ [ExprsInParens] ‘)’ exprs | ‘(’ ‘using’ ExprsInParens ‘)’ | ‘(’ [ExprsInParens ‘,’] PostfixExpr ‘*’ ‘)’ exprs :+ Typed(expr, Ident(wildcardStar)) @@ -321,7 +325,7 @@ Pattern2 ::= [id ‘@’] InfixPattern InfixPattern ::= SimplePattern { id [nl] SimplePattern } InfixOp(pat, op, pat) SimplePattern ::= PatVar Ident(wildcard) | Literal Bind(name, Ident(wildcard)) - | ‘(’ [Patterns] ‘)’ Parens(pats) Tuple(pats) + | ‘(’ [Patterns | NamedPatterns] ‘)’ Parens(pats) Tuple(pats) | Quoted | XmlPattern (to be dropped) | SimplePattern1 [TypeArgs] [ArgumentPatterns] @@ -331,6 +335,9 @@ SimplePattern1 ::= SimpleRef PatVar ::= varid | ‘_’ Patterns ::= Pattern {‘,’ Pattern} +NamedPatterns ::= NamedPattern {‘,’ NamedPattern} +NamedPattern ::= id '=' Pattern + ArgumentPatterns ::= ‘(’ [Patterns] ‘)’ Apply(fn, pats) | ‘(’ [Patterns ‘,’] PatVar ‘*’ ‘)’ ``` diff --git a/docs/_docs/reference/experimental/named-tuples.md b/docs/_docs/reference/experimental/named-tuples.md new file mode 100644 index 000000000000..a2b69a5ae0cf --- /dev/null +++ b/docs/_docs/reference/experimental/named-tuples.md @@ -0,0 +1,158 @@ +--- +layout: doc-page +title: "Named Tuples" +nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/named-tuples.html +--- + +The elements of a tuple can now be named. Example: +```scala +type Person = (name: String, age: Int) +val Bob: Person = (name = "Bob", age = 33) + +Bob match + case (name, age) => + println(s"$name is $age years old") + +val persons: List[Person] = ... +val minors = persons.filter: p => + p.age < 18 +``` +Named bindings in tuples are similar to function parameters and arguments. We use `name: Type` for element types and `name = value` for element values. It is illegal to mix named and unnamed elements in a tuple, or to use the same same +name for two different elements. + +Fields of named tuples can be selected by their name, as in the line `p.age < 18` above. + +### Conformance + +The order of names in a named tuple matters. For instance, the type `Person` above and the type `(age: Int, name: String)` would be different, incompatible types. + +Values of named tuple types can also be be defined using regular tuples. For instance: +```scala +val x: Person = ("Laura", 25) + +def register(person: Person) = ... +register(person = ("Silvain", 16)) +register(("Silvain", 16)) +``` +This follows since a regular tuple `(T_1, ..., T_n)` is treated as a subtype of a named tuple `(N_1 = T_1, ..., N_n = T_n)` with the same element types. On the other hand, named tuples do not conform to unnamed tuples, so the following is an error: +```scala +val x: (String, Int) = Bob // error: type mismatch +``` +One can convert a named tuple to an unnamed tuple with the `dropNames` method, so the following works: +```scala +val x: (String, Int) = Bob.dropNames // ok +``` +Note that conformance rules for named tuples are analogous to the rules for named parameters. One can assign parameters by position to a named parameter list. +```scala + def f(param: Int) = ... + f(param = 1) // OK + f(2) // Also OK +``` +But one cannot use a name to pass an argument to an unnamed parameter: +```scala + val f: Int => T + f(2) // OK + f(param = 2) // Not OK +``` +The rules for tuples are analogous. Unnamed tuples conform to named tuple types, but the opposite does not hold. + + +### Pattern Matching + +When pattern matching on a named tuple, the pattern may be named or unnamed. +If the pattern is named it needs to mention only a subset of the tuple names, and these names can come in any order. So the following are all OK: +```scala +Bob match + case (name, age) => ... + +Bob match + case (name = x, age = y) => ... + +Bob match + case (age = x) => ... + +Bob match + case (age = x, name = y) => ... +``` + +### Expansion + +Named tuples are in essence just a convenient syntax for regular tuples with element types. In the internal representation, an element like `age: Int` of a named tuple is given the type `NamedTuple.Element["age", Int]`. This type +is defined in object `NamedTuple` as follows: +```scala + opaque type Element[name <: String & Singleton, A] >: A = A +``` +`Element` is an opaque type alias of its second, value parameter. The first parameter is a string constant type which determines the name of the element. +Since the type is just an alias of its value part, names are erased at runtime, +and named tuples and regular tuples have the same representation. + +The `Element` type publicly known to be a supertype (but not a subtype) of its value paramater, which means that regular tuples can be assigned to named tuples but not _vice versa_. + +A minimal implementation of the `NamedTuple` object looks like this: +```scala +package scala +object NamedTuple: + + opaque type Element[name <: String & Singleton, A] >: A = A + + object Element: + def apply[S <: String & Singleton, A](name: S, x: A): Element[name.type, A] = x + def extract[S <: String & Singleton]: ValueExtractor[S] = ValueExtractor[S]() + extension [S <: String & Singleton, A](named: Element[S, A]) def value: A = named + + class ValueExtractor[S <: String & Singleton]: + def unapply[A](x: Element[S, A]): Some[A] = Some(x) + end Element + + type DropNames[T <: Tuple] = T match + case Element[_, x] *: xs => x *: DropNames[xs] + case _ => T + + extension [T <: Tuple](x: T) def dropNames: DropNames[T] = + x.asInstanceOf // named and unnamed tuples have the same runtime representation +end NamedTuple +``` +A named tuple element value like `name = value` is expanded by the compiler to `NamedTuple.Element.apply("name", value)`. A selection on a named tuple like `x.age` is expanded as follows: First, find at compile-time the index of the field of the tuple `x` that is of type `Element["age", T]`, for some type `T`. Say this index is `n`. Then, expand the selection `x.age` to `x(n).value`. The `value` method +is an extension method on named tuple `Element`s that strips the name from the type and returns just the value part. + +A pattern match with a named tuple pattern like `age = x` and a selector value `s` translates to the `unapply` call `NamedTuple.Element.extract("age").unapply(s)`. + +This translation of named tuples to opaque element types is fixed by the specification and therefore known to the programmer. This means that: + + - All tuple operations also work with named tuples "out of the box". + - Macro libraries can rely on this expansion. + +### Restrictions + +The following restrictions apply to named tuple elements: + + 1. Either all elements of a tuple are named or none are named. It is illegal to mix named and unnamed elements in a tuple. For instance, the following is in error: + ```scala + val illFormed1 = ("Bob", age = 33) // error + ``` + 2. Each element name in a named tuple must be unique. For instance, the following is in error: + ```scala + val illFormed2 = (name = "", age = 0, name = true) // error + ``` + 3. Named tuples can be matched with either named or regular patterns. But regular tuples and other selector types can only be matched with regular tuple patterns. For instance, the following is in error: + ```scala + (tuple: Tuple) match + case (age = x) => // error + ``` + +### Syntax + +The syntax of Scala is extended as follows to support named tuples: +``` +SimpleType ::= ... + | ‘(’ NameAndType {‘,’ NameAndType} ‘)’ +NameAndType ::= id ':' Type + +SimpleExpr ::= ... + | '(' NamedExprInParens {‘,’ NamedExprInParens} ')' +NamedExprInParens ::= id '=' ExprInParens + +SimplePattern ::= ... + | '(' NamedPattern {‘,’ NamedPattern} ')' +NamedPattern ::= id '=' Pattern +``` diff --git a/docs/_docs/reference/syntax.md b/docs/_docs/reference/syntax.md index cebee26c34e6..8a4bbe99ccb9 100644 --- a/docs/_docs/reference/syntax.md +++ b/docs/_docs/reference/syntax.md @@ -198,7 +198,7 @@ SimpleType ::= SimpleLiteral | id | Singleton ‘.’ id | Singleton ‘.’ ‘type’ - | ‘(’ Types | NamesAndTypes ‘)’ + | ‘(’ [Types] ‘)’ | Refinement | SimpleType1 TypeArgs | SimpleType1 ‘#’ id @@ -216,8 +216,6 @@ Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> TypeBounds ::= [‘>:’ Type] [‘<:’ Type] TypeParamBounds ::= TypeBounds {‘:’ Type} Types ::= Type {‘,’ Type} -NamesAndTypes ::= NameAndType {‘,’ NameAndType} -NameAndType ::= id ':' Type ``` ### Expressions @@ -281,9 +279,7 @@ ExprSplice ::= spliceId | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern ExprsInParens ::= ExprInParens {‘,’ ExprInParens} - | NamedExprInParens {‘,’ NamedExprInParens} -ExprInParens ::= (PostfixExpr ‘:’ Type | Expr) -NamedExprInParens ::= id '=' ExprInParens +ExprInParens ::= PostfixExpr ‘:’ Type | Expr ParArgumentExprs ::= ‘(’ [ExprsInParens] ‘)’ | ‘(’ ‘using’ ExprsInParens ‘)’ | ‘(’ [ExprsInParens ‘,’] PostfixExpr ‘*’ ‘)’ @@ -334,8 +330,6 @@ SimplePattern1 ::= SimpleRef PatVar ::= varid | ‘_’ Patterns ::= Pattern {‘,’ Pattern} -NamedPatterns ::= NamedPattern {‘,’ NamedPattern} -NamedPattern ::= id '=' Pattern ArgumentPatterns ::= ‘(’ [Patterns] ‘)’ | ‘(’ [Patterns ‘,’] PatVar ‘*’ ‘)’ diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 65d7ac2f9ee4..e5b9e6a3438d 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -153,6 +153,7 @@ subsection: - page: reference/experimental/cc.md - page: reference/experimental/purefuns.md - page: reference/experimental/tupled-function.md + - page: reference/experimental/named-tuples.md - page: reference/syntax.md - title: Language Versions index: reference/language-versions/language-versions.md From 53201dad797338fc06e179cb3605fd95df46ce08 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 28 Nov 2023 09:55:18 +0100 Subject: [PATCH 12/15] Update experimental definitions list --- tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index 28788c24958e..37890e432dca 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -102,10 +102,6 @@ val experimentalDefinitionInLibrary = Set( // New feature: named tuples "scala.NamedTuple", "scala.NamedTuple$", - "scala.Tuple$.NamedValue", - "scala.Tuple$.NamedValue$", - - ) From a410f43614d869d62cff7a38cdb600569e934db1 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 28 Nov 2023 15:33:07 +0100 Subject: [PATCH 13/15] Two improvements: - Add unapply method to NamedTuple.Element - Avoid spurious refutability warning when matching a named tuple RHS against an unnamed pattern. --- compiler/src/dotty/tools/dotc/typer/Checking.scala | 12 +++++++++++- library/src/scala/NamedTuple.scala | 5 +++++ tests/pos/named-tuples.check | 10 ++++++++++ tests/run/named-tuples.scala | 14 ++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/pos/named-tuples.check diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 07085082cf3f..28ef384a150d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -944,7 +944,17 @@ trait Checking { false } - def check(pat: Tree, pt: Type): Boolean = (pt <:< pat.tpe) || fail(pat, pt, Reason.NonConforming) + def check(pat: Tree, pt: Type): Boolean = + // If pat is an unnamed tuple patterns, strip any named elements from `pt`. + // This could be avoided if we adapted in desugaring unnamed patterns + // with named scrutinees to be named instead. I did not follow that route + // because I fear that with the introduction of named pattern matching + // this case would be very common, and this alternative scheme would lead + // to a large amount of additional code that needs to be checked. + val normPt = pat.tpe.tupleElementTypesUpTo(1) match + case Some(defn.NamedTupleElem(_, _) :: _) => pt + case _ => pt.dropNamedTupleElems + (normPt <:< pat.tpe) || fail(pat, pt, Reason.NonConforming) def recur(pat: Tree, pt: Type): Boolean = !sourceVersion.isAtLeast(`3.2`) diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala index 2eca72016b69..32f341a98872 100644 --- a/library/src/scala/NamedTuple.scala +++ b/library/src/scala/NamedTuple.scala @@ -8,6 +8,10 @@ object NamedTuple: object Element: def apply[S <: String & Singleton, A](name: S, x: A): Element[name.type, A] = x + + inline def unapply[S <: String & Singleton, A](named: Element[S, A]): Some[(S, A)] = + Some((compiletime.constValue[S], named)) + def extract[S <: String & Singleton]: ValueExtractor[S] = ValueExtractor[S]() extension [S <: String & Singleton, A](named: Element[S, A]) def value: A = named @@ -22,3 +26,4 @@ object NamedTuple: extension [T <: Tuple](x: T) def dropNames: DropNames[T] = x.asInstanceOf // named and unnamed tuples have the same runtime representation end NamedTuple + diff --git a/tests/pos/named-tuples.check b/tests/pos/named-tuples.check new file mode 100644 index 000000000000..24928c7dbdac --- /dev/null +++ b/tests/pos/named-tuples.check @@ -0,0 +1,10 @@ +(Bob,33) +33 +Bob +(Bob,33,Lausanne,1003) +33 +no match +Bob is younger than Bill +Bob is younger than Lucy +Bill is younger than Lucy +matched elements (name, Bob), (age, 33) diff --git a/tests/run/named-tuples.scala b/tests/run/named-tuples.scala index f547578c7017..88235347ff8e 100644 --- a/tests/run/named-tuples.scala +++ b/tests/run/named-tuples.scala @@ -96,4 +96,18 @@ val _: CombinedInfo = bob ++ addr val x: Person = bob1 // bob1 still has type Person with the unswapped elements case _ => assert(false) + val (bobName, _) = bob + val _: String = bobName + + val bobNamed *: _ = bob + val _: NamedTuple.Element["name", String] = bobNamed + + import NamedTuple.* + val Element(nameStr, n) *: Element(ageStr, a) *: EmptyTuple = bob + println(s"matched elements ($nameStr, $n), ($ageStr, $a)") + + val Element(ageStr1, age) = bob(1) + assert(ageStr1 == "age" && age == 33) + + From 64e166c876cf1076566936fadbb9eb56bb347811 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 28 Nov 2023 18:10:27 +0100 Subject: [PATCH 14/15] Implement named pattern matching It's just a tiny change, once we have named tuples. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 45 ++++++++++--------- .../dotty/tools/dotc/parsing/Parsers.scala | 7 ++- .../dotty/tools/dotc/typer/Applications.scala | 15 ++++--- docs/_docs/internals/syntax.md | 4 +- tests/run/named-patterns.check | 10 +++++ tests/run/named-patterns.scala | 43 ++++++++++++++++++ tests/run/named-tuples.scala | 6 +-- 7 files changed, 93 insertions(+), 37 deletions(-) create mode 100644 tests/run/named-patterns.check create mode 100644 tests/run/named-patterns.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 573d82020b7f..0edd08af9cf3 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1475,7 +1475,9 @@ object desugar { */ def tuple(tree: Tuple, pt: Type)(using Context): (Tree, Type) = checkWellFormedTupleElems(tree.trees) - val (adapted, pt1) = adaptTupleElems(tree.trees, pt) + val (adapted, pt1) = + if ctx.mode.is(Mode.Pattern) then adaptPatternArgs(tree.trees, pt) + else (tree.trees, pt) val elems = adapted.mapConserve(desugarTupleElem) val arity = elems.length if arity <= Definitions.MaxTupleArity then @@ -1489,20 +1491,26 @@ object desugar { else (cpy.Tuple(tree)(elems), pt1) - /** When desugaring a pattern, adapt tuple elements `elems` and expected type `pt` - * to each other. This means: + /** 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, drop any tuple element names from `pt`. + * - If `elems` are unnamed elements, and `pt` is a named tuple, drop all + * 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): + def adaptPatternArgs(elems: List[Tree], pt: Type)(using Context): (List[Tree], Type) = + + def reorderedNamedArgs(wildcardSpan: Span): List[untpd.Tree] = + val nameIdx = pt.tupleElementTypes match + case Some(selElems) => + for case (defn.NamedTupleElem(name, _), idx) <- selElems.zipWithIndex yield + (name, idx) + case None => + val cls = pt.classSymbol + if cls.is(CaseClass) then cls.caseAccessors.map(_.name).zipWithIndex + else Nil + val nameToIdx = nameIdx.toMap + val reordered = Array.fill[untpd.Tree](nameIdx.length): untpd.Ident(nme.WILDCARD).withSpan(wildcardSpan) for case arg @ NamedArg(name, _) <- elems do nameToIdx.get(name) match @@ -1515,15 +1523,10 @@ object desugar { 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 + elems match + case (first @ NamedArg(_, _)) :: _ => (reorderedNamedArgs(first.span.startPos), pt) + case _ => (elems, pt.dropNamedTupleElems) + end adaptPatternArgs private def desugarTupleElem(elem: Tree)(using Context): Tree = elem match case NamedArg(name, arg) => diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 221bb78474d3..64bc430dd569 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3072,13 +3072,12 @@ object Parsers { p /** Patterns ::= Pattern [`,' Pattern] - * NamedPatterns ::= NamedPattern {‘,’ NamedPattern} + * | NamedPattern {‘,’ NamedPattern} * NamedPattern ::= id '=' Pattern */ def patterns(location: Location = Location.InPattern): List[Tree] = - val pat = () => pattern(location) - commaSeparated( // TODO: Drop the distinction once we allow named argument patterns - if location == Location.InPattern then maybeNamed(pat) else pat) + commaSeparated(maybeNamed(() => pattern(location))) + // check that patterns are all named or all unnamed is done at desugaring def patternsOpt(location: Location = Location.InPattern): List[Tree] = if (in.token == RPAREN) Nil else patterns(location) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 03824a398ed5..9d9841b3cbb7 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1283,7 +1283,7 @@ trait Applications extends Compatibility { def typedUnApply(tree: untpd.Apply, selType: Type)(using Context): Tree = { record("typedUnApply") - val Apply(qual, args) = tree + val Apply(qual, unadaptedArgs) = tree def notAnExtractor(tree: Tree): Tree = // prefer inner errors @@ -1461,12 +1461,17 @@ trait Applications extends Compatibility { loop(unapp) res.result() } + val (args, unappResultType) = desugar.adaptPatternArgs(unadaptedArgs, unapplyApp.tpe) - var argTypes = unapplyArgs(unapplyApp.tpe, unapplyFn, args, tree.srcPos) - for (argType <- argTypes) assert(!isBounds(argType), unapplyApp.tpe.show) + var argTypes = unapplyArgs(unappResultType, unapplyFn, args, tree.srcPos) + unapp.println(i"unapplyArgs = $unappResultType with $argTypes / $args") + for (argType <- argTypes) assert(!isBounds(argType), unappResultType.show) val bunchedArgs = argTypes match { case argType :: Nil => - if (args.lengthCompare(1) > 0 && Feature.autoTuplingEnabled && defn.isTupleNType(argType)) untpd.Tuple(args) :: Nil + if args.lengthCompare(1) > 0 + && Feature.autoTuplingEnabled + && defn.isTupleNType(argType) + then untpd.Tuple(args) :: Nil else args case _ => args } @@ -1482,7 +1487,7 @@ trait Applications extends Compatibility { else tryWithTypeTest(Typed(result, TypeTree(ownType)), selType) case tp => val unapplyErr = if (tp.isError) unapplyFn else notAnExtractor(unapplyFn) - val typedArgsErr = args.mapconserve(typed(_, defn.AnyType)) + val typedArgsErr = unadaptedArgs.mapconserve(typed(_, defn.AnyType)) cpy.UnApply(tree)(unapplyErr, Nil, typedArgsErr) withType unapplyErr.tpe } } diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index ba1213517944..914f920a26fe 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -325,7 +325,7 @@ Pattern2 ::= [id ‘@’] InfixPattern InfixPattern ::= SimplePattern { id [nl] SimplePattern } InfixOp(pat, op, pat) SimplePattern ::= PatVar Ident(wildcard) | Literal Bind(name, Ident(wildcard)) - | ‘(’ [Patterns | NamedPatterns] ‘)’ Parens(pats) Tuple(pats) + | ‘(’ [Patterns] ‘)’ Parens(pats) Tuple(pats) | Quoted | XmlPattern (to be dropped) | SimplePattern1 [TypeArgs] [ArgumentPatterns] @@ -335,7 +335,7 @@ SimplePattern1 ::= SimpleRef PatVar ::= varid | ‘_’ Patterns ::= Pattern {‘,’ Pattern} -NamedPatterns ::= NamedPattern {‘,’ NamedPattern} + | NamedPattern {‘,’ NamedPattern} NamedPattern ::= id '=' Pattern ArgumentPatterns ::= ‘(’ [Patterns] ‘)’ Apply(fn, pats) diff --git a/tests/run/named-patterns.check b/tests/run/named-patterns.check new file mode 100644 index 000000000000..ba8dbb8b21f7 --- /dev/null +++ b/tests/run/named-patterns.check @@ -0,0 +1,10 @@ +name Bob, age 22 +name Bob +age 22 +age 22, name Bob +Bob, 22 +1003 Lausanne, Rue de la Gare 44 +1003 Lausanne +Rue de la Gare in Lausanne +1003 Lausanne, Rue de la Gare 44 +1003 Lausanne, Rue de la Gare 44 diff --git a/tests/run/named-patterns.scala b/tests/run/named-patterns.scala new file mode 100644 index 000000000000..1e7e0697e782 --- /dev/null +++ b/tests/run/named-patterns.scala @@ -0,0 +1,43 @@ +import language.experimental.namedTuples + +object Test1: + class Person(val name: String, val age: Int) + + object Person: + def unapply(p: Person): (name: String, age: Int) = (p.name, p.age) + + case class Address(city: String, zip: Int, street: String, number: Int) + + @main def Test = + val bob = Person("Bob", 22) + bob match + case Person(name = n, age = a) => println(s"name $n, age $a") + bob match + case Person(name = n) => println(s"name $n") + bob match + case Person(age = a) => println(s"age $a") + bob match + case Person(age = a, name = n) => println(s"age $a, name $n") + bob match + case Person(age, name) => println(s"$age, $name") + + val addr = Address("Lausanne", 1003, "Rue de la Gare", 44) + addr match + case Address(city = c, zip = z, street = s, number = n) => + println(s"$z $c, $s $n") + addr match + case Address(zip = z, city = c) => + println(s"$z $c") + addr match + case Address(city = c, street = s) => + println(s"$s in $c") + addr match + case Address(number = n, street = s, zip = z, city = c) => + println(s"$z $c, $s $n") + addr match + case Address(c, z, s, number) => + println(s"$z $c, $s $number") + + + + diff --git a/tests/run/named-tuples.scala b/tests/run/named-tuples.scala index 88235347ff8e..52a9dee5897c 100644 --- a/tests/run/named-tuples.scala +++ b/tests/run/named-tuples.scala @@ -102,11 +102,7 @@ val _: CombinedInfo = bob ++ addr val bobNamed *: _ = bob val _: NamedTuple.Element["name", String] = bobNamed - import NamedTuple.* - val Element(nameStr, n) *: Element(ageStr, a) *: EmptyTuple = bob - println(s"matched elements ($nameStr, $n), ($ageStr, $a)") - - val Element(ageStr1, age) = bob(1) + val NamedTuple.Element(ageStr1, age) = bob(1) assert(ageStr1 == "age" && age == 33) From 05a64bc37ccf2a7e152ae03fc34bd3e642c0c849 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 30 Nov 2023 14:45:50 +0100 Subject: [PATCH 15/15] Test demonstrating a useful operation on named tuples Shows how UpdateWith on named tuples can be implemented on the tpe level. --- .../test/dotc/pos-test-pickling.blacklist | 1 + tests/pos/NamedTupleOps.scala | 136 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 tests/pos/NamedTupleOps.scala diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 67ee566de076..50a220615b07 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -62,6 +62,7 @@ tuple-fold.scala mt-redux-norm.perspective.scala i18211.scala named-tuples1.scala +NamedTupleOps.scala # Opaque type i5720.scala diff --git a/tests/pos/NamedTupleOps.scala b/tests/pos/NamedTupleOps.scala new file mode 100644 index 000000000000..2552e26a2b11 --- /dev/null +++ b/tests/pos/NamedTupleOps.scala @@ -0,0 +1,136 @@ +import compiletime.* +import compiletime.ops.int.* + +object TupleOps: + + /** The index of `Y` in tuple `X`, or `Size[X]` if `Y` does not occur in `X` */ + type IndexOf[X <: Tuple, Y] <: Int = X match + case Y *: _ => 0 + case x *: xs => S[IndexOf[xs, Y]] + case EmptyTuple => 0 + + /** The `X` tuple, with its element at index `N` replaced by `Y`. + * If `N` is not an index of `X`, the element `Y` is appended instead + */ + type UpdateOrAppend[X <: Tuple, N <: Int, Y] <: Tuple = X match + case x *: xs => + N match + case 0 => Y *: xs + case S[n1] => x *: UpdateOrAppend[xs, n1, Y] + case EmptyTuple => Y *: EmptyTuple + + inline def updateOrAppend[X <: Tuple, N <: Int, Y](xs: X, y: Y): UpdateOrAppend[X, N, Y] = + def recur(xs: Tuple, n: Int): Tuple = xs match + case x *: xs1 => + if n == 0 then y *: xs1 else x *: recur(xs1, n - 1) + case EmptyTuple => + y *: EmptyTuple + recur(xs, constValue[N]).asInstanceOf[UpdateOrAppend[X, N, Y]] + + /** If `Y` does not occur in tuple `X`, `X` with `Y` appended. Otherwise `X`. */ + type AppendIfDistinct[X <: Tuple, Y] <: Tuple = X match + case Y *: xs => X + case x *: xs => x *: AppendIfDistinct[xs, Y] + case EmptyTuple => Y *: EmptyTuple + + /** `X` with all elements from `Y` that do not occur in `X` appended */ + type ConcatDistinct[X <: Tuple, Y <: Tuple] <: Tuple = Y match + case y *: ys => ConcatDistinct[AppendIfDistinct[X, y], ys] + case EmptyTuple => X + + // TODO: Implement appendIfDistinct, concatDistinct + +object NamedTupleOps: + import TupleOps.* + + opaque type NamedTuple[N <: Tuple, +X <: Tuple] >: X = X + + /** The names of the named tuple type `NT` */ + type Names[NT] <: Tuple = NT match + case NamedTuple[n, _] => n + + /** The value types of the named tuple type `NT` */ + type DropNames[NT] <: Tuple = NT match + case NamedTuple[_, x] => x + + extension [N <: Tuple, X <: Tuple](x: NamedTuple[N, X]) + transparent inline def dropNames: X = x.asInstanceOf + transparent inline def names: N = ??? + + /** Internal use only: Merge names and value components of two named tuple to + * impement `UpdateWith`. + * @param N the names of the combined tuple + * @param X the value types of the first named tuple + * @param N2 the names of the second named tuple + * @param Y the value types of the second named tuple + */ + type Merge[N <: Tuple, X <: Tuple, N2 <: Tuple, Y <: Tuple] = (N2, Y) match + case (n *: ns, y *: ys) => + Merge[N, UpdateOrAppend[X, IndexOf[N, n], y], ns, ys] + case (EmptyTuple, EmptyTuple) => + NamedTuple[N, X] + + /** A joint named tuple where + * - The names are the names of named tuple `NT1` followed by those names of `NT2` which + * do not appear in `NT1` + * - The values are the values of `NT1` and `NT2` corresponding to these names. + * If a name is present in both `NT1` and `NT2` the value in `NT2` is used. + */ + type UpdateWith[NT1, NT2] = + Merge[ConcatDistinct[Names[NT1], Names[NT2]], DropNames[NT1], Names[NT2], DropNames[NT2]] + + // TODO: Implement merge, updateWith + +@main def Test = + import TupleOps.* + import NamedTupleOps.* + + type Names = "first" *: "last" *: "age" *: EmptyTuple + type Values = "Bob" *: "Miller" *: 33 *: EmptyTuple + + val names: Names = ("first", "last", "age") + val values: Values = ("Bob", "Miller", 33) + + val x1: IndexOf[Names, "first"] = constValue + val _: 0 = x1 + + val x2: IndexOf[Names, "age"] = constValue + val _: 2 = x2 + + val x3: IndexOf[Names, "what?"] = constValue + val _: 3 = x3 + + type Releases = "first" *: "middle" *: EmptyTuple + type ReleaseValues = 1.0 *: true *: EmptyTuple + + val x4: UpdateOrAppend[Values, IndexOf[Names, "age"], 11] = + updateOrAppend[Values, IndexOf[Names, "age"], 11](values, 11) + val _: ("Bob", "Miller", 11) = x4 + assert(("Bob", "Miller", 11) == x4) + + val x5: UpdateOrAppend[Values, IndexOf[Names, "what"], true] = + updateOrAppend[Values, IndexOf[Names, "what"], true](values, true) + val _: ("Bob", "Miller", 33, true) = x5 + assert(("Bob", "Miller", 33, true) == x5) + + val x6: UpdateOrAppend[Values, IndexOf[Names, "first"], "Peter"] = + updateOrAppend[Values, IndexOf[Names, "first"], "Peter"](values, "Peter") + val _: ("Peter", "Miller", 33) = x6 + assert(("Peter", "Miller", 33) == x6) + + val x7: ConcatDistinct[Names, Releases] = ??? + val _: ("first", "last", "age", "middle") = x7 + + val x8: ConcatDistinct[Releases, Names] = ??? + val _: ("first", "middle", "last", "age") = x8 + + val x9: Merge[ConcatDistinct[Names, Releases], Values, Releases, ReleaseValues] = ??? + val _: NamedTuple[("first", "last", "age", "middle"), (1.0, "Miller", 33, true)] = x9 + + val x10: UpdateWith[NamedTuple[Names, Values], NamedTuple[Releases, ReleaseValues]] = ??? + val _: ("first", "last", "age", "middle") = x10.names + val _: (1.0, "Miller", 33, true) = x10.dropNames + + val x11: UpdateWith[NamedTuple[Releases, ReleaseValues], NamedTuple[Names, Values]] = ??? + val _: NamedTuple[("first", "middle", "last", "age"), ("Bob", true, "Miller", 33)] = x11 +