diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 03505075121a..1801a7fada7c 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 @@ -234,7 +234,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 @@ -1254,8 +1254,9 @@ object desugar { pats.forall(isVarPattern) case _ => false } + val isMatchingTuple: Tree => Boolean = { - case Tuple(es) => isTuplePattern(es.length) + case Tuple(es) => isTuplePattern(es.length) && !hasNamedArg(es) case _ => false } @@ -1441,22 +1442,99 @@ object desugar { AppliedTypeTree( TypeTree(defn.throwsAlias.typeRef).withSpan(op.span), tpt :: excepts :: Nil) - /** Translate tuple expressions of arity <= 22 + private def checkWellFormedTupleElems(elems: List[Tree])(using Context): List[Tree] = + val seen = mutable.Set[Name]() + for case arg @ NamedArg(name, _) <- elems do + if seen.contains(name) then + report.error(em"Duplicate tuple element name", arg.srcPos) + seen += name + if name.startsWith("_") && name.toString.tail.toIntOption.isDefined then + report.error( + em"$name cannot be used as the name of a tuple element because it is a regular tuple selector", + arg.srcPos) + + elems match + case elem :: elems1 => + val mismatchOpt = + if elem.isInstanceOf[NamedArg] + then elems1.find(!_.isInstanceOf[NamedArg]) + else elems1.find(_.isInstanceOf[NamedArg]) + mismatchOpt match + case Some(misMatch) => + report.error(em"Illegal combination of named and unnamed tuple elements", misMatch.srcPos) + elems.mapConserve(stripNamedArg) + case None => elems + case _ => elems + end checkWellFormedTupleElems + + /** Translate tuple expressions * * () ==> () * (t) ==> t * (t1, ..., tN) ==> TupleN(t1, ..., tN) */ - def smallTuple(tree: Tuple)(using Context): Tree = { - val ts = tree.trees - val arity = ts.length - assert(arity <= Definitions.MaxTupleArity) - def tupleTypeRef = defn.TupleType(arity).nn - if (arity == 0) - if (ctx.mode is Mode.Type) TypeTree(defn.UnitType) else unitLiteral - else if (ctx.mode is Mode.Type) AppliedTypeTree(ref(tupleTypeRef), ts) - else Apply(ref(tupleTypeRef.classSymbol.companionModule.termRef), ts) - } + def tuple(tree: Tuple, pt: Type)(using Context): Tree = + var elems = checkWellFormedTupleElems(tree.trees) + if ctx.mode.is(Mode.Pattern) then elems = adaptPatternArgs(elems, pt) + val elemValues = elems.mapConserve(stripNamedArg) + val tup = + val arity = elems.length + if arity <= Definitions.MaxTupleArity then + def tupleTypeRef = defn.TupleType(arity).nn + val tree1 = + if arity == 0 then + if ctx.mode is Mode.Type then TypeTree(defn.UnitType) else unitLiteral + else if ctx.mode is Mode.Type then AppliedTypeTree(ref(tupleTypeRef), elemValues) + else Apply(ref(tupleTypeRef.classSymbol.companionModule.termRef), elemValues) + tree1.withSpan(tree.span) + else + cpy.Tuple(tree)(elemValues) + val names = elems.collect: + case NamedArg(name, arg) => name + if names.isEmpty || ctx.mode.is(Mode.Pattern) then + tup + else + def namesTuple = withModeBits(ctx.mode &~ Mode.Pattern | Mode.Type): + tuple(Tuple( + names.map: name => + SingletonTypeTree(Literal(Constant(name.toString))).withSpan(tree.span)), + WildcardType) + if ctx.mode.is(Mode.Type) then + AppliedTypeTree(ref(defn.NamedTupleTypeRef), namesTuple :: tup :: Nil) + else + TypeApply( + Apply(Select(ref(defn.NamedTupleModule), nme.withNames), tup), + namesTuple :: Nil) + + /** When desugaring a list pattern arguments `elems` adapt them and the + * expected type `pt` to each other. This means: + * - If `elems` are named pattern elements, rearrange them to match `pt`. + * This requires all names in `elems` to be also present in `pt`. + */ + def adaptPatternArgs(elems: List[Tree], pt: Type)(using Context): List[Tree] = + + def reorderedNamedArgs(wildcardSpan: Span): List[untpd.Tree] = + var selNames = pt.namedTupleElementTypes.map(_(0)) + if selNames.isEmpty && pt.classSymbol.is(CaseClass) then + selNames = pt.classSymbol.caseAccessors.map(_.name.asTermName) + val nameToIdx = selNames.zipWithIndex.toMap + val reordered = Array.fill[untpd.Tree](selNames.length): + untpd.Ident(nme.WILDCARD).withSpan(wildcardSpan) + for case arg @ NamedArg(name: TermName, _) <- elems do + nameToIdx.get(name) match + case Some(idx) => + if reordered(idx).isInstanceOf[Ident] then + reordered(idx) = arg + else + report.error(em"Duplicate named pattern", arg.srcPos) + case _ => + report.error(em"No element named `$name` is defined in selector type $pt", arg.srcPos) + reordered.toList + + elems match + case (first @ NamedArg(_, _)) :: _ => reorderedNamedArgs(first.span.startPos) + case _ => elems + end adaptPatternArgs private def isTopLevelDef(stat: Tree)(using Context): Boolean = stat match case _: ValDef | _: PatDef | _: DefDef | _: Export | _: ExtMethods => true @@ -1990,7 +2068,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/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 34c87eedb081..941e7b8f1219 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -108,6 +108,10 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] => case _ => tree + def stripNamedArg(tree: Tree) = tree match + case NamedArg(_, arg) => arg + case _ => tree + /** The number of arguments in an application */ def numArgs(tree: Tree): Int = unsplice(tree) match { case Apply(fn, args) => numArgs(fn) + args.length diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 08f3db4981ff..0dfe52c421d9 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -107,7 +107,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def forwardTo: Tree = t } case class Tuple(trees: List[Tree])(implicit @constructorOnly src: SourceFile) extends Tree { - override def isTerm: Boolean = trees.isEmpty || trees.head.isTerm + override def isTerm: Boolean = trees.isEmpty || stripNamedArg(trees.head).isTerm override def isType: Boolean = !isTerm } case class Throw(expr: Tree)(implicit @constructorOnly src: SourceFile) extends TermTree @@ -528,15 +528,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/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 4852eaba9334..1fe9cae936c9 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") def experimentalAutoEnableFeatures(using Context): List[TermName] = defn.languageExperimentalFeatures diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 29cb83000fde..d0c30a665289 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -95,14 +95,14 @@ object Contexts { inline def atPhaseNoEarlier[T](limit: Phase)(inline op: Context ?=> T)(using Context): T = op(using if !limit.exists || limit <= ctx.phase then ctx else ctx.withPhase(limit)) - inline private def inMode[T](mode: Mode)(inline op: Context ?=> T)(using ctx: Context): T = + inline def withModeBits[T](mode: Mode)(inline op: Context ?=> T)(using ctx: Context): T = op(using if mode != ctx.mode then ctx.fresh.setMode(mode) else ctx) inline def withMode[T](mode: Mode)(inline op: Context ?=> T)(using ctx: Context): T = - inMode(ctx.mode | mode)(op) + withModeBits(ctx.mode | mode)(op) inline def withoutMode[T](mode: Mode)(inline op: Context ?=> T)(using ctx: Context): T = - inMode(ctx.mode &~ mode)(op) + withModeBits(ctx.mode &~ mode)(op) /** A context is passed basically everywhere in dotc. * This is convenient but carries the risk of captured contexts in diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 675084ec230b..15880207b3c8 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -949,6 +949,9 @@ class Definitions { def TupleXXL_fromIterator(using Context): Symbol = TupleXXLModule.requiredMethod("fromIterator") def TupleXXL_unapplySeq(using Context): Symbol = TupleXXLModule.requiredMethod(nme.unapplySeq) + @tu lazy val NamedTupleModule = requiredModule("scala.NamedTuple") + @tu lazy val NamedTupleTypeRef: TypeRef = NamedTupleModule.termRef.select(tpnme.NamedTuple).asInstanceOf + @tu lazy val RuntimeTupleMirrorTypeRef: TypeRef = requiredClassRef("scala.runtime.TupleMirror") @tu lazy val RuntimeTuplesModule: Symbol = requiredModule("scala.runtime.Tuples") @@ -1304,9 +1307,20 @@ class Definitions { case ByNameFunction(_) => true case _ => false + object NamedTuple: + def apply(nmes: Type, vals: Type)(using Context): Type = + AppliedType(NamedTupleTypeRef, nmes :: vals :: Nil) + def unapply(t: Type)(using Context): Option[(Type, Type)] = t match + case AppliedType(tycon, nmes :: vals :: Nil) if tycon.typeSymbol == NamedTupleTypeRef.symbol => + Some((nmes, vals)) + case _ => None + final def isCompiletime_S(sym: Symbol)(using Context): Boolean = sym.name == tpnme.S && sym.owner == CompiletimeOpsIntModuleClass + final def isNamedTuple_From(sym: Symbol)(using Context): Boolean = + sym.name == tpnme.From && sym.owner == NamedTupleModule.moduleClass + private val compiletimePackageAnyTypes: Set[Name] = Set( tpnme.Equals, tpnme.NotEquals, tpnme.IsConst, tpnme.ToString ) @@ -1335,7 +1349,7 @@ class Definitions { tpnme.Plus, tpnme.Length, tpnme.Substring, tpnme.Matches, tpnme.CharAt ) private val compiletimePackageOpTypes: Set[Name] = - Set(tpnme.S) + Set(tpnme.S, tpnme.From) ++ compiletimePackageAnyTypes ++ compiletimePackageIntTypes ++ compiletimePackageLongTypes @@ -1348,6 +1362,7 @@ class Definitions { compiletimePackageOpTypes.contains(sym.name) && ( isCompiletime_S(sym) + || isNamedTuple_From(sym) || sym.owner == CompiletimeOpsAnyModuleClass && compiletimePackageAnyTypes.contains(sym.name) || sym.owner == CompiletimeOpsIntModuleClass && compiletimePackageIntTypes.contains(sym.name) || sym.owner == CompiletimeOpsLongModuleClass && compiletimePackageLongTypes.contains(sym.name) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 9772199678d7..62d7afa22ed2 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -362,6 +362,8 @@ object StdNames { val EnumValue: N = "EnumValue" val ExistentialTypeTree: N = "ExistentialTypeTree" val Flag : N = "Flag" + val Fields: N = "Fields" + val From: N = "From" val Ident: N = "Ident" val Import: N = "Import" val Literal: N = "Literal" @@ -374,6 +376,7 @@ object StdNames { val MirroredMonoType: N = "MirroredMonoType" val MirroredType: N = "MirroredType" val Modifiers: N = "Modifiers" + val NamedTuple: N = "NamedTuple" val NestedAnnotArg: N = "NestedAnnotArg" val NoFlags: N = "NoFlags" val NoPrefix: N = "NoPrefix" @@ -620,6 +623,7 @@ object StdNames { val throws: N = "throws" val toArray: N = "toArray" val toList: N = "toList" + val toTuple: N = "toTuple" val toObjectArray : N = "toObjectArray" val toSeq: N = "toSeq" val toString_ : N = "toString" @@ -649,6 +653,7 @@ object StdNames { val wildcardType: N = "wildcardType" val withFilter: N = "withFilter" val withFilterIfRefutable: N = "withFilterIfRefutable$" + val withNames: N = "withNames" val WorksheetWrapper: N = "WorksheetWrapper" val wrap: N = "wrap" val writeReplace: N = "writeReplace" diff --git a/compiler/src/dotty/tools/dotc/core/TypeEval.scala b/compiler/src/dotty/tools/dotc/core/TypeEval.scala index b5684b07f181..af4f1e0153dd 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeEval.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeEval.scala @@ -6,11 +6,14 @@ import Types.*, Contexts.*, Symbols.*, Constants.*, Decorators.* import config.Printers.typr import reporting.trace import StdNames.tpnme +import Flags.CaseClass +import TypeOps.nestedPairs object TypeEval: def tryCompiletimeConstantFold(tp: AppliedType)(using Context): Type = tp.tycon match case tycon: TypeRef if defn.isCompiletimeAppliedType(tycon.symbol) => + extension (tp: Type) def fixForEvaluation: Type = tp.normalized.dealias match // enable operations for constant singleton terms. E.g.: @@ -94,6 +97,22 @@ object TypeEval: throw TypeError(em"${e.getMessage.nn}") ConstantType(Constant(result)) + def fieldsOf: Option[Type] = + expectArgsNum(1) + val arg = tp.args.head + val cls = arg.classSymbol + if cls.is(CaseClass) then + val fields = cls.caseAccessors + val fieldLabels = fields.map: field => + ConstantType(Constant(field.name.toString)) + val fieldTypes = fields.map(arg.memberInfo) + Some: + defn.NamedTupleTypeRef.appliedTo: + nestedPairs(fieldLabels) :: nestedPairs(fieldTypes) :: Nil + else arg.widenDealias match + case arg @ defn.NamedTuple(_, _) => Some(arg) + case _ => None + def constantFold1[T](extractor: Type => Option[T], op: T => Any): Option[Type] = expectArgsNum(1) extractor(tp.args.head).map(a => runConstantOp(op(a))) @@ -122,11 +141,14 @@ object TypeEval: yield runConstantOp(op(a, b, c)) trace(i"compiletime constant fold $tp", typr, show = true) { - val name = tycon.symbol.name - val owner = tycon.symbol.owner + val sym = tycon.symbol + val name = sym.name + val owner = sym.owner val constantType = - if defn.isCompiletime_S(tycon.symbol) then + if defn.isCompiletime_S(sym) then constantFold1(natValue, _ + 1) + else if defn.isNamedTuple_From(sym) then + fieldsOf else if owner == defn.CompiletimeOpsAnyModuleClass then name match case tpnme.Equals => constantFold2(constValue, _ == _) case tpnme.NotEquals => constantFold2(constValue, _ != _) diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 012464f71d9b..8461c0f091fe 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -385,7 +385,12 @@ object TypeOps: (tp.tp1.dealias, tp.tp2.dealias) match case (tp1 @ AppliedType(tycon1, args1), tp2 @ AppliedType(tycon2, args2)) if tycon1.typeSymbol == tycon2.typeSymbol && (tycon1 =:= tycon2) => - mergeRefinedOrApplied(tp1, tp2) + mergeRefinedOrApplied(tp1, tp2) match + case tp: AppliedType if tp.isUnreducibleWild => + // fall back to or-dominators rather than inferring a type that would + // cause an unreducible type error later. + approximateOr(tp1, tp2) + case tp => tp case (tp1, tp2) => approximateOr(tp1, tp2) case _ => diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index a3d6ab065a77..d4be03e9aae4 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -4,9 +4,11 @@ package core import TypeErasure.ErasedValueType import Types.*, Contexts.*, Symbols.*, Flags.*, Decorators.* -import Names.Name +import Names.{Name, TermName} +import Constants.Constant class TypeUtils { + /** A decorator that provides methods on types * that are needed in the transformer pipeline. */ @@ -65,8 +67,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 @@ -114,6 +120,34 @@ class TypeUtils { case Some(types) => TypeOps.nestedPairs(types) case None => throw new AssertionError("not a tuple") + def namedTupleElementTypesUpTo(bound: Int, normalize: Boolean = true)(using Context): List[(TermName, Type)] = + (if normalize then self.normalized else self).dealias match + case defn.NamedTuple(nmes, vals) => + val names = nmes.tupleElementTypesUpTo(bound, normalize).getOrElse(Nil).map: + case ConstantType(Constant(str: String)) => str.toTermName + case t => throw TypeError(em"Malformed NamedTuple: names must be string types, but $t was found.") + val values = vals.tupleElementTypesUpTo(bound, normalize).getOrElse(Nil) + names.zip(values) + case t => + Nil + + def namedTupleElementTypes(using Context): List[(TermName, Type)] = + namedTupleElementTypesUpTo(Int.MaxValue) + + def isNamedTupleType(using Context): Boolean = self match + case defn.NamedTuple(_, _) => true + case _ => false + + /** Drop all named elements in tuple type */ + def stripNamedTuple(using Context): Type = self.normalized.dealias match + case defn.NamedTuple(_, vals) => + vals + case self @ AnnotatedType(tp, annot) => + val tp1 = tp.stripNamedTuple + if tp1 ne tp then AnnotatedType(tp1, annot) else self + case _ => + self + def refinedWith(name: Name, info: Type)(using Context) = RefinedType(self, name, info) /** Is this type a methodic type that takes at least one parameter? */ diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 2f3daa79fb07..e1f355f30c40 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -646,6 +646,14 @@ object Parsers { ts.toList else leading :: Nil + def maybeNamed(op: () => Tree): () => Tree = () => + if isIdent && in.lookahead.token == EQUALS && in.featureEnabled(Feature.namedTuples) 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) @@ -1644,7 +1652,14 @@ object Parsers { && in.featureEnabled(Feature.into) && canStartTypeTokens.contains(in.lookahead.token) - 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)).withSpan(t.span) + case _ => t + if in.token == LPAREN then in.nextToken() if in.token == RPAREN then @@ -1660,7 +1675,6 @@ object Parsers { in.currentRegion.withCommasExpected: funArgType() match case Ident(name) if name != tpnme.WILDCARD && in.isColon => - isValParamList = true def funParam(start: Offset, mods: Modifiers) = atSpan(start): addErased() @@ -1698,12 +1712,13 @@ object Parsers { cpy.Function(arg)(args, sanitize(res)) case arg => arg - val args1 = args.mapConserve(sanitize) - if isValParamList || in.isArrow || isPureArrow then + + if in.isArrow || isPureArrow || erasedArgs.contains(true) then functionRest(args) else - val tuple = atSpan(start)(makeTupleOrParens(args1)) + val tuple = atSpan(start): + makeTupleOrParens(args.mapConserve(convertToElem)) typeRest: infixTypeRest: refinedTypeRest: @@ -1979,6 +1994,7 @@ object Parsers { * | Singleton `.' id * | Singleton `.' type * | ‘(’ ArgTypes ‘)’ + * | ‘(’ NamesAndTypes ‘)’ * | Refinement * | TypeSplice -- deprecated syntax (since 3.0.0) * | SimpleType1 TypeArgs @@ -1987,7 +2003,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)) } @@ -2070,32 +2086,33 @@ 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() = { - val name = ident() - accept(EQUALS) - NamedArg(name.toTypeName, argType()) - } + def namedArgType() = + atSpan(in.offset): + 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() = + atSpan(in.offset): + val name = ident() + acceptColon() + NamedArg(name, argType()) + + if namedOK && isIdent && in.lookahead.token == EQUALS then + commaSeparated(() => namedArgType()) + else if tupleOK && isIdent && in.lookahead.isColon && in.featureEnabled(Feature.namedTuples) then + commaSeparated(() => namedElem()) + else + commaSeparated(() => argType()) + end argTypes def paramTypeOf(core: () => Tree): Tree = if in.token == ARROW || isPureArrow(nme.PUREARROW) then @@ -2142,7 +2159,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 `}' */ @@ -2719,7 +2736,9 @@ object Parsers { } /** ExprsInParens ::= ExprInParens {`,' ExprInParens} + * | NamedExprInParens {‘,’ NamedExprInParens} * Bindings ::= Binding {`,' Binding} + * NamedExprInParens ::= id '=' ExprInParens */ def exprsInParensOrBindings(): List[Tree] = if in.token == RPAREN then Nil @@ -2729,7 +2748,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) @@ -3083,7 +3102,7 @@ object Parsers { * | Literal * | Quoted * | XmlPattern - * | `(' [Patterns] `)' + * | `(' [Patterns | NamedPatterns] `)' * | SimplePattern1 [TypeArgs] [ArgumentPatterns] * | ‘given’ RefinedType * SimplePattern1 ::= SimpleRef @@ -3134,9 +3153,12 @@ object Parsers { p /** Patterns ::= Pattern [`,' Pattern] + * | NamedPattern {‘,’ NamedPattern} + * NamedPattern ::= id '=' Pattern */ def patterns(location: Location = Location.InPattern): List[Tree] = - commaSeparated(() => pattern(location)) + 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/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 241bfb4f7c7b..87f7c88e0407 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -69,7 +69,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 _ => @@ -121,16 +122,17 @@ 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 arg => toText(arg) /** Pretty-print comma-separated type arguments for a constructor to be inserted among parentheses or brackets * (hence with `GlobalPrec` precedence). */ protected def argsText(args: List[Type]): Text = - atPrec(GlobalPrec) { Text(args.map(arg => argText(arg) ), ", ") } + atPrec(GlobalPrec) { Text(args.map(argText(_)), ", ") } /** The longest sequence of refinement types, starting at given type * and following parents. diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index e84cbc7c50d5..0329f0639d87 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -205,6 +205,11 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { def toTextTuple(args: List[Type]): Text = "(" ~ argsText(args) ~ ")" + def toTextNamedTuple(elems: List[(TermName, Type)]): Text = + val elemsText = atPrec(GlobalPrec): + Text(elems.map((name, tp) => toText(name) ~ " : " ~ argText(tp)), ", ") + "(" ~ elemsText ~ ")" + def isInfixType(tp: Type): Boolean = tp match case AppliedType(tycon, args) => args.length == 2 @@ -239,8 +244,16 @@ 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) + val namedElems = + try tp.namedTupleElementTypesUpTo(200, normalize = false) + catch case ex: TypeError => Nil + if namedElems.nonEmpty then + toTextNamedTuple(namedElems) + else tp.tupleElementTypesUpTo(200, normalize = false) match + //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) ~ "*" @@ -490,7 +503,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 => @@ -559,7 +572,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { changePrec(AndTypePrec) { toText(args(0)) ~ " & " ~ atPrec(AndTypePrec + 1) { toText(args(1)) } } else if defn.isFunctionSymbol(tpt.symbol) && tpt.isInstanceOf[TypeTree] && tree.hasType && !printDebug - then changePrec(GlobalPrec) { toText(tree.typeOpt) } + then + changePrec(GlobalPrec) { toText(tree.typeOpt) } + else if tpt.symbol == defn.NamedTupleTypeRef.symbol + && !printDebug && tree.typeOpt.exists + then + toText(tree.typeOpt) else args match case arg :: _ if arg.isTerm => toTextLocal(tpt) ~ "(" ~ Text(args.map(argText), ", ") ~ ")" diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index bed29a122399..0b8507f3b6c7 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -112,8 +112,13 @@ object PatternMatcher { sanitize(tpe), coord = rhs.span) // TODO: Drop Case once we use everywhere else `isPatmatGenerated`. + private def dropNamedTuple(tree: Tree): Tree = + val tpe = tree.tpe.widen + if tpe.isNamedTupleType then tree.cast(tpe.stripNamedTuple) else tree + /** The plan `let x = rhs in body(x)` where `x` is a fresh variable */ - private def letAbstract(rhs: Tree, tpe: Type = NoType)(body: Symbol => Plan): Plan = { + private def letAbstract(rhs0: Tree, tpe: Type = NoType)(body: Symbol => Plan): Plan = { + val rhs = dropNamedTuple(rhs0) val declTpe = if tpe.exists then tpe else rhs.tpe val vble = newVar(rhs, EmptyFlags, declTpe) initializer(vble) = rhs @@ -334,6 +339,7 @@ object PatternMatcher { def unapplyPlan(unapp: Tree, args: List[Tree]): Plan = { def caseClass = unapp.symbol.owner.linkedClass lazy val caseAccessors = caseClass.caseAccessors + val unappType = unapp.tpe.widen.stripNamedTuple def isSyntheticScala2Unapply(sym: Symbol) = sym.is(Synthetic) && sym.owner.is(Scala2x) @@ -341,39 +347,45 @@ object PatternMatcher { def tupleApp(i: Int, receiver: Tree) = // manually inlining the call to NonEmptyTuple#apply, because it's an inline method ref(defn.RuntimeTuplesModule) .select(defn.RuntimeTuples_apply) - .appliedTo(receiver, Literal(Constant(i))) + .appliedTo( + receiver.ensureConforms(defn.NonEmptyTupleTypeRef), // If scrutinee is a named tuple, cast to underlying tuple + Literal(Constant(i))) if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length) - def tupleSel(sym: Symbol) = ref(scrutinee).select(sym) + def tupleSel(sym: Symbol) = + // If scrutinee is a named tuple, cast to underlying tuple, so that we can + // continue to select with _1, _2, ... + ref(scrutinee).ensureConforms(scrutinee.info.stripNamedTuple).select(sym) val isGenericTuple = defn.isTupleClass(caseClass) && !defn.isTupleNType(tree.tpe match { case tp: OrType => tp.join case tp => tp }) // widen even hard unions, to see if it's a union of tuples - val components = if isGenericTuple then caseAccessors.indices.toList.map(tupleApp(_, ref(scrutinee))) else caseAccessors.map(tupleSel) + val components = + if isGenericTuple then caseAccessors.indices.toList.map(tupleApp(_, ref(scrutinee))) + else caseAccessors.map(tupleSel) matchArgsPlan(components, args, onSuccess) - else if (unapp.tpe <:< (defn.BooleanType)) + else if unappType.isRef(defn.BooleanClass) then TestPlan(GuardTest, unapp, unapp.span, onSuccess) else letAbstract(unapp) { unappResult => val isUnapplySeq = unapp.symbol.name == nme.unapplySeq - if (isProductMatch(unapp.tpe.widen, args.length) && !isUnapplySeq) { - val selectors = productSelectors(unapp.tpe).take(args.length) + if isProductMatch(unappType, args.length) && !isUnapplySeq then + val selectors = productSelectors(unappType).take(args.length) .map(ref(unappResult).select(_)) matchArgsPlan(selectors, args, onSuccess) - } - else if (isUnapplySeq && unapplySeqTypeElemTp(unapp.tpe.widen.finalResultType).exists) { + else if isUnapplySeq && unapplySeqTypeElemTp(unappType.finalResultType).exists then unapplySeqPlan(unappResult, args) - } - else if (isUnapplySeq && isProductSeqMatch(unapp.tpe.widen, args.length, unapp.srcPos)) { - val arity = productArity(unapp.tpe.widen, unapp.srcPos) + else if isUnapplySeq && isProductSeqMatch(unappType, args.length, unapp.srcPos) then + val arity = productArity(unappType, unapp.srcPos) unapplyProductSeqPlan(unappResult, args, arity) - } else if unappResult.info <:< defn.NonEmptyTupleTypeRef then - val components = (0 until foldApplyTupleType(unappResult.denot.info).length).toList.map(tupleApp(_, ref(unappResult))) + val components = + (0 until unappResult.denot.info.tupleElementTypes.getOrElse(Nil).length) + .toList.map(tupleApp(_, ref(unappResult))) matchArgsPlan(components, args, onSuccess) else { - assert(isGetMatch(unapp.tpe)) + assert(isGetMatch(unappType)) val argsPlan = { val get = ref(unappResult).select(nme.get, _.info.isParameterless) - val arity = productArity(get.tpe, unapp.srcPos) + val arity = productArity(get.tpe.stripNamedTuple, unapp.srcPos) if (isUnapplySeq) letAbstract(get) { getResult => if unapplySeqTypeElemTp(get.tpe).exists @@ -384,7 +396,7 @@ object PatternMatcher { letAbstract(get) { getResult => val selectors = if (args.tail.isEmpty) ref(getResult) :: Nil - else productSelectors(get.tpe).map(ref(getResult).select(_)) + else productSelectors(getResult.info).map(ref(getResult).select(_)) matchArgsPlan(selectors, args, onSuccess) } } diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index d91c4592a77b..76d057f15408 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -18,6 +18,7 @@ import Names.* import StdNames.* import ContextOps.* import NameKinds.DefaultGetterName +import Typer.tryEither import ProtoTypes.* import Inferencing.* import reporting.* @@ -135,14 +136,6 @@ object Applications { sels.takeWhile(_.exists).toList } - def getUnapplySelectors(tp: Type, args: List[untpd.Tree], pos: SrcPos)(using Context): List[Type] = - if (args.length > 1 && !(tp.derivesFrom(defn.SeqClass))) { - val sels = productSelectorTypes(tp, pos) - if (sels.length == args.length) sels - else tp :: Nil - } - else tp :: Nil - def productSeqSelectors(tp: Type, argsNum: Int, pos: SrcPos)(using Context): List[Type] = { val selTps = productSelectorTypes(tp, pos) val arity = selTps.length @@ -150,61 +143,122 @@ object Applications { (0 until argsNum).map(i => if (i < arity - 1) selTps(i) else elemTp).toList } - def unapplyArgs(unapplyResult: Type, unapplyFn: Tree, args: List[untpd.Tree], pos: SrcPos)(using Context): List[Type] = { - def getName(fn: Tree): Name = + /** A utility class that matches results of unapplys with patterns. Two queriable members: + * val argTypes: List[Type] + * def typedPatterns(qual: untpd.Tree, typer: Typer): List[Tree] + * TODO: Move into Applications trait. No need to keep it outside. But it's a large + * refactor, so do this when the rest is merged. + */ + class UnapplyArgs(unapplyResult: Type, unapplyFn: Tree, unadaptedArgs: List[untpd.Tree], pos: SrcPos)(using Context): + private var args = unadaptedArgs + + private def getName(fn: Tree): Name = fn match case TypeApply(fn, _) => getName(fn) case Apply(fn, _) => getName(fn) case fn: RefTree => fn.name - val unapplyName = getName(unapplyFn) // tolerate structural `unapply`, which does not have a symbol + private val unapplyName = getName(unapplyFn) // tolerate structural `unapply`, which does not have a symbol - def getTp = extractorMemberType(unapplyResult, nme.get, pos) + private def getTp = extractorMemberType(unapplyResult, nme.get, pos) - def fail = { + private def fail = { report.error(UnapplyInvalidReturnType(unapplyResult, unapplyName), pos) Nil } - def unapplySeq(tp: Type)(fallback: => List[Type]): List[Type] = { + private def unapplySeq(tp: Type)(fallback: => List[Type]): List[Type] = val elemTp = unapplySeqTypeElemTp(tp) - if (elemTp.exists) args.map(Function.const(elemTp)) - else if (isProductSeqMatch(tp, args.length, pos)) productSeqSelectors(tp, args.length, pos) - else if tp.derivesFrom(defn.NonEmptyTupleClass) then foldApplyTupleType(tp) + if elemTp.exists then + args.map(Function.const(elemTp)) + else if isProductSeqMatch(tp, args.length, pos) then + productSeqSelectors(tp, args.length, pos) + else if tp.derivesFrom(defn.NonEmptyTupleClass) then + tp.tupleElementTypes.getOrElse(Nil) else fallback - } - if (unapplyName == nme.unapplySeq) - unapplySeq(unapplyResult) { - if (isGetMatch(unapplyResult, pos)) unapplySeq(getTp)(fail) - else fail - } - else { - assert(unapplyName == nme.unapply) - if (isProductMatch(unapplyResult, args.length, pos)) - productSelectorTypes(unapplyResult, pos) - else if (isGetMatch(unapplyResult, pos)) - getUnapplySelectors(getTp, args, pos) - else if (unapplyResult.widenSingleton isRef defn.BooleanClass) - Nil - else if (defn.isProductSubType(unapplyResult) && productArity(unapplyResult, pos) != 0) - productSelectorTypes(unapplyResult, pos) - // this will cause a "wrong number of arguments in pattern" error later on, - // which is better than the message in `fail`. - else if unapplyResult.derivesFrom(defn.NonEmptyTupleClass) then - foldApplyTupleType(unapplyResult) - else fail - } - } + private def tryAdaptPatternArgs(elems: List[untpd.Tree], pt: Type)(using Context): Option[List[untpd.Tree]] = + tryEither[Option[List[untpd.Tree]]] + (Some(desugar.adaptPatternArgs(elems, pt))) + ((_, _) => None) + + private def getUnapplySelectors(tp: Type)(using Context): List[Type] = + // We treat patterns as product elements if + // they are named, or there is more than one pattern + val isProduct = args match + case x :: xs => x.isInstanceOf[untpd.NamedArg] || xs.nonEmpty + case _ => false + if isProduct && !tp.derivesFrom(defn.SeqClass) then + productUnapplySelectors(tp).getOrElse: + // There are unapplys with return types which have `get` and `_1, ..., _n` + // as members, but which are not subtypes of Product. So `productUnapplySelectors` + // would return None for these, but they are still valid types + // for a get match. A test case is pos/extractors.scala. + val sels = productSelectorTypes(tp, pos) + if (sels.length == args.length) sels + else tp :: Nil + else tp :: Nil - def foldApplyTupleType(tp: Type)(using Context): List[Type] = - object tupleFold extends TypeAccumulator[List[Type]]: - override def apply(accum: List[Type], t: Type): List[Type] = - t match - case AppliedType(tycon, x :: x2 :: Nil) if tycon.typeSymbol == defn.PairClass => - apply(x :: accum, x2) - case x => foldOver(accum, x) - end tupleFold - tupleFold(Nil, tp).reverse + private def productUnapplySelectors(tp: Type)(using Context): Option[List[Type]] = + if defn.isProductSubType(tp) then + tryAdaptPatternArgs(args, tp) match + case Some(args1) if isProductMatch(tp, args1.length, pos) => + args = args1 + Some(productSelectorTypes(tp, pos)) + case _ => None + else tp.widen.normalized.dealias match + case tp @ defn.NamedTuple(_, tt) => + tryAdaptPatternArgs(args, tp) match + case Some(args1) => + args = args1 + tt.tupleElementTypes + case _ => None + case _ => None + + /** The computed argument types which will be the scutinees of the sub-patterns. */ + val argTypes: List[Type] = + if unapplyName == nme.unapplySeq then + unapplySeq(unapplyResult): + if (isGetMatch(unapplyResult, pos)) unapplySeq(getTp)(fail) + else fail + else + assert(unapplyName == nme.unapply) + productUnapplySelectors(unapplyResult).getOrElse: + if isGetMatch(unapplyResult, pos) then + getUnapplySelectors(getTp) + else if unapplyResult.derivesFrom(defn.BooleanClass) then + Nil + else if unapplyResult.derivesFrom(defn.NonEmptyTupleClass) then + unapplyResult.tupleElementTypes.getOrElse(Nil) + else if defn.isProductSubType(unapplyResult) && productArity(unapplyResult, pos) != 0 then + productSelectorTypes(unapplyResult, pos) + // this will cause a "wrong number of arguments in pattern" error later on, + // which is better than the message in `fail`. + else fail + + /** The typed pattens of this unapply */ + def typedPatterns(qual: untpd.Tree, typer: Typer): List[Tree] = + unapp.println(i"unapplyQual = $qual, unapplyArgs = ${unapplyResult} with $argTypes / $args") + for argType <- argTypes do + assert(!isBounds(argType), unapplyResult.show) + val alignedArgs = argTypes match + case argType :: Nil + if args.lengthCompare(1) > 0 + && Feature.autoTuplingEnabled + && defn.isTupleNType(argType) => + untpd.Tuple(args) :: Nil + case _ => + args + val alignedArgTypes = + if argTypes.length == alignedArgs.length then + argTypes + else + report.error(UnapplyInvalidNumberOfArguments(qual, argTypes), pos) + argTypes.take(args.length) ++ + List.fill(argTypes.length - args.length)(WildcardType) + alignedArgs.lazyZip(alignedArgTypes).map(typer.typed(_, _)) + .showing(i"unapply patterns = $result", unapp) + + end UnapplyArgs def wrapDefs(defs: mutable.ListBuffer[Tree] | Null, tree: Tree)(using Context): Tree = if (defs != null && defs.nonEmpty) tpd.Block(defs.toList, tree) else tree @@ -1343,9 +1397,10 @@ trait Applications extends Compatibility { case _ => false case _ => false - def typedUnApply(tree: untpd.Apply, selType: Type)(using Context): Tree = { + def typedUnApply(tree: untpd.Apply, selType0: Type)(using Context): Tree = { record("typedUnApply") - val Apply(qual, args) = tree + val Apply(qual, unadaptedArgs) = tree + val selType = selType0.stripNamedTuple def notAnExtractor(tree: Tree): Tree = // prefer inner errors @@ -1558,27 +1613,14 @@ trait Applications extends Compatibility { typedExpr(untpd.TypedSplice(Apply(unapplyFn, dummyArg :: Nil))) inlinedUnapplyFnAndApp(dummyArg, unapplyAppCall) - var argTypes = unapplyArgs(unapplyApp.tpe, unapplyFn, args, tree.srcPos) - for (argType <- argTypes) assert(!isBounds(argType), unapplyApp.tpe.show) - val bunchedArgs = argTypes match { - case argType :: Nil => - if (args.lengthCompare(1) > 0 && Feature.autoTuplingEnabled && defn.isTupleNType(argType)) untpd.Tuple(args) :: Nil - else args - case _ => args - } - if (argTypes.length != bunchedArgs.length) { - report.error(UnapplyInvalidNumberOfArguments(qual, argTypes), tree.srcPos) - argTypes = argTypes.take(args.length) ++ - List.fill(argTypes.length - args.length)(WildcardType) - } - val unapplyPatterns = bunchedArgs.lazyZip(argTypes) map (typed(_, _)) + val unapplyPatterns = UnapplyArgs(unapplyApp.tpe, unapplyFn, unadaptedArgs, tree.srcPos) + .typedPatterns(qual, this) val result = assignType(cpy.UnApply(tree)(newUnapplyFn, unapplyImplicits(dummyArg, unapplyApp), unapplyPatterns), ownType) - unapp.println(s"unapply patterns = $unapplyPatterns") if (ownType.stripped eq selType.stripped) || ownType.isError then result 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/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 3c74e9f4ed90..7745c620312c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -29,7 +29,7 @@ import config.Printers.{typr, patmatch} import NameKinds.DefaultGetterName import NameOps.* import SymDenotations.{NoCompleter, NoDenotation} -import Applications.unapplyArgs +import Applications.UnapplyArgs import Inferencing.isFullyDefined import transform.patmat.SpaceEngine.{isIrrefutable, isIrrefutableQuotePattern} import transform.ValueClasses.underlyingOfValueClass @@ -966,10 +966,16 @@ trait Checking { false } - def check(pat: Tree, pt: Type): Boolean = + // Is scrutinee type `pt` a subtype of `pat.tpe`, after stripping named tuples + // and accounting for large generic tuples? + // Named tuples need to be stripped off, since names are dropped in patterns + def conforms(pat: Tree, pt: Type): Boolean = pt.isTupleXXLExtract(pat.tpe) // See isTupleXXLExtract, fixes TupleXXL parameter type - || pt <:< pat.tpe - || fail(pat, pt, Reason.NonConforming) + || pt.stripNamedTuple <:< pat.tpe + || (pt.widen ne pt) && conforms(pat, pt.widen) + + def check(pat: Tree, pt: Type): Boolean = + conforms(pat, pt) || fail(pat, pt, Reason.NonConforming) def recur(pat: Tree, pt: Type): Boolean = !sourceVersion.isAtLeast(`3.2`) @@ -982,7 +988,7 @@ trait Checking { case UnApply(fn, implicits, pats) => check(pat, pt) && (isIrrefutable(fn, pats.length) || fail(pat, pt, Reason.RefutableExtractor)) && { - val argPts = unapplyArgs(fn.tpe.widen.finalResultType, fn, pats, pat.srcPos) + val argPts = UnapplyArgs(fn.tpe.widen.finalResultType, fn, pats, pat.srcPos).argTypes pats.corresponds(argPts)(recur) } case Alternative(pats) => diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 9f2e0628e70e..bc19e97b85d8 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -857,6 +857,8 @@ trait Implicits: || inferView(dummyTreeOfType(from), to) (using ctx.fresh.addMode(Mode.ImplicitExploration).setExploreTyperState()).isSuccess // TODO: investigate why we can't TyperState#test here + || from.widen.isNamedTupleType && to.derivesFrom(defn.TupleClass) + && from.widen.stripNamedTuple <:< to ) /** Find an implicit conversion to apply to given tree `from` so that the diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 68a14603cb7a..46982cf1406d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -113,6 +113,31 @@ object Typer { def rememberSearchFailure(tree: tpd.Tree, fail: SearchFailure) = tree.putAttachment(HiddenSearchFailure, fail :: tree.attachmentOrElse(HiddenSearchFailure, Nil)) + + def tryEither[T](op: Context ?=> T)(fallBack: (T, TyperState) => T)(using Context): T = { + val nestedCtx = ctx.fresh.setNewTyperState() + val result = op(using nestedCtx) + if (nestedCtx.reporter.hasErrors && !nestedCtx.reporter.hasStickyErrors) { + record("tryEither.fallBack") + fallBack(result, nestedCtx.typerState) + } + else { + record("tryEither.commit") + nestedCtx.typerState.commit() + result + } + } + + /** Try `op1`, if there are errors, try `op2`, if `op2` also causes errors, fall back + * to errors and result of `op1`. + */ + def tryAlternatively[T](op1: Context ?=> T)(op2: Context ?=> T)(using Context): T = + tryEither(op1) { (failedVal, failedState) => + tryEither(op2) { (_, _) => + failedState.commit() + failedVal + } + } } /** Typecheck trees, the main entry point is `typed`. * @@ -710,66 +735,116 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer checkLegalValue(select, pt) ConstFold(select) + // If regular selection is typeable, we are done if checkedType.exists then - finish(tree, qual, checkedType) - else if selName == nme.apply && qual.tpe.widen.isInstanceOf[MethodType] then - // Simplify `m.apply(...)` to `m(...)` - qual - else if couldInstantiateTypeVar(qual.tpe.widen) then + return finish(tree, qual, checkedType) + + // Otherwise, simplify `m.apply(...)` to `m(...)` + if selName == nme.apply && qual.tpe.widen.isInstanceOf[MethodType] then + return qual + + // Otherwise, if there's a simply visible type variable in the result, try again + // with a more defined qualifier type. 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. + if couldInstantiateTypeVar(qual.tpe.widen) then // there's a simply visible type variable in the result; try again with a more defined qualifier type // 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 + return typedSelect(tree, pt, qual) + + // Otherwise, try to expand a named tuple selection + val namedTupleElems = qual.tpe.widen.namedTupleElementTypes + val nameIdx = namedTupleElems.indexWhere(_._1 == selName) + if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then + return typed( + untpd.Apply( + untpd.Select(untpd.TypedSplice(qual), nme.apply), + untpd.Literal(Constant(nameIdx))), + pt) + + // Otherwise, map combinations of A *: B *: .... EmptyTuple with nesting levels <= 22 + // to the Tuple class of the right arity and select from that one + if qual.tpe.isSmallGenericTuple then val elems = qual.tpe.widenTermRefExpr.tupleElementTypes.getOrElse(Nil) - typedSelect(tree, pt, qual.cast(defn.tupleType(elems))) - else - val tree1 = { - if selName.isTypeName then EmptyTree - else 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 - } + return typedSelect(tree, pt, qual.cast(defn.tupleType(elems))) + + // Otherwise try an extension or conversion + if selName.isTermName then + val tree1 = tryExtensionOrConversion( + tree, pt, IgnoredProto(pt), qual, ctx.typerState.ownedVars, this, inSelect = true) 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) + return tree1 + + // Otherwise, try a 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. + if ctx.gadt.isNarrowing then + 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") + return finish(tree1, qual1, checkedType1) + + if qual1.tpe.isSmallGenericTuple then + gadts.println(i"Tuple member selection healed by GADT approximation") + return typedSelect(tree, pt, qual1) + + val tree2 = tryExtensionOrConversion(tree1, pt, IgnoredProto(pt), qual1, ctx.typerState.ownedVars, this, inSelect = true) + if !tree2.isEmpty then + return tree2 + + // Otherwise, if there are uninstantiated type variables in the qualifier type, + // instantiate them and try again + if canDefineFurther(qual.tpe.widen) then + return typedSelect(tree, pt, qual) + + def dynamicSelect(pt: Type) = + val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName) + if pt.isInstanceOf[FunOrPolyProto] || pt == LhsProto then + assignType(tree2, TryDynamicCallType) else - assignType(tree, - rawType match - case rawType: NamedType => - inaccessibleErrorType(rawType, superAccess, tree.srcPos) + typedDynamicSelect(tree2, Nil, pt) + + // Otherwise, if the qualifier derives from class Dynamic, expand to a + // dynamic dispatch using selectDynamic or applyDynamic + if qual.tpe.derivesFrom(defn.DynamicClass) && selName.isTermName && !isDynamicExpansion(tree) then + return dynamicSelect(pt) + + // Otherwise, if the qualifier derives from class Selectable, + // and the selector name matches one of the element of the `Fields` type member, + // and the selector is neither applied nor assigned to, + // expand to a typed dynamic dispatch using selectDynamic wrapped in a cast + if qual.tpe.derivesFrom(defn.SelectableClass) && !isDynamicExpansion(tree) + && !pt.isInstanceOf[FunOrPolyProto] && pt != LhsProto + then + val fieldsType = qual.tpe.select(tpnme.Fields).dealias.simplified + val fields = fieldsType.namedTupleElementTypes + typr.println(i"try dyn select $qual, $selName, $fields") + fields.find(_._1 == selName) match + case Some((_, fieldType)) => + val dynSelected = dynamicSelect(fieldType) + dynSelected match + case Apply(sel: Select, _) if !sel.denot.symbol.exists => + // Reject corner case where selectDynamic needs annother selectDynamic to be called. E.g. as in neg/unselectable-fields.scala. + report.error(i"Cannot use selectDynamic here since it needs another selectDynamic to be invoked", tree.srcPos) case _ => - notAMemberErrorType(tree, qual, pt)) + return dynSelected.ensureConforms(fieldType) + case _ => + + // Otherwise, report an error + 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 = { @@ -2450,7 +2525,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer body1.isInstanceOf[RefTree] && !isWildcardArg(body1) || body1.isInstanceOf[Literal] val symTp = - if isStableIdentifierOrLiteral then pt + if isStableIdentifierOrLiteral || pt.isNamedTupleType then pt + // need to combine tuple element types with expected named type else if isWildcardStarArg(body1) || pt == defn.ImplicitScrutineeTypeRef || body1.tpe <:< pt // There is some strange interaction with gadt matching. @@ -3050,37 +3126,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, pt) + if tree1 ne tree then typed(tree1, pt) + else + val arity = tree.trees.length + val pts = pt.stripNamedTuple.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 { @@ -3436,31 +3507,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typedPattern(tree: untpd.Tree, selType: Type = WildcardType)(using Context): Tree = withMode(Mode.Pattern)(typed(tree, selType)) - def tryEither[T](op: Context ?=> T)(fallBack: (T, TyperState) => T)(using Context): T = { - val nestedCtx = ctx.fresh.setNewTyperState() - val result = op(using nestedCtx) - if (nestedCtx.reporter.hasErrors && !nestedCtx.reporter.hasStickyErrors) { - record("tryEither.fallBack") - fallBack(result, nestedCtx.typerState) - } - else { - record("tryEither.commit") - nestedCtx.typerState.commit() - result - } - } - - /** Try `op1`, if there are errors, try `op2`, if `op2` also causes errors, fall back - * to errors and result of `op1`. - */ - def tryAlternatively[T](op1: Context ?=> T)(op2: Context ?=> T)(using Context): T = - tryEither(op1) { (failedVal, failedState) => - tryEither(op2) { (_, _) => - failedState.commit() - failedVal - } - } - /** Is `pt` a prototype of an `apply` selection, or a parameterless function yielding one? */ def isApplyProto(pt: Type)(using Context): Boolean = pt.revealIgnored match { case pt: SelectionProto => pt.name == nme.apply @@ -4279,7 +4325,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case _: SelectionProto => tree // adaptations for selections are handled in typedSelect case _ if ctx.mode.is(Mode.ImplicitsEnabled) && tree.tpe.isValueType => - if pt.isRef(defn.AnyValClass, skipRefined = false) + if tree.tpe.widen.isNamedTupleType && pt.derivesFrom(defn.TupleClass) then + readapt(typed(untpd.Select(untpd.TypedSplice(tree), nme.toTuple))) + else if pt.isRef(defn.AnyValClass, skipRefined = false) || pt.isRef(defn.ObjectClass, skipRefined = false) then recover(TooUnspecific(pt)) diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.blacklist index 638455e7f2de..a856a5b84d92 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.blacklist @@ -66,6 +66,7 @@ tuple-fold.scala mt-redux-norm.perspective.scala i18211.scala 10867.scala +named-tuples1.scala # Opaque type i5720.scala diff --git a/compiler/test/dotc/run-test-pickling.blacklist b/compiler/test/dotc/run-test-pickling.blacklist index 954a64db1b66..dacbc63bb520 100644 --- a/compiler/test/dotc/run-test-pickling.blacklist +++ b/compiler/test/dotc/run-test-pickling.blacklist @@ -45,4 +45,5 @@ t6138-2 i12656.scala trait-static-forwarder i17255 +named-tuples-strawman-2.scala diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 4207a13ea66d..8cc070d5dbc5 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -198,7 +198,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) @@ -222,6 +222,8 @@ Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> TypeBounds ::= [‘>:’ Type] [‘<:’ Type] TypeBoundsTree(lo, hi) TypeParamBounds ::= TypeBounds {‘:’ Type} ContextBounds(typeBounds, tps) Types ::= Type {‘,’ Type} +NamesAndTypes ::= NameAndType {‘,’ NameAndType} +NameAndType ::= id ':' Type ``` ### Expressions @@ -290,8 +292,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)) @@ -343,6 +347,9 @@ SimplePattern1 ::= SimpleRef PatVar ::= varid | ‘_’ Patterns ::= Pattern {‘,’ Pattern} + | 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..3867b4d13f15 --- /dev/null +++ b/docs/_docs/reference/experimental/named-tuples.md @@ -0,0 +1,263 @@ +--- +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 and Convertibility + +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 Laura: 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. + +In the other direction, one can convert a named tuple to an unnamed tuple with the `toTuple` method. Example: +```scala +val x: (String, Int) = Bob.toTuple // ok +``` +`toTuple` is defined as an extension method in the `NamedTuple` object. +It returns the given tuple unchanged and simply "forgets" the names. + +A `.toTuple` selection is inserted implicitly by the compiler if it encounters a named tuple but the expected type is a regular tuple. So the following works as well: +```scala +val x: (String, Int) = Bob // works, expanded to Bob.toTuple +``` +The difference between subtyping in one direction and automatic `.toTuple` conversions in the other is relatively minor. The main difference is that `.toTuple` conversions don't work inside type constructors. So the following is OK: +```scala + val names = List("Laura", "Silvain") + val ages = List(25, 16) + val persons: List[Person] = names.zip(ages) +``` +But the following would be illegal. +```scala + val persons: List[Person] = List(Bob, Laura) + val pairs: List[(String, Int)] = persons // error +``` +We would need an explicit `_.toTuple` selection to express this: +```scala + val pairs: List[(String, Int)] = persons.map(_.toTuple) +``` +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 requires a conversion. + +### 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. In the internal representation, a named tuple type is represented at compile time as a pair of two tuples. One tuple contains the names as literal constant string types, the other contains the element types. The runtime representation of a named tuples consists of just the element values, whereas the names are forgotten. This is achieved by declaring `NamedTuple` +in package `scala` as an opaque type as follows: +```scala + opaque type NamedTuple[N <: Tuple, +V <: Tuple] >: V = V +``` +For instance, the `Person` type would be represented as the type +```scala +NamedTuple[("name", "age"), (String, Int)] +``` +`NamedTuple` 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. + +A `NamedTuple[N, V]` type is publicly known to be a supertype (but not a subtype) of its value paramater `V`, which means that regular tuples can be assigned to named tuples but not _vice versa_. + +The `NamedTuple` object contains a number of extension methods for named tuples hat mirror the same functions in `Tuple`. Examples are +`apply`, `head`, `tail`, `take`, `drop`, `++`, `map`, or `zip`. +Similar to `Tuple`, the `NamedTuple` object also contains types such as `Elem`, `Head`, `Concat` +that describe the results of these extension methods. + +The translation of named tuples to instances of `NamedTuple` 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. + +### The NamedTuple.From Type + +The `NamedTuple` object contains a type definition +```scala + type From[T] <: AnyNamedTuple +``` +`From` is treated specially by the compiler. When `NamedTuple.From` is applied to +an argument type that is an instance of a case class, the type expands to the named +tuple consisting of all the fields of that case class. +Here, _fields_ means: elements of the first parameter section. For instance, assuming +```scala +case class City(zip: Int, name: String, population: Int) +``` +then `NamedTuple.From[City]` is the named tuple +```scala +(zip: Int, name: String, population: Int) +``` +The same works for enum cases expanding to case classes, abstract types with case classes as upper bound, alias types expanding to case classes +and singleton types with case classes as underlying type. + +`From` is also defined on named tuples. If `NT` is a named tuple type, then `From[NT] = NT`. + + +### 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 + ``` + 4. Regular selector names `_1`, `_2`, ... are not allowed as names in named tuples. + +### 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 + +Patterns ::= Pattern {‘,’ Pattern} + | NamedPattern {‘,’ NamedPattern} +NamedPattern ::= id '=' Pattern +``` + +### Named Pattern Matching + +We allow named patterns not just for named tuples but also for case classes. +For instance: +```scala +city match + case c @ City(name = "London") => println(p.population) + case City(name = n, zip = 1026, population = pop) => println(pop) +``` + +Named constructor patterns are analogous to named tuple patterns. In both cases + + - either all fields are named or none is, + - every name must match the name some field of the selector, + - names can come in any order, + - not all fields of the selector need to be matched. + +This revives SIP 43, with a much simpler desugaring than originally proposed. +Named patterns are compatible with extensible pattern matching simply because +`unapply` results can be named tuples. + +### Source Incompatibilities + +There are some source incompatibilities involving named tuples of length one. +First, what was previously classified as an assignment could now be interpreted as a named tuple. Example: +```scala +var age: Int +(age = 1) +``` +This was an assignment in parentheses before, and is a named tuple of arity one now. It is however not idiomatic Scala code, since assignments are not usually enclosed in parentheses. + +Second, what was a named argument to an infix operator can now be interpreted as a named tuple. +```scala +class C: + infix def f(age: Int) +val c: C +``` +then +```scala +c f (age = 1) +``` +will now construct a tuple as second operand instead of passing a named parameter. + +### Computed Field Names + +The `Selectable` trait now has a `Fields` type member that can be instantiated +to a named tuple. + +```scala +trait Selectable: + type Fields <: NamedTuple.AnyNamedTuple +``` + +If `Fields` is instantiated in a subclass of `Selectable` to some named tuple type, +then the available fields and their types will be defined by that type. Assume `n: T` +is an element of the `Fields` type in some class `C` that implements `Selectable`, +that `c: C`, and that `n` is not otherwise legal as a name of a selection on `c`. +Then `c.n` is a legal selection, which expands to `c.selectDynamic("n").asInstanceOf[T]`. + +It is the task of the implementation of `selectDynamic` in `C` to ensure that its +computed result conforms to the predicted type `T` + +As an example, assume we have a query type `Q[T]` defined as follows: + +```scala +trait Q[T] extends Selectable: + type Fields = NamedTuple.Map[NamedTuple.From[T], Q] + def selectDynamic(fieldName: String) = ... +``` + +Assume in the user domain: +```scala +case class City(zipCode: Int, name: String, population: Int) +val city: Q[City] +``` +Then +```scala +city.zipCode +``` +has type `Q[Int]` and it expands to +```scala +city.selectDynamic("zipCode").asInstanceOf[Q[Int]] +``` diff --git a/docs/_docs/reference/syntax.md b/docs/_docs/reference/syntax.md index f8e7ba6a5cbc..ae541b65d8c4 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] ‘)’ | Refinement | SimpleType1 TypeArgs | SimpleType1 ‘#’ id @@ -263,7 +263,7 @@ SimpleExpr ::= SimpleRef | quoteId -- only inside splices | ‘new’ ConstrApp {‘with’ ConstrApp} [TemplateBody] | ‘new’ TemplateBody - | ‘(’ ExprsInParens ‘)’ + | ‘(’ [ExprsInParens] ‘)’ | SimpleExpr ‘.’ id | SimpleExpr ‘.’ MatchClause | SimpleExpr TypeArgs @@ -279,8 +279,7 @@ ExprSplice ::= spliceId | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern ExprsInParens ::= ExprInParens {‘,’ ExprInParens} -ExprInParens ::= PostfixExpr ‘:’ Type - | Expr +ExprInParens ::= PostfixExpr ‘:’ Type | Expr ParArgumentExprs ::= ‘(’ [ExprsInParens] ‘)’ | ‘(’ ‘using’ ExprsInParens ‘)’ | ‘(’ [ExprsInParens ‘,’] PostfixExpr ‘*’ ‘)’ @@ -331,6 +330,7 @@ SimplePattern1 ::= SimpleRef PatVar ::= varid | ‘_’ Patterns ::= Pattern {‘,’ Pattern} + ArgumentPatterns ::= ‘(’ [Patterns] ‘)’ | ‘(’ [Patterns ‘,’] PatVar ‘*’ ‘)’ ``` diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 5d72f15838cd..b38e057f06b1 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -154,6 +154,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 diff --git a/library/src/scala/NamedTuple.scala b/library/src/scala/NamedTuple.scala new file mode 100644 index 000000000000..dc6e6c3144f6 --- /dev/null +++ b/library/src/scala/NamedTuple.scala @@ -0,0 +1,217 @@ +package scala +import annotation.experimental +import compiletime.ops.boolean.* + +@experimental +object NamedTuple: + + /** The type to which named tuples get mapped to. For instance, + * (name: String, age: Int) + * gets mapped to + * NamedTuple[("name", "age"), (String, Int)] + */ + opaque type NamedTuple[N <: Tuple, +V <: Tuple] >: V <: AnyNamedTuple = V + + /** A type which is a supertype of all named tuples */ + opaque type AnyNamedTuple = Any + + def apply[N <: Tuple, V <: Tuple](x: V): NamedTuple[N, V] = x + + def unapply[N <: Tuple, V <: Tuple](x: NamedTuple[N, V]): Some[V] = Some(x) + + extension [V <: Tuple](x: V) + inline def withNames[N <: Tuple]: NamedTuple[N, V] = x + + export NamedTupleDecomposition.{Names, DropNames} + + extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V]) + + /** The underlying tuple without the names */ + inline def toTuple: V = x + + /** The number of elements in this tuple */ + inline def size: Tuple.Size[V] = toTuple.size + + // This intentionally works for empty named tuples as well. I think NonEmptyTuple is a dead end + // and should be reverted, just like NonEmptyList is also appealing at first, but a bad idea + // in the end. + + /** The value (without the name) at index `n` of this tuple */ + inline def apply(n: Int): Tuple.Elem[V, n.type] = + inline toTuple match + case tup: NonEmptyTuple => tup(n).asInstanceOf[Tuple.Elem[V, n.type]] + case tup => tup.productElement(n).asInstanceOf[Tuple.Elem[V, n.type]] + + /** The first element value of this tuple */ + inline def head: Tuple.Elem[V, 0] = apply(0) + + /** The tuple consisting of all elements of this tuple except the first one */ + inline def tail: NamedTuple[Tuple.Tail[N], Tuple.Tail[V]] = + toTuple.drop(1).asInstanceOf[NamedTuple[Tuple.Tail[N], Tuple.Tail[V]]] + + /** The last element value of this tuple */ + inline def last: Tuple.Last[V] = apply(size - 1).asInstanceOf[Tuple.Last[V]] + + /** The tuple consisting of all elements of this tuple except the last one */ + inline def init: NamedTuple[Tuple.Init[N], Tuple.Init[V]] = + toTuple.take(size - 1).asInstanceOf[NamedTuple[Tuple.Init[N], Tuple.Init[V]]] + + /** The tuple consisting of the first `n` elements of this tuple, or all + * elements if `n` exceeds `size`. + */ + inline def take(n: Int): NamedTuple[Tuple.Take[N, n.type], Tuple.Take[V, n.type]] = + toTuple.take(n) + + /** The tuple consisting of all elements of this tuple except the first `n` ones, + * or no elements if `n` exceeds `size`. + */ + inline def drop(n: Int): NamedTuple[Tuple.Drop[N, n.type], Tuple.Drop[V, n.type]] = + toTuple.drop(n) + + /** The tuple `(x.take(n), x.drop(n))` */ + inline def splitAt(n: Int): + (NamedTuple[Tuple.Take[N, n.type], Tuple.Take[V, n.type]], + NamedTuple[Tuple.Drop[N, n.type], Tuple.Drop[V, n.type]]) = + // would be nice if this could have type `Split[NamedTuple[N, V]]` instead, but + // we get a type error then. Similar for other methods here. + toTuple.splitAt(n) + + /** The tuple consisting of all elements of this tuple followed by all elements + * of tuple `that`. The names of the two tuples must be disjoint. + */ + inline def ++ [N2 <: Tuple, V2 <: Tuple](that: NamedTuple[N2, V2])(using Tuple.Disjoint[N, N2] =:= true) + : NamedTuple[Tuple.Concat[N, N2], Tuple.Concat[V, V2]] + = toTuple ++ that.toTuple + + // inline def :* [L] (x: L): NamedTuple[Append[N, ???], Append[V, L] = ??? + // inline def *: [H] (x: H): NamedTuple[??? *: N], H *: V] = ??? + + /** The named tuple consisting of all element values of this tuple mapped by + * the polymorphic mapping function `f`. The names of elements are preserved. + * If `x = (n1 = v1, ..., ni = vi)` then `x.map(f) = `(n1 = f(v1), ..., ni = f(vi))`. + */ + inline def map[F[_]](f: [t] => t => F[t]): NamedTuple[N, Tuple.Map[V, F]] = + toTuple.map(f).asInstanceOf[NamedTuple[N, Tuple.Map[V, F]]] + + /** The named tuple consisting of all elements of this tuple in reverse */ + inline def reverse: NamedTuple[Tuple.Reverse[N], Tuple.Reverse[V]] = + toTuple.reverse + + /** The named tuple consisting of all elements values of this tuple zipped + * with corresponding element values in named tuple `that`. + * If the two tuples have different sizes, + * the extra elements of the larger tuple will be disregarded. + * The names of `x` and `that` at the same index must be the same. + * The result tuple keeps the same names as the operand tuples. + */ + inline def zip[V2 <: Tuple](that: NamedTuple[N, V2]): NamedTuple[N, Tuple.Zip[V, V2]] = + toTuple.zip(that.toTuple) + + /** A list consisting of all element values */ + inline def toList: List[Tuple.Union[V]] = toTuple.toList.asInstanceOf[List[Tuple.Union[V]]] + + /** An array consisting of all element values */ + inline def toArray: Array[Object] = toTuple.toArray + + /** An immutable array consisting of all element values */ + inline def toIArray: IArray[Object] = toTuple.toIArray + + end extension + + /** The size of a named tuple, represented as a literal constant subtype of Int */ + type Size[X <: AnyNamedTuple] = Tuple.Size[DropNames[X]] + + /** The type of the element value at position N in the named tuple X */ + type Elem[X <: AnyNamedTuple, N <: Int] = Tuple.Elem[DropNames[X], N] + + /** The type of the first element value of a named tuple */ + type Head[X <: AnyNamedTuple] = Elem[X, 0] + + /** The type of the last element value of a named tuple */ + type Last[X <: AnyNamedTuple] = Tuple.Last[DropNames[X]] + + /** The type of a named tuple consisting of all elements of named tuple X except the first one */ + type Tail[X <: AnyNamedTuple] = Drop[X, 1] + + /** The type of the initial part of a named tuple without its last element */ + type Init[X <: AnyNamedTuple] = + NamedTuple[Tuple.Init[Names[X]], Tuple.Init[DropNames[X]]] + + /** The type of the named tuple consisting of the first `N` elements of `X`, + * or all elements if `N` exceeds `Size[X]`. + */ + type Take[X <: AnyNamedTuple, N <: Int] = + NamedTuple[Tuple.Take[Names[X], N], Tuple.Take[DropNames[X], N]] + + /** The type of the named tuple consisting of all elements of `X` except the first `N` ones, + * or no elements if `N` exceeds `Size[X]`. + */ + type Drop[X <: AnyNamedTuple, N <: Int] = + NamedTuple[Tuple.Drop[Names[X], N], Tuple.Drop[DropNames[X], N]] + + /** The pair type `(Take(X, N), Drop[X, N]). */ + type Split[X <: AnyNamedTuple, N <: Int] = (Take[X, N], Drop[X, N]) + + /** Type of the concatenation of two tuples `X` and `Y` */ + type Concat[X <: AnyNamedTuple, Y <: AnyNamedTuple] = + NamedTuple[Tuple.Concat[Names[X], Names[Y]], Tuple.Concat[DropNames[X], DropNames[Y]]] + + /** The type of the named tuple `X` mapped with the type-level function `F`. + * If `X = (n1 : T1, ..., ni : Ti)` then `Map[X, F] = `(n1 : F[T1], ..., ni : F[Ti])`. + */ + type Map[X <: AnyNamedTuple, F[_ <: Tuple.Union[DropNames[X]]]] = + NamedTuple[Names[X], Tuple.Map[DropNames[X], F]] + + /** A named tuple with the elements of tuple `X` in reversed order */ + type Reverse[X <: AnyNamedTuple] = + NamedTuple[Tuple.Reverse[Names[X]], Tuple.Reverse[DropNames[X]]] + + /** The type of the named tuple consisting of all element values of + * named tuple `X` zipped with corresponding element values of + * named tuple `Y`. If the two tuples have different sizes, + * the extra elements of the larger tuple will be disregarded. + * The names of `X` and `Y` at the same index must be the same. + * The result tuple keeps the same names as the operand tuples. + * For example, if + * ``` + * X = (n1 : S1, ..., ni : Si) + * Y = (n1 : T1, ..., nj : Tj) where j >= i + * ``` + * then + * ``` + * Zip[X, Y] = (n1 : (S1, T1), ..., ni: (Si, Ti)) + * ``` + * @syntax markdown + */ + type Zip[X <: AnyNamedTuple, Y <: AnyNamedTuple] = + Names[X] match + case Names[Y] => + NamedTuple[Names[X], Tuple.Zip[DropNames[X], DropNames[Y]]] + + /** A type specially treated by the compiler to represent all fields of a + * class argument `T` as a named tuple. Or, if `T` is already a named tyuple, + * `From[T]` is the same as `T`. + */ + type From[T] <: AnyNamedTuple + + /** The type of the empty named tuple */ + type Empty = EmptyTuple.type + + /** The empty named tuple */ + val Empty: Empty = EmptyTuple.asInstanceOf[Empty] + +end NamedTuple + +/** Separate from NamedTuple object so that we can match on the opaque type NamedTuple. */ +@experimental +object NamedTupleDecomposition: + import NamedTuple.* + + /** The names of a named tuple, represented as a tuple of literal string values. */ + type Names[X <: AnyNamedTuple] <: Tuple = X match + case NamedTuple[n, _] => n + + /** The value types of a named tuple represented as a regular tuple. */ + type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match + case NamedTuple[_, x] => x + diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index 3738bd05a19b..8074fe3664e5 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -1,6 +1,6 @@ package scala -import annotation.{experimental, showAsInfix} +import annotation.showAsInfix import compiletime.* import compiletime.ops.int.* @@ -82,7 +82,6 @@ sealed trait Tuple extends Product { /** Given a tuple `(a1, ..., am)`, returns the reversed tuple `(am, ..., a1)` * consisting all its elements. */ - @experimental inline def reverse[This >: this.type <: Tuple]: Reverse[This] = runtime.Tuples.reverse(this).asInstanceOf[Reverse[This]] } @@ -201,11 +200,9 @@ object Tuple { type IsMappedBy[F[_]] = [X <: Tuple] =>> X =:= Map[InverseMap[X, F], F] /** Type of the reversed tuple */ - @experimental type Reverse[X <: Tuple] = ReverseOnto[X, EmptyTuple] /** Prepends all elements of a tuple in reverse order onto the other tuple */ - @experimental type ReverseOnto[From <: Tuple, +To <: Tuple] <: Tuple = From match case x *: xs => ReverseOnto[xs, x *: To] case EmptyTuple => To @@ -238,6 +235,25 @@ object Tuple { */ type Union[T <: Tuple] = Fold[T, Nothing, [x, y] =>> x | y] + /** A type level Boolean indicating whether the tuple `X` has an element + * that matches `Y`. + * @pre The elements of `X` are assumed to be singleton types + */ + type Contains[X <: Tuple, Y] <: Boolean = X match + case Y *: _ => true + case _ *: xs => Contains[xs, Y] + case EmptyTuple => false + + /** A type level Boolean indicating whether the type `Y` contains + * none of the elements of `X`. + * @pre The elements of `X` and `Y` are assumed to be singleton types + */ + type Disjoint[X <: Tuple, Y <: Tuple] <: Boolean = X match + case x *: xs => Contains[Y, x] match + case true => false + case false => Disjoint[xs, Y] + case EmptyTuple => true + /** 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/library/src/scala/runtime/Tuples.scala b/library/src/scala/runtime/Tuples.scala index 41425e8559ba..efb54c54d50b 100644 --- a/library/src/scala/runtime/Tuples.scala +++ b/library/src/scala/runtime/Tuples.scala @@ -1,7 +1,5 @@ package scala.runtime -import scala.annotation.experimental - object Tuples { inline val MaxSpecialized = 22 @@ -505,7 +503,6 @@ object Tuples { } } - @experimental def reverse(self: Tuple): Tuple = (self: Any) match { case xxl: TupleXXL => xxlReverse(xxl) case _ => specialCaseReverse(self) diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 372e1e34bb85..b2bd4b791423 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/project/MiMaFilters.scala b/project/MiMaFilters.scala index 5ccb70ad6fdf..064bb9cc2260 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -10,6 +10,14 @@ object MiMaFilters { Build.mimaPreviousDottyVersion -> Seq( ProblemFilters.exclude[DirectMissingMethodProblem]("scala.annotation.experimental.this"), ProblemFilters.exclude[FinalClassProblem]("scala.annotation.experimental"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.Tuple.fromArray"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.Tuple.fromIArray"), + ProblemFilters.exclude[MissingFieldProblem]("scala.Tuple.helpers"), + ProblemFilters.exclude[MissingClassProblem]("scala.Tuple$helpers$"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.fromArray"), + ProblemFilters.exclude[DirectMissingMethodProblem]("scala.runtime.Tuples.fromIArray"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.namedTuples"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$namedTuples$"), ), // Additions since last LTS 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/fieldsOf.scala b/tests/neg/fieldsOf.scala new file mode 100644 index 000000000000..d3539070b556 --- /dev/null +++ b/tests/neg/fieldsOf.scala @@ -0,0 +1,11 @@ +case class Person(name: String, age: Int) +class Anon(name: String, age: Int) +def foo[T](): NamedTuple.From[T] = ??? + +def test = + var x: NamedTuple.From[Person] = ??? + x = foo[Person]() // ok + x = foo[Anon]() // error + x = foo() // error + + diff --git a/tests/neg/i7247.scala b/tests/neg/i7247.scala index 9172f90fad07..3514f20c47fe 100644 --- a/tests/neg/i7247.scala +++ b/tests/neg/i7247.scala @@ -1,2 +1,2 @@ val x = "foo" match - case _: (a *: (b: Any)) => ??? // error \ No newline at end of file + case _: (a *: (b: Any)) => ??? // error, now OK since (b: Any) is a named tuple \ No newline at end of file diff --git a/tests/neg/i7751.scala b/tests/neg/i7751.scala index 978ed860574f..fd66e7d451be 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 // error val a = Some(x=y,)=> diff --git a/tests/neg/named-tuples-2.check b/tests/neg/named-tuples-2.check new file mode 100644 index 000000000000..0a52d5f3989b --- /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 (String, Int, 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 (String, Int, 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-3.check b/tests/neg/named-tuples-3.check new file mode 100644 index 000000000000..2091c36191c0 --- /dev/null +++ b/tests/neg/named-tuples-3.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg/named-tuples-3.scala:7:16 ----------------------------------------------------- +7 |val p: Person = f // error + | ^ + | Found: NamedTuple.NamedTuple[(Int, Any), (Int, String)] + | Required: Person + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/named-tuples-3.scala b/tests/neg/named-tuples-3.scala new file mode 100644 index 000000000000..0f1215338b0a --- /dev/null +++ b/tests/neg/named-tuples-3.scala @@ -0,0 +1,7 @@ +import language.experimental.namedTuples + +def f: NamedTuple.NamedTuple[(Int, Any), (Int, String)] = ??? + +type Person = (name: Int, age: String) + +val p: Person = f // error diff --git a/tests/neg/named-tuples.check b/tests/neg/named-tuples.check new file mode 100644 index 000000000000..db3cc703722f --- /dev/null +++ b/tests/neg/named-tuples.check @@ -0,0 +1,103 @@ +-- 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:20:20 ------------------------------------------------------ +20 | 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:21:18 ------------------------------------------------------ +21 | val _: Person = nameOnly // error + | ^^^^^^^^ + | Found: (Test.nameOnly : (name : String)) + | Required: Test.Person + | + | longer explanation available when compiling with `-explain` +-- [E172] Type Error: tests/neg/named-tuples.scala:22:41 --------------------------------------------------------------- +22 | val _: Person = (name = "") ++ nameOnly // error + | ^ + | Cannot prove that Tuple.Disjoint[Tuple1[("name" : String)], Tuple1[("name" : String)]] =:= (true : Boolean). +-- [E008] Not Found Error: tests/neg/named-tuples.scala:23:9 ----------------------------------------------------------- +23 | person._1 // error + | ^^^^^^^^^ + | value _1 is not a member of (name : String, age : Int) +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:25:36 ------------------------------------------------------ +25 | 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:27:17 --------------------------------------------------------------------------- +27 | 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:30:10 --------------------------------------------------------------------------- +30 | case (name = n, age = a) => () // error // error + | ^^^^^^^^ + | No element named `name` is defined in selector type (String, Int) +-- Error: tests/neg/named-tuples.scala:30:20 --------------------------------------------------------------------------- +30 | case (name = n, age = a) => () // error // error + | ^^^^^^^ + | No element named `age` is defined in selector type (String, Int) +-- [E172] Type Error: tests/neg/named-tuples.scala:32:27 --------------------------------------------------------------- +32 | val pp = person ++ (1, 2) // error + | ^ + | Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), Tuple] =:= (true : Boolean). +-- [E172] Type Error: tests/neg/named-tuples.scala:35:18 --------------------------------------------------------------- +35 | person ++ (1, 2) match // error + | ^ + | Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), Tuple] =:= (true : Boolean). +-- Error: tests/neg/named-tuples.scala:38:17 --------------------------------------------------------------------------- +38 | val bad = ("", age = 10) // error + | ^^^^^^^^ + | Illegal combination of named and unnamed tuple elements +-- Error: tests/neg/named-tuples.scala:41:20 --------------------------------------------------------------------------- +41 | case (name = n, age) => () // error + | ^^^ + | Illegal combination of named and unnamed tuple elements +-- Error: tests/neg/named-tuples.scala:42:16 --------------------------------------------------------------------------- +42 | case (name, age = a) => () // error + | ^^^^^^^ + | Illegal combination of named and unnamed tuple elements +-- Error: tests/neg/named-tuples.scala:45:10 --------------------------------------------------------------------------- +45 | case (age = x) => // error + | ^^^^^^^ + | No element named `age` is defined in selector type Tuple +-- [E172] Type Error: tests/neg/named-tuples.scala:47:27 --------------------------------------------------------------- +47 | val p2 = person ++ person // error + | ^ + |Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), (("name" : String), ("age" : String))] =:= (true : Boolean). +-- [E172] Type Error: tests/neg/named-tuples.scala:48:43 --------------------------------------------------------------- +48 | val p3 = person ++ (first = 11, age = 33) // error + | ^ + |Cannot prove that Tuple.Disjoint[(("name" : String), ("age" : String)), (("first" : String), ("age" : String))] =:= (true : Boolean). +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:50:22 ------------------------------------------------------ +50 | val p5 = person.zip((first = 11, age = 33)) // error + | ^^^^^^^^^^^^^^^^^^^^^^ + | Found: (first : Int, age : Int) + | Required: NamedTuple.NamedTuple[(("name" : String), ("age" : String)), Tuple] + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/named-tuples.scala:61:32 ------------------------------------------------------ +61 | val typo: (name: ?, age: ?) = (name = "he", ag = 1) // error + | ^^^^^^^^^^^^^^^^^^^^^ + | Found: (name : String, ag : Int) + | Required: (name : ?, age : ?) + | + | 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..8f78f7915206 --- /dev/null +++ b/tests/neg/named-tuples.scala @@ -0,0 +1,61 @@ +import annotation.experimental +import language.experimental.namedTuples + +@experimental object Test: + + 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") + + val y: (String, Int) = person // ok, conversion + val _: (String, Int) = (name = "", age = 0) // ok, conversion + val _: NameOnly = person // error + val _: Person = nameOnly // error + val _: Person = (name = "") ++ nameOnly // error + person._1 // error + + val _: (age: Int, name: String) = person // error + + val (name = x, agee = y) = person // error + + ("Ives", 2) match + case (name = n, age = a) => () // error // error + + val pp = person ++ (1, 2) // 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 + + (??? : Tuple) match + case (age = x) => // error + + val p2 = person ++ person // error + val p3 = person ++ (first = 11, age = 33) // error + val p4 = person.zip(person) // ok + val p5 = person.zip((first = 11, age = 33)) // error + // Note: this one depends on the details of the conversion named -> unnamed + // we do a conversion only of the expected type is a tuple. If we used a + // regular implicit conversion, then (first = 11, age = 33) would be converted + // to (Int, Int) and that would be upcast to (name: Int, age: Int), which + // would hide an error. So we have be careful that the "downwards" conversion + // is specific and does not apply to a different "upwards" type. + // The same problem happens if we assume named <: unnamed. In that case we would first + // upcast (first: Int, age: Int) to (Int, Int), and then use the downwards + // conversion to (name: Int, age: Int). This one would be harder to guard against. + + val typo: (name: ?, age: ?) = (name = "he", ag = 1) // error 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 | ^ diff --git a/tests/neg/unselectable-fields.check b/tests/neg/unselectable-fields.check new file mode 100644 index 000000000000..f7f0bf51a6bc --- /dev/null +++ b/tests/neg/unselectable-fields.check @@ -0,0 +1,4 @@ +-- Error: tests/neg/unselectable-fields.scala:4:13 --------------------------------------------------------------------- +4 |val _ = foo1.xyz // error + | ^^^^^^^^ + | Cannot use selectDynamic here since it needs another selectDynamic to be invoked diff --git a/tests/neg/unselectable-fields.scala b/tests/neg/unselectable-fields.scala new file mode 100644 index 000000000000..7abe49d24764 --- /dev/null +++ b/tests/neg/unselectable-fields.scala @@ -0,0 +1,6 @@ +val foo1 = new Selectable: + type Fields = (xyz: Int) + def selectDynamic(name: String): Any = 23 +val _ = foo1.xyz // error + + diff --git a/tests/new/test.scala b/tests/new/test.scala index e6bfc29fd808..16a823547553 100644 --- a/tests/new/test.scala +++ b/tests/new/test.scala @@ -1,2 +1,9 @@ -object Test: - def f: Any = 1 +import language.experimental.namedTuples + +type Person = (name: String, age: Int) + +def test = + val bob = (name = "Bob", age = 33): (name: String, age: Int) + + val silly = bob match + case (name = n, age = a) => n.length + a diff --git a/tests/pos/fieldsOf.scala b/tests/pos/fieldsOf.scala new file mode 100644 index 000000000000..2594dae2cbf7 --- /dev/null +++ b/tests/pos/fieldsOf.scala @@ -0,0 +1,18 @@ +import language.experimental.namedTuples + +case class Person(name: String, age: Int) + +type PF = NamedTuple.From[Person] + +def foo[T]: NamedTuple.From[T] = ??? + +class Anon(name: String, age: Int) + +def test = + var x: NamedTuple.From[Person] = ??? + val y: (name: String, age: Int) = x + x = y + x = foo[Person] + //x = foo[Anon] // error + + diff --git a/tests/pos/named-tuple-widen.scala b/tests/pos/named-tuple-widen.scala new file mode 100644 index 000000000000..410832e04c17 --- /dev/null +++ b/tests/pos/named-tuple-widen.scala @@ -0,0 +1,9 @@ +import language.experimental.namedTuples + +class A +class B +val y1: (a1: A, b1: B) = ??? +val y2: (a2: A, b2: B) = ??? +var z1 = if ??? then y1 else y2 // -- what is the type of z2 +var z2: NamedTuple.AnyNamedTuple = z1 +val _ = z1 = z2 \ 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..35675d1bfc76 --- /dev/null +++ b/tests/pos/named-tuples-strawman.scala @@ -0,0 +1,49 @@ +// Currently does not compile because of #19434 +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 toTuple: 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.toTuple + + x = y + + val z = y.toTuple + + 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.toTuple // 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-tuples1.scala b/tests/pos/named-tuples1.scala new file mode 100644 index 000000000000..58e3fc065e61 --- /dev/null +++ b/tests/pos/named-tuples1.scala @@ -0,0 +1,13 @@ +import annotation.experimental +import language.experimental.namedTuples + +@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/pos/namedtuple-src-incompat.scala b/tests/pos/namedtuple-src-incompat.scala new file mode 100644 index 000000000000..57451a4321b7 --- /dev/null +++ b/tests/pos/namedtuple-src-incompat.scala @@ -0,0 +1,17 @@ +import language.experimental.namedTuples +var age = 22 +val x = (age = 1) +val _: (age: Int) = x +val x2 = {age = 1} +val _: Unit = x2 + +class C: + infix def id[T](age: T): T = age + +def test = + val c: C = ??? + val y = c id (age = 1) + val _: (age: Int) = y + val y2 = c.id(age = 1) + val _: Int = y2 + diff --git a/tests/pos/tuple-ops.scala b/tests/pos/tuple-ops.scala new file mode 100644 index 000000000000..739b1ebeeb02 --- /dev/null +++ b/tests/pos/tuple-ops.scala @@ -0,0 +1,18 @@ +import language.experimental.namedTuples +import Tuple.* + +def test = + summon[Disjoint[(1, 2, 3), (4, 5)] =:= true] + summon[Disjoint[(1, 2, 6), (4, 5)] =:= true] + summon[Disjoint[(1, 2, 6), EmptyTuple] =:= true] + summon[Disjoint[EmptyTuple, EmptyTuple] =:= true] + + summon[Contains[(1, 2, 3), Int] =:= true] + summon[Contains[(1, 2, 3), 2] =:= true] + summon[Contains[(1, 2, 3), 4] =:= false] + + summon[Disjoint[(1, 2, 3), (4, 2)] =:= false] + summon[Disjoint[("a", "b"), ("b", "c")] =:= false] + summon[Disjoint[(1, 2, 6), Tuple1[2]] =:= false] + summon[Disjoint[Tuple1[3], (4, 3, 6)] =:= false] + diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index 76c08fa24213..48ff5407ac87 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -71,14 +71,14 @@ val experimentalDefinitionInLibrary = Set( "scala.quoted.Quotes.reflectModule.TermParamClauseMethods.erasedArgs", "scala.quoted.Quotes.reflectModule.TermParamClauseMethods.hasErasedArgs", - // New feature: reverse method on Tuple - "scala.Tuple.reverse", // can be stabilized in 3.5 - "scala.Tuple$.Reverse", // can be stabilized in 3.5 - "scala.Tuple$.ReverseOnto", // can be stabilized in 3.5 - "scala.runtime.Tuples$.reverse", // can be stabilized in 3.5 - // New feature: fromNullable for explicit nulls "scala.Predef$.fromNullable", + + // New feature: named tuples + "scala.NamedTuple", + "scala.NamedTuple$", + "scala.NamedTupleDecomposition", + "scala.NamedTupleDecomposition$", ) diff --git a/tests/run/named-patmatch.scala b/tests/run/named-patmatch.scala new file mode 100644 index 000000000000..e62497e4aa8f --- /dev/null +++ b/tests/run/named-patmatch.scala @@ -0,0 +1,36 @@ +import annotation.experimental +import language.experimental.namedTuples + +@main def Test = + locally: + val (x = x, y = y) = (x = 11, y = 22) + assert(x == 11 && y == 22) + + locally: + val (x = a, y = b) = (x = 1, y = 2) + assert(a == 1 && b == 2) + + locally: + val (x = a, y = b) = (x = 1, y = 2) + assert(a == 1 && b == 2) + + locally: + val (x, y) = (x = 1, y = 2) + assert(x == 1 && y == 2) + + locally: + val (a, b) = (x = 1, y = 2) + assert(a == 1 && b == 2) + + (x = 1, y = 2) match + case (x = x, y = y) => assert(x == 1 && y == 2) + + (x = 1, y = 2) match + case (x, y) => assert(x == 1 && y == 2) + + (x = 1, y = 2) match + case (a, b) => assert(a == 1 && b == 2) + + + + diff --git a/tests/run/named-patterns.check b/tests/run/named-patterns.check new file mode 100644 index 000000000000..9ccc08d67069 --- /dev/null +++ b/tests/run/named-patterns.check @@ -0,0 +1,20 @@ +name Bob, age 22 +name Bob +age 22 +age 22, name Bob +Bob, 22 +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 +Bob, aged 22, in 1003 Lausanne, Rue de la Gare 44 +Bob in 1003 Lausanne +aged 22 in Rue de la Gare in Lausanne +Bob, aged 22 in 1003 Lausanne, Rue de la Gare 44 +Bob, aged 22 in 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..7c24dc8d683a --- /dev/null +++ b/tests/run/named-patterns.scala @@ -0,0 +1,74 @@ +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) + + class Person2(val name: String, val age: Int) + object Person2: + def unapply(p: Person2): Option[(name: String, age: Int)] = Some((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 bob2 = Person2("Bob", 22) + bob2 match + case Person2(name = n, age = a) => println(s"name $n, age $a") + bob2 match + case Person2(name = n) => println(s"name $n") + bob2 match + case Person2(age = a) => println(s"age $a") + bob2 match + case Person2(age = a, name = n) => println(s"age $a, name $n") + bob2 match + case Person2(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") + + type Person3 = (p: Person2, addr: Address) + + val p3 = (p = bob2, addr = addr) + p3 match + case (addr = Address(city = c, zip = z, street = s, number = n), p = Person2(name = nn, age = a)) => + println(s"$nn, aged $a, in $z $c, $s $n") + p3 match + case (p = Person2(name = nn), addr = Address(zip = z, city = c)) => + println(s"$nn in $z $c") + p3 match + case (p = Person2(age = a), addr = Address(city = c, street = s)) => + println(s"aged $a in $s in $c") + p3 match + case (Person2(age = a, name = nn), Address(number = n, street = s, zip = z, city = c)) => + println(s"$nn, aged $a in $z $c, $s $n") + p3 match + case (Person2(nn, a), Address(c, z, s, number)) => + println(s"$nn, aged $a in $z $c, $s $number") diff --git a/tests/run/named-tuple-ops.scala b/tests/run/named-tuple-ops.scala new file mode 100644 index 000000000000..076ab5028c6c --- /dev/null +++ b/tests/run/named-tuple-ops.scala @@ -0,0 +1,89 @@ +//> using options -source future +import language.experimental.namedTuples +import scala.compiletime.asMatchable + +type City = (name: String, zip: Int, pop: Int) +type Raw = (String, Int, Int) + +type Coord = (x: Double, y: Double) +type Labels = (x: String, y: String) + +@main def Test = + val city: City = (name = "Lausanne", zip = 1000, pop = 140000) + val coord: Coord = (x = 1.0, y = 0.0) + val labels: Labels = (x = "west", y = "north") + + val size: 3 = city.size + assert(city.size == 3) + + val zip: Int = city(1) + assert(zip == 1000) + + val name: String = city.head + assert(name == "Lausanne") + + val zip_pop: (zip: Int, pop: Int) = city.tail + val (_: Int, _: Int) = zip_pop + assert(zip_pop == (zip = 1000, pop = 140000)) + + val cinit = city.init + val _: (name: String, zip: Int) = cinit + assert(cinit == (name = "Lausanne", zip = 1000)) + + val ctake1: (name: String) = city.take(1) + assert(ctake1 == (name = "Lausanne")) + + val cdrop1 = city.drop(1) + val _: (zip: Int, pop: Int) = cdrop1 + assert(cdrop1 == zip_pop) + + val cdrop3 = city.drop(3) + val _: NamedTuple.Empty = cdrop3 + assert(cdrop3 == NamedTuple.Empty) + + val cdrop4 = city.drop(4) + val _: NamedTuple.Empty = cdrop4 + assert(cdrop4 == NamedTuple.Empty) + + val csplit = city.splitAt(1) + val _: ((name: String), (zip: Int, pop: Int)) = csplit + assert(csplit == ((name = "Lausanne"), zip_pop)) + + val city_coord = city ++ coord + val _: NamedTuple.Concat[City, Coord] = city_coord + val _: (name: String, zip: Int, pop: Int, x: Double, y: Double) = city_coord + assert(city_coord == (name = "Lausanne", zip = 1000, pop = 140000, x = 1.0, y = 0.0)) + + type IntToString[X] = X match + case Int => String + case _ => X + + val intToString = [X] => (x: X) => x.asMatchable match + case x: Int => x.toString + case x => x + + val citymap = city.map[IntToString](intToString.asInstanceOf) + val _: (name: String, zip: String, pop: String) = citymap + assert(citymap == (name = "Lausanne", zip = "1000", pop = "140000")) + + val cityreverse = city.reverse + val _: (pop: Int, zip: Int, name: String) = cityreverse + assert(cityreverse == (pop = 140000, zip = 1000, name = "Lausanne")) + + val zipped = coord.zip(labels) + val _: (x: (Double, String), y: (Double, String)) = zipped + val (x3, y3) = zipped + val _: (Double, String) = x3 + assert(zipped == (x = (1.0, "west"), y = (0.0, "north"))) + + val zippedRaw = ((1.0, "west"), (0.0, "north")) + val (x1: (Double, String), x2: (Double, String)) = zippedRaw + + val cityFields = city.toList + val _: List[String | Int] = cityFields + assert(cityFields == List("Lausanne", 1000, 140000)) + + val citArr = city.toArray + val _: List[String | Int] = cityFields + assert(cityFields == List("Lausanne", 1000, 140000)) + diff --git a/tests/run/named-tuples-strawman-2.scala b/tests/run/named-tuples-strawman-2.scala new file mode 100644 index 000000000000..95f37ad23a93 --- /dev/null +++ b/tests/run/named-tuples-strawman-2.scala @@ -0,0 +1,248 @@ +import compiletime.* +import compiletime.ops.int.* +import compiletime.ops.boolean.! +import Tuple.* + +object TupleOps: + + private object helpers: + + /** Used to implement IndicesWhere */ + type IndicesWhereHelper[X <: Tuple, P[_ <: Union[X]] <: Boolean, N <: Int] <: Tuple = X match + case EmptyTuple => EmptyTuple + case h *: t => P[h] match + case true => N *: IndicesWhereHelper[t, P, S[N]] + case false => IndicesWhereHelper[t, P, S[N]] + + end helpers + + /** A type level Boolean indicating whether the tuple `X` has an element + * that matches `Y`. + * @pre The elements of `X` are assumed to be singleton types + */ + type Contains[X <: Tuple, Y] <: Boolean = X match + case Y *: _ => true + case _ *: xs => Contains[xs, Y] + case EmptyTuple => false + + /** The index of `Y` in tuple `X` as a literal constant Int, + * or `Size[X]` if `Y` is disjoint from all element types in `X`. + */ + type IndexOf[X <: Tuple, Y] <: Int = X match + case Y *: _ => 0 + case _ *: xs => S[IndexOf[xs, Y]] + case EmptyTuple => 0 + + /** A tuple consisting of those indices `N` of tuple `X` where the predicate `P` + * is true for `Elem[X, N]`. Indices are type level values <: Int. + */ + type IndicesWhere[X <: Tuple, P[_ <: Union[X]] <: Boolean] = + helpers.IndicesWhereHelper[X, P, 0] + + extension [X <: Tuple](inline x: X) + + /** The index (starting at 0) of the first occurrence of `y.type` in the type `X` of `x` + * or `Size[X]` if no such element exists. + */ + inline def indexOf(y: Any): IndexOf[X, y.type] = constValue[IndexOf[X, y.type]] + + /** A boolean indicating whether there is an element `y.type` in the type `X` of `x` */ + inline def contains(y: Any): Contains[X, y.type] = constValue[Contains[X, y.type]] + + end extension + + + /** The `X` tuple, with its element at index `N` replaced by `Y`. + * If `N` is equal to `Size[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 => + N match + case 0 => Y *: EmptyTuple + + inline def updateOrAppend[X <: Tuple, N <: Int, Y](xs: X, y: Y): UpdateOrAppend[X, N, Y] = + locally: + val n = constValue[N] + val size = xs.size + require(0 <= n && n <= xs.size, s"Index $n out of range 0..$size") + if n == size then xs :* y + else + val elems = xs.toArray + elems(n) = y.asInstanceOf[Object] + fromArray(elems) + .asInstanceOf[UpdateOrAppend[X, N, Y]] + + extension [X <: Tuple](inline xs: X) + // Note: Y must be inferred precisely, or given explicitly. This means even though `updateOrAppend` + // is clearly useful, we cannot yet move it to tuple since it is still too awkward to use. + // Once we have precise inference, we could replace `Y <: Singleton` with `Y: Precise` + // and then it should work beautifully. + inline def updateOrAppend[N <: Int & Singleton, Y <: Singleton](inline n: N, inline y: Y): UpdateOrAppend[X, N, Y] = + locally: + val size = xs.size + require(0 <= n && n <= size, s"Index $n out of range 0..$size") + if n == size then xs :* y + else + val elems = xs.toArray + elems(n) = y.asInstanceOf[Object] + fromArray(elems) + .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 + + inline def appendIfDistinct[X <: Tuple, Y](xs: X, y: Y): AppendIfDistinct[X, Y] = + (if xs.contains(y) then xs else xs :* y).asInstanceOf[AppendIfDistinct[X, Y]] + + /** `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 + + inline def concatDistinct[X <: Tuple, Y <: Tuple](xs: X, ys: Y): ConcatDistinct[X, Y] = + (xs ++ filter[Y, [Elem] =>> ![Contains[X, Elem]]](ys)).asInstanceOf[ConcatDistinct[X, Y]] + + /** A tuple consisting of all elements of this tuple that have types + * for which the given type level predicate `P` reduces to the literal + * constant `true`. + */ + inline def filter[X <: Tuple, P[_] <: Boolean](xs: X): Filter[X, P] = + val toInclude = constValueTuple[IndicesWhere[X, P]].toArray + val arr = new Array[Object](toInclude.length) + for i <- toInclude.indices do + arr(i) = xs.productElement(toInclude(i).asInstanceOf[Int]).asInstanceOf[Object] + Tuple.fromArray(arr).asInstanceOf[Filter[X, P]] + +object NamedTupleDecomposition: + import NamedTupleOps.* + + /** The names of the named tuple type `NT` */ + type Names[NT <: AnyNamedTuple] <: Tuple = NT match + case NamedTuple[n, _] => n + + /** The value types of the named tuple type `NT` */ + type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match + case NamedTuple[_, x] => x + +object NamedTupleOps: + import TupleOps.* + + opaque type AnyNamedTuple = Any + + opaque type NamedTuple[N <: Tuple, +X <: Tuple] >: X <: AnyNamedTuple = X + + export NamedTupleDecomposition.* + + object NamedTuple: + def apply[N <: Tuple, X <: Tuple](x: X): NamedTuple[N, X] = x + + extension [NT <: AnyNamedTuple](x: NT) + inline def toTuple: DropNames[NT] = x.asInstanceOf + inline def names: Names[NT] = constValueTuple[Names[NT]] + + /** 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 <: AnyNamedTuple, NT2 <: AnyNamedTuple] = + Merge[ConcatDistinct[Names[NT1], Names[NT2]], DropNames[NT1], Names[NT2], DropNames[NT2]] + + extension [NT1 <: AnyNamedTuple](nt1: NT1) + inline def updateWith[NT2 <: AnyNamedTuple](nt2: NT2): UpdateWith[NT1, NT2] = + val names = constValueTuple[ConcatDistinct[Names[NT1], Names[NT2]]].toArray + val names2 = constValueTuple[Names[NT2]].toArray + val values1 = nt1.toTuple + val values2 = nt2.toTuple + val values = new Array[Object](names.length) + values1.toArray.copyToArray(values) + for i <- 0 until values2.size do + val idx = names.indexOf(names2(i)) + values(idx) = values2.productElement(i).asInstanceOf[Object] + Tuple.fromArray(values).asInstanceOf[UpdateWith[NT1, NT2]] + +@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"] = names.indexOf("age") + val _: 2 = x2 + + val x3: IndexOf[Names, "what?"] = names.indexOf("what?") + val _: 3 = x3 + + type Releases = "first" *: "middle" *: EmptyTuple + type ReleaseValues = 1.0 *: true *: EmptyTuple + + val releases: Releases = ("first", "middle") + val releaseValues: ReleaseValues = (1.0, true) + + val x4 = values.updateOrAppend(names.indexOf("age"), 11) + //updateOrAppend[Values](values)[IndexOf[Names, "age"], 11](indexOf[Names](names)["age"]("age"), 11) + val _: ("Bob", "Miller", 11) = x4 + assert(("Bob", "Miller", 11) == x4) + + val x5 = 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"](values, "Peter") + val _: ("Peter", "Miller", 33) = x6 + assert(("Peter", "Miller", 33) == x6) + + val x7 = concatDistinct[Names, Releases](names, releases) + val _: ("first", "last", "age", "middle") = x7 + assert(("first", "last", "age", "middle") == x7, x7) + + val x8 = concatDistinct[Releases, Names](releases, names) + val _: ("first", "middle", "last", "age") = x8 + assert(("first", "middle", "last", "age") == x8) + + def x9: Merge[ConcatDistinct[Names, Releases], Values, Releases, ReleaseValues] = ??? + def x9c: NamedTuple[("first", "last", "age", "middle"), (1.0, "Miller", 33, true)] = x9 + + val person = NamedTuple[Names, Values](values) + val release = NamedTuple[Releases, ReleaseValues](releaseValues) + + val x10 = person.updateWith(release) + val _: UpdateWith[NamedTuple[Names, Values], NamedTuple[Releases, ReleaseValues]] = x10 + val _: ("first", "last", "age", "middle") = x10.names + val _: (1.0, "Miller", 33, true) = x10.toTuple + assert((("first", "last", "age", "middle") == x10.names)) + assert((1.0, "Miller", 33, true) == x10.toTuple) + + val x11 = release.updateWith(person) + val _: UpdateWith[NamedTuple[Releases, ReleaseValues], NamedTuple[Names, Values]] = x11 + val _: NamedTuple[("first", "middle", "last", "age"), ("Bob", true, "Miller", 33)] = x11 + assert(("first", "middle", "last", "age") == x11.names) + assert(("Bob", true, "Miller", 33) == x11.toTuple) 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..3a0a1e5e1294 --- /dev/null +++ b/tests/run/named-tuples-xxl.scala @@ -0,0 +1,91 @@ +import language.experimental.namedTuples +import NamedTuple.toTuple + +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 = NamedTuple.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) => + 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.toTuple + + 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) + val age1 = bob(12) + + 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.check b/tests/run/named-tuples.check new file mode 100644 index 000000000000..6485aefafa9a --- /dev/null +++ b/tests/run/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 +(((Lausanne,Pully),Preverenges),((1003,1009),1028)) diff --git a/tests/run/named-tuples.scala b/tests/run/named-tuples.scala new file mode 100644 index 000000000000..676c21a0e434 --- /dev/null +++ b/tests/run/named-tuples.scala @@ -0,0 +1,114 @@ +import language.experimental.namedTuples +import NamedTuple.* + +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, zipCode: Int) +val addr = (city = "Lausanne", zipCode = 1003) +val _: AddressInfo = addr + +type CombinedInfo = NamedTuple.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 = "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) + case _ => println("no match") + + val x = bob.age + assert(x == 33) + + val y: (String, Int) = bob.toTuple + + def ageOf(person: Person) = person.age + + assert(ageOf(bob) == 33) + 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) + val age1 = bob(1) + val _: String = name1 + val _: Int = age1 + + val bobS = bob.reverse + val _: (age: Int, name: String) = bobS + val _: NamedTuple.Reverse[Person] = 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 bob1 @ (age = 33, name = "Bob") => + val x: Person = bob1 // bob1 still has type Person with the unswapped elements + case _ => assert(false) + + val addr2 = (city = "Pully", zipCode = 1009) + val addr3 = addr.zip(addr2) + val addr4 = addr3.zip("Preverenges", 1028) + println(addr4) + + // testing conversions +object Conv: + + val p: (String, Int) = bob + def f22(x: (String, Int)) = x._1 + def f22(x: String) = x + f22(bob) + + + + + diff --git a/tests/run/tyql.scala b/tests/run/tyql.scala new file mode 100644 index 000000000000..35777e9a4c13 --- /dev/null +++ b/tests/run/tyql.scala @@ -0,0 +1,205 @@ +import language.experimental.namedTuples +import NamedTuple.{NamedTuple, AnyNamedTuple} + +/* This is a demonstrator that shows how to map regular for expressions to + * internal data that can be optimized by a query engine. It needs NamedTuples + * and type classes but no macros. It's so far very provisional and experimental, + * intended as a basis for further exploration. + */ + +/** The type of expressions in the query language */ +trait Expr[Result] extends Selectable: + + /** This type is used to support selection with any of the field names + * defined by Fields. + */ + type Fields = NamedTuple.Map[NamedTuple.From[Result], Expr] + + /** A selection of a field name defined by Fields is implemented by `selectDynamic`. + * The implementation will add a cast to the right Expr type corresponding + * to the field type. + */ + def selectDynamic(fieldName: String) = Expr.Select(this, fieldName) + + /** Member methods to implement universal equality on Expr level. */ + def == (other: Expr[?]): Expr[Boolean] = Expr.Eq(this, other) + def != (other: Expr[?]): Expr[Boolean] = Expr.Ne(this, other) + +object Expr: + + /** Sample extension methods for individual types */ + extension (x: Expr[Int]) + def > (y: Expr[Int]): Expr[Boolean] = Gt(x, y) + def > (y: Int): Expr[Boolean] = Gt(x, IntLit(y)) + extension (x: Expr[Boolean]) + def &&(y: Expr[Boolean]): Expr[Boolean] = And(x, y) + def || (y: Expr[Boolean]): Expr[Boolean] = Or(x, y) + + // Note: All field names of constructors in the query language are prefixed with `$` + // so that we don't accidentally pick a field name of a constructor class where we want + // a name in the domain model instead. + + // Some sample constructors for Exprs + case class Gt($x: Expr[Int], $y: Expr[Int]) extends Expr[Boolean] + case class Plus(x: Expr[Int], y: Expr[Int]) extends Expr[Int] + case class And($x: Expr[Boolean], $y: Expr[Boolean]) extends Expr[Boolean] + case class Or($x: Expr[Boolean], $y: Expr[Boolean]) extends Expr[Boolean] + + // So far Select is weakly typed, so `selectDynamic` is easy to implement. + // Todo: Make it strongly typed like the other cases + case class Select[A]($x: Expr[A], $name: String) extends Expr + + case class Single[S <: String, A]($x: Expr[A]) + extends Expr[NamedTuple[S *: EmptyTuple, A *: EmptyTuple]] + + case class Concat[A <: AnyNamedTuple, B <: AnyNamedTuple]($x: Expr[A], $y: Expr[B]) + extends Expr[NamedTuple.Concat[A, B]] + + case class Join[A <: AnyNamedTuple](a: A) + extends Expr[NamedTuple.Map[A, StripExpr]] + + type StripExpr[E] = E match + case Expr[b] => b + + // Also weakly typed in the arguents since these two classes model universal equality */ + case class Eq($x: Expr[?], $y: Expr[?]) extends Expr[Boolean] + case class Ne($x: Expr[?], $y: Expr[?]) extends Expr[Boolean] + + /** References are placeholders for parameters */ + private var refCount = 0 + + case class Ref[A]($name: String = "") extends Expr[A]: + val id = refCount + refCount += 1 + override def toString = s"ref$id(${$name})" + + /** Literals are type-specific, tailored to the types that the DB supports */ + case class IntLit($value: Int) extends Expr[Int] + + /** Scala values can be lifted into literals by conversions */ + given Conversion[Int, IntLit] = IntLit(_) + + /** The internal representation of a function `A => B` + * Query languages are ususally first-order, so Fun is not an Expr + */ + case class Fun[A, B](param: Ref[A], f: B) + + type Pred[A] = Fun[A, Expr[Boolean]] + + /** Explicit conversion from + * (name_1: Expr[T_1], ..., name_n: Expr[T_n]) + * to + * Expr[(name_1: T_1, ..., name_n: T_n)] + */ + extension [A <: AnyNamedTuple](x: A) def toRow: Join[A] = Join(x) + + /** Same as _.toRow, as an implicit conversion */ + given [A <: AnyNamedTuple]: Conversion[A, Expr.Join[A]] = Expr.Join(_) + +end Expr + +/** The type of database queries. So far, we have queries + * that represent whole DB tables and queries that reify + * for-expressions as data. + */ +trait Query[A] + +object Query: + import Expr.{Pred, Fun, Ref} + + case class Filter[A]($q: Query[A], $p: Pred[A]) extends Query[A] + case class Map[A, B]($q: Query[A], $f: Fun[A, Expr[B]]) extends Query[B] + case class FlatMap[A, B]($q: Query[A], $f: Fun[A, Query[B]]) extends Query[B] + + // Extension methods to support for-expression syntax for queries + extension [R](x: Query[R]) + + def withFilter(p: Ref[R] => Expr[Boolean]): Query[R] = + val ref = Ref[R]() + Filter(x, Fun(ref, p(ref))) + + def map[B](f: Ref[R] => Expr[B]): Query[B] = + val ref = Ref[R]() + Map(x, Fun(ref, f(ref))) + + def flatMap[B](f: Ref[R] => Query[B]): Query[B] = + val ref = Ref[R]() + FlatMap(x, Fun(ref, f(ref))) +end Query + +/** The type of query references to database tables */ +case class Table[R]($name: String) extends Query[R] + +// Everything below is code using the model ----------------------------- + +// Some sample types +case class City(zipCode: Int, name: String, population: Int) +type Address = (city: City, street: String, number: Int) +type Person = (name: String, age: Int, addr: Address) + +@main def Test = + + val cities = Table[City]("cities") + + val q1 = cities.map: c => + c.zipCode + val q2 = cities.withFilter: city => + city.population > 10_000 + .map: city => + city.name + + val q3 = + for + city <- cities + if city.population > 10_000 + yield city.name + + val q4 = + for + city <- cities + alt <- cities + if city.name == alt.name && city.zipCode != alt.zipCode + yield + city + + val addresses = Table[Address]("addresses") + val q5 = + for + city <- cities + addr <- addresses + if addr.street == city.name + yield + (name = city.name, num = addr.number) + + val q6 = + cities.map: city => + (name = city.name, zipCode = city.zipCode) + + def run[T](q: Query[T]): Iterator[T] = ??? + + def x1: Iterator[Int] = run(q1) + def x2: Iterator[String] = run(q2) + def x3: Iterator[String] = run(q3) + def x4: Iterator[City] = run(q4) + def x5: Iterator[(name: String, num: Int)] = run(q5) + def x6: Iterator[(name: String, zipCode: Int)] = run(q6) + + println(q1) + println(q2) + println(q3) + println(q4) + println(q5) + println(q6) + +/* The following is not needed currently + +/** A type class for types that can map to a database table */ +trait Row: + type Self + type Fields = NamedTuple.From[Self] + type FieldExprs = NamedTuple.Map[Fields, Expr] + + //def toFields(x: Self): Fields = ??? + //def fromFields(x: Fields): Self = ??? + +*/ \ No newline at end of file