From 0a8d19bf31aea94c60e74d63409d14e53c766ad3 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 30 Sep 2022 14:19:34 +0200 Subject: [PATCH 1/4] Implement `into` modifier on parameter types Source input: From Scala 3: `(x: into T)` From Scala 2: `(@allowConversions x: T)` Gets translated to (x: [T]) where `` is a new synthetic alias marker type defined as type [T] = T `into` is not accessible from user programs. Parameters labeled `into` allow implicit argument conversions without a import language.implicitConversions import. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 44 +++++---- compiler/src/dotty/tools/dotc/ast/untpd.scala | 8 ++ .../src/dotty/tools/dotc/config/Feature.scala | 1 + .../dotty/tools/dotc/core/Decorators.scala | 8 +- .../dotty/tools/dotc/core/Definitions.scala | 4 + .../src/dotty/tools/dotc/core/Names.scala | 4 +- .../tools/dotc/core/OrderingConstraint.scala | 2 +- .../src/dotty/tools/dotc/core/StdNames.scala | 2 + .../src/dotty/tools/dotc/core/Types.scala | 60 +++++++++--- .../dotty/tools/dotc/parsing/Parsers.scala | 11 ++- .../src/dotty/tools/dotc/parsing/Tokens.scala | 4 +- .../tools/dotc/printing/RefinedPrinter.scala | 1 + .../tools/dotc/transform/TypeUtils.scala | 2 +- .../src/dotty/tools/dotc/typer/Checking.scala | 9 +- .../src/dotty/tools/dotc/typer/Namer.scala | 6 +- .../src/dotty/tools/dotc/typer/Typer.scala | 7 +- .../dotty/tools/dotc/CompilationTests.scala | 6 +- docs/_docs/internals/syntax.md | 3 +- .../reference/experimental/into-modifier.md | 81 ++++++++++++++++ docs/sidebar.yml | 1 + .../scala/annotation/allowConversions.scala | 10 ++ .../runtime/stdLibPatches/language.scala | 8 ++ project/MiMaFilters.scala | 2 + .../neg-custom-args/feature/convertible.scala | 29 ++++++ .../{ => feature}/feature-shadowing.scala | 0 .../{ => feature}/i13946/BadPrinter.scala | 0 .../{ => feature}/i13946/Printer.scala | 0 .../{ => feature}/impl-conv/A.scala | 0 .../{ => feature}/impl-conv/B.scala | 0 .../implicit-conversions-old.scala | 0 .../{ => feature}/implicit-conversions.scala | 0 .../fatal-warnings/convertible.check | 4 + .../fatal-warnings/convertible.scala | 32 ++++++ .../stdlibExperimentalDefinitions.scala | 3 + tests/run/Parser.scala | 97 +++++++++++++++++++ 35 files changed, 390 insertions(+), 59 deletions(-) create mode 100644 docs/_docs/reference/experimental/into-modifier.md create mode 100644 library/src/scala/annotation/allowConversions.scala create mode 100644 tests/neg-custom-args/feature/convertible.scala rename tests/neg-custom-args/{ => feature}/feature-shadowing.scala (100%) rename tests/neg-custom-args/{ => feature}/i13946/BadPrinter.scala (100%) rename tests/neg-custom-args/{ => feature}/i13946/Printer.scala (100%) rename tests/neg-custom-args/{ => feature}/impl-conv/A.scala (100%) rename tests/neg-custom-args/{ => feature}/impl-conv/B.scala (100%) rename tests/neg-custom-args/{ => feature}/implicit-conversions-old.scala (100%) rename tests/neg-custom-args/{ => feature}/implicit-conversions.scala (100%) create mode 100644 tests/run-custom-args/fatal-warnings/convertible.check create mode 100644 tests/run-custom-args/fatal-warnings/convertible.scala create mode 100644 tests/run/Parser.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 1e1db19bcf25..3454e31b55a1 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -6,6 +6,7 @@ import core._ import util.Spans._, Types._, Contexts._, Constants._, Names._, NameOps._, Flags._ import Symbols._, StdNames._, Trees._, ContextOps._ import Decorators._, transform.SymUtils._ +import Annotations.Annotation import NameKinds.{UniqueName, EvidenceParamName, DefaultGetterName, WildcardParamName} import typer.{Namer, Checking} import util.{Property, SourceFile, SourcePosition, Chars} @@ -165,32 +166,41 @@ object desugar { * * Generate setter where needed */ - def valDef(vdef0: ValDef)(using Context): Tree = { + def valDef(vdef0: ValDef)(using Context): Tree = val vdef @ ValDef(_, tpt, rhs) = vdef0 - val mods = vdef.mods - val valName = normalizeName(vdef, tpt).asTermName - val vdef1 = cpy.ValDef(vdef)(name = valName) + var mods1 = vdef.mods + + def dropInto(tpt: Tree): Tree = tpt match + case Into(tpt1) => + mods1 = vdef.mods.withAddedAnnotation( + TypedSplice( + Annotation(defn.AllowConversionsAnnot).tree.withSpan(tpt.span.startPos))) + tpt1 + case ByNameTypeTree(tpt1) => + cpy.ByNameTypeTree(tpt)(dropInto(tpt1)) + case PostfixOp(tpt1, op) if op.name == tpnme.raw.STAR => + cpy.PostfixOp(tpt)(dropInto(tpt1), op) + case _ => + tpt + + val vdef1 = cpy.ValDef(vdef)(name = valName, tpt = dropInto(tpt)) + .withMods(mods1) - if (isSetterNeeded(vdef)) { - // TODO: copy of vdef as getter needed? - // val getter = ValDef(mods, name, tpt, rhs) withPos vdef.pos? - // right now vdef maps via expandedTree to a thicket which concerns itself. - // I don't see a problem with that but if there is one we can avoid it by making a copy here. + if isSetterNeeded(vdef) then val setterParam = makeSyntheticParameter(tpt = SetterParamTree().watching(vdef)) // The rhs gets filled in later, when field is generated and getter has parameters (see Memoize miniphase) val setterRhs = if (vdef.rhs.isEmpty) EmptyTree else unitLiteral val setter = cpy.DefDef(vdef)( - name = valName.setterName, - paramss = (setterParam :: Nil) :: Nil, - tpt = TypeTree(defn.UnitType), - rhs = setterRhs - ).withMods((mods | Accessor) &~ (CaseAccessor | GivenOrImplicit | Lazy)) - .dropEndMarker() // the end marker should only appear on the getter definition + name = valName.setterName, + paramss = (setterParam :: Nil) :: Nil, + tpt = TypeTree(defn.UnitType), + rhs = setterRhs + ).withMods((vdef.mods | Accessor) &~ (CaseAccessor | GivenOrImplicit | Lazy)) + .dropEndMarker() // the end marker should only appear on the getter definition Thicket(vdef1, setter) - } else vdef1 - } + end valDef def makeImplicitParameters(tpts: List[Tree], implicitFlag: FlagSet, forPrimaryConstructor: Boolean = false)(using Context): List[ValDef] = for (tpt <- tpts) yield { diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index f72cafd4205d..8a6ba48d22c5 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -117,6 +117,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class ContextBounds(bounds: TypeBoundsTree, cxBounds: List[Tree])(implicit @constructorOnly src: SourceFile) extends TypTree case class PatDef(mods: Modifiers, pats: List[Tree], tpt: Tree, rhs: Tree)(implicit @constructorOnly src: SourceFile) extends DefTree case class ExtMethods(paramss: List[ParamClause], methods: List[Tree])(implicit @constructorOnly src: SourceFile) extends Tree + case class Into(tpt: Tree)(implicit @constructorOnly src: SourceFile) extends Tree case class MacroTree(expr: Tree)(implicit @constructorOnly src: SourceFile) extends Tree case class ImportSelector(imported: Ident, renamed: Tree = EmptyTree, bound: Tree = EmptyTree)(implicit @constructorOnly src: SourceFile) extends Tree { @@ -649,6 +650,9 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def ExtMethods(tree: Tree)(paramss: List[ParamClause], methods: List[Tree])(using Context): Tree = tree match case tree: ExtMethods if (paramss eq tree.paramss) && (methods == tree.methods) => tree case _ => finalize(tree, untpd.ExtMethods(paramss, methods)(tree.source)) + def Into(tree: Tree)(tpt: Tree)(using Context): Tree = tree match + case tree: Into if tpt eq tree.tpt => tree + case _ => finalize(tree, untpd.Into(tpt)(tree.source)) def ImportSelector(tree: Tree)(imported: Ident, renamed: Tree, bound: Tree)(using Context): Tree = tree match { case tree: ImportSelector if (imported eq tree.imported) && (renamed eq tree.renamed) && (bound eq tree.bound) => tree case _ => finalize(tree, untpd.ImportSelector(imported, renamed, bound)(tree.source)) @@ -718,6 +722,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { cpy.PatDef(tree)(mods, transform(pats), transform(tpt), transform(rhs)) case ExtMethods(paramss, methods) => cpy.ExtMethods(tree)(transformParamss(paramss), transformSub(methods)) + case Into(tpt) => + cpy.Into(tree)(transform(tpt)) case ImportSelector(imported, renamed, bound) => cpy.ImportSelector(tree)(transformSub(imported), transform(renamed), transform(bound)) case Number(_, _) | TypedSplice(_) => @@ -777,6 +783,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { this(this(this(x, pats), tpt), rhs) case ExtMethods(paramss, methods) => this(paramss.foldLeft(x)(apply), methods) + case Into(tpt) => + this(x, tpt) case ImportSelector(imported, renamed, bound) => this(this(this(x, imported), renamed), bound) case Number(_, _) => diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index e7117f542384..6ebb95cc7f2d 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -30,6 +30,7 @@ object Feature: val saferExceptions = experimental("saferExceptions") val pureFunctions = experimental("pureFunctions") val captureChecking = experimental("captureChecking") + val into = experimental("into") val globalOnlyImports: Set[TermName] = Set(pureFunctions, captureChecking) diff --git a/compiler/src/dotty/tools/dotc/core/Decorators.scala b/compiler/src/dotty/tools/dotc/core/Decorators.scala index 54faf9a41177..e469818541ed 100644 --- a/compiler/src/dotty/tools/dotc/core/Decorators.scala +++ b/compiler/src/dotty/tools/dotc/core/Decorators.scala @@ -78,7 +78,7 @@ object Decorators { /** Implements filterConserve, zipWithConserve methods * on lists that avoid duplication of list nodes where feasible. */ - implicit class ListDecorator[T](val xs: List[T]) extends AnyVal { + extension [T](xs: List[T]) final def mapconserve[U](f: T => U): List[U] = { @tailrec @@ -207,11 +207,7 @@ object Decorators { } /** Union on lists seen as sets */ - def | (ys: List[T]): List[T] = xs ::: (ys filterNot (xs contains _)) - - /** Intersection on lists seen as sets */ - def & (ys: List[T]): List[T] = xs filter (ys contains _) - } + def setUnion (ys: List[T]): List[T] = xs ::: ys.filterNot(xs contains _) extension [T, U](xss: List[List[T]]) def nestedMap(f: T => U): List[List[U]] = xss match diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index b43857b7d28c..fccbac537b24 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -644,6 +644,8 @@ class Definitions { @tu lazy val RepeatedParamClass: ClassSymbol = enterSpecialPolyClass(tpnme.REPEATED_PARAM_CLASS, Covariant, Seq(ObjectType, SeqType)) + @tu lazy val IntoType: TypeSymbol = enterAliasType(tpnme.INTO, HKTypeLambda(TypeBounds.empty :: Nil)(_.paramRefs(0))) + // fundamental classes @tu lazy val StringClass: ClassSymbol = requiredClass("java.lang.String") def StringType: Type = StringClass.typeRef @@ -973,6 +975,7 @@ class Definitions { @tu lazy val RefiningAnnotationClass: ClassSymbol = requiredClass("scala.annotation.RefiningAnnotation") // Annotation classes + @tu lazy val AllowConversionsAnnot: ClassSymbol = requiredClass("scala.annotation.allowConversions") @tu lazy val AnnotationDefaultAnnot: ClassSymbol = requiredClass("scala.annotation.internal.AnnotationDefault") @tu lazy val BeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BeanProperty") @tu lazy val BooleanBeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BooleanBeanProperty") @@ -2005,6 +2008,7 @@ class Definitions { orType, RepeatedParamClass, ByNameParamClass2x, + IntoType, AnyValClass, NullClass, NothingClass, diff --git a/compiler/src/dotty/tools/dotc/core/Names.scala b/compiler/src/dotty/tools/dotc/core/Names.scala index f13c3a184bf9..7932ad7727ef 100644 --- a/compiler/src/dotty/tools/dotc/core/Names.scala +++ b/compiler/src/dotty/tools/dotc/core/Names.scala @@ -15,8 +15,8 @@ import scala.annotation.internal.sharable object Names { import NameKinds._ - /** Things that can be turned into names with `totermName` and `toTypeName` - * Decorators defines implements these as extension methods for strings. + /** Things that can be turned into names with `toTermName` and `toTypeName`. + * Decorators implements these as extension methods for strings. */ type PreName = Name | String diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index ac6cb78f9e91..0f974252812e 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -626,7 +626,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds, case param: TypeParamRef if contains(param) => param :: (if (isUpper) upper(param) else lower(param)) case tp: AndType if isUpper => - dependentParams(tp.tp1, isUpper) | (dependentParams(tp.tp2, isUpper)) + dependentParams(tp.tp1, isUpper).setUnion(dependentParams(tp.tp2, isUpper)) case tp: OrType if !isUpper => dependentParams(tp.tp1, isUpper).intersect(dependentParams(tp.tp2, isUpper)) case EtaExpansion(tycon) => diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 50c96191143c..dff423fd0bb4 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -128,6 +128,7 @@ object StdNames { val EXCEPTION_RESULT_PREFIX: N = "exceptionResult" val EXPAND_SEPARATOR: N = str.EXPAND_SEPARATOR val IMPORT: N = "" + val INTO: N = "" val MODULE_SUFFIX: N = str.MODULE_SUFFIX val OPS_PACKAGE: N = "" val OVERLOADED: N = "" @@ -500,6 +501,7 @@ object StdNames { val info: N = "info" val inlinedEquals: N = "inlinedEquals" val internal: N = "internal" + val into: N = "into" val isArray: N = "isArray" val isDefinedAt: N = "isDefinedAt" val isDefinedAtImpl: N = "$isDefinedAt" diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 29a2496ab2a7..f859fc787544 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -397,6 +397,10 @@ object Types { def isRepeatedParam(using Context): Boolean = typeSymbol eq defn.RepeatedParamClass + /** Is this a parameter type that allows implicit argument converson? */ + def isConvertibleParam(using Context): Boolean = + typeSymbol eq defn.IntoType + /** Is this the type of a method that has a repeated parameter type as * last parameter type? */ @@ -536,7 +540,7 @@ object Types { case tp: ClassInfo => tp.cls :: Nil case AndType(l, r) => - l.parentSymbols(include) | r.parentSymbols(include) + l.parentSymbols(include).setUnion(r.parentSymbols(include)) case OrType(l, r) => l.parentSymbols(include) intersect r.parentSymbols(include) // TODO does not conform to spec case _ => @@ -1864,6 +1868,11 @@ object Types { def dropRepeatedAnnot(using Context): Type = dropAnnot(defn.RepeatedAnnot) + /** A translation from types of original parameter ValDefs to the types + * of parameters in MethodTypes. + * Translates `Seq[T] @repeated` or `Array[T] @repeated` to `[T]`. + * That way, repeated arguments are made manifest without risk of dropped annotations. + */ def annotatedToRepeated(using Context): Type = this match { case tp @ ExprType(tp1) => tp.derivedExprType(tp1.annotatedToRepeated) @@ -3948,27 +3957,48 @@ object Types { * and inline parameters: * - replace @repeated annotations on Seq or Array types by types * - add @inlineParam to inline parameters + * - add @erasedParam to erased parameters + * - wrap types of parameters that have an @allowConversions annotation with Into[_] */ - def fromSymbols(params: List[Symbol], resultType: Type)(using Context): MethodType = { - def translateInline(tp: Type): Type = tp match { - case ExprType(resType) => ExprType(AnnotatedType(resType, Annotation(defn.InlineParamAnnot))) - case _ => AnnotatedType(tp, Annotation(defn.InlineParamAnnot)) - } - def translateErased(tp: Type): Type = tp match { - case ExprType(resType) => ExprType(AnnotatedType(resType, Annotation(defn.ErasedParamAnnot))) - case _ => AnnotatedType(tp, Annotation(defn.ErasedParamAnnot)) - } - def paramInfo(param: Symbol) = { + def fromSymbols(params: List[Symbol], resultType: Type)(using Context): MethodType = + def addAnnotation(tp: Type, cls: ClassSymbol): Type = tp match + case ExprType(resType) => ExprType(addAnnotation(resType, cls)) + case _ => AnnotatedType(tp, Annotation(cls)) + + def wrapConvertible(tp: Type) = + AppliedType(defn.IntoType.typeRef, tp :: Nil) + + /** Add `Into[..] to the type itself and if it is a function type, to all its + * curried result type(s) as well. + */ + def addInto(tp: Type): Type = tp match + case tp @ AppliedType(tycon, args) if tycon.typeSymbol == defn.RepeatedParamClass => + tp.derivedAppliedType(tycon, addInto(args.head) :: Nil) + case tp @ AppliedType(tycon, args) if defn.isFunctionType(tp) => + wrapConvertible(tp.derivedAppliedType(tycon, args.init :+ addInto(args.last))) + case tp @ RefinedType(parent, rname, rinfo) if defn.isFunctionType(tp) => + wrapConvertible(tp.derivedRefinedType(parent, rname, addInto(rinfo))) + case tp: MethodType => + tp.derivedLambdaType(resType = addInto(tp.resType)) + case ExprType(resType) => + ExprType(addInto(resType)) + case _ => + wrapConvertible(tp) + + def paramInfo(param: Symbol) = var paramType = param.info.annotatedToRepeated - if (param.is(Inline)) paramType = translateInline(paramType) - if (param.is(Erased)) paramType = translateErased(paramType) + if param.is(Inline) then + paramType = addAnnotation(paramType, defn.InlineParamAnnot) + if param.is(Erased) then + paramType = addAnnotation(paramType, defn.ErasedParamAnnot) + if param.hasAnnotation(defn.AllowConversionsAnnot) then + paramType = addInto(paramType) paramType - } apply(params.map(_.name.asTermName))( tl => params.map(p => tl.integrate(params, paramInfo(p))), tl => tl.integrate(params, resultType)) - } + end fromSymbols final def apply(paramNames: List[TermName])(paramInfosExp: MethodType => List[Type], resultTypeExp: MethodType => Type)(using Context): MethodType = checkValid(unique(new CachedMethodType(paramNames)(paramInfosExp, resultTypeExp, self))) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index a198cccc85cc..792e0a681482 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1917,6 +1917,13 @@ object Parsers { else core() + private def maybeInto(tp: () => Tree) = + if in.isIdent(nme.into) + && in.featureEnabled(Feature.into) + && canStartTypeTokens.contains(in.lookahead.token) + then atSpan(in.skipToken()) { Into(tp()) } + else tp() + /** FunArgType ::= Type * | `=>' Type * | [CaptureSet] `->' Type @@ -1929,10 +1936,10 @@ object Parsers { */ def paramType(): Tree = paramTypeOf(paramValueType) - /** ParamValueType ::= Type [`*'] + /** ParamValueType ::= [`into`] Type [`*'] */ def paramValueType(): Tree = { - val t = toplevelTyp() + val t = maybeInto(toplevelTyp) if (isIdent(nme.raw.STAR)) { in.nextToken() atSpan(startOffset(t)) { PostfixOp(t, Ident(tpnme.raw.STAR)) } diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 7d27b3ca82b9..dba0ad3fa2ee 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -231,6 +231,8 @@ object Tokens extends TokensCommon { final val canStartInfixTypeTokens: TokenSet = literalTokens | identifierTokens | BitSet( THIS, SUPER, USCORE, LPAREN, LBRACE, AT) + final val canStartTypeTokens: TokenSet = canStartInfixTypeTokens | BitSet(LBRACE) + final val templateIntroTokens: TokenSet = BitSet(CLASS, TRAIT, OBJECT, ENUM, CASECLASS, CASEOBJECT) final val dclIntroTokens: TokenSet = BitSet(DEF, VAL, VAR, TYPE, GIVEN) @@ -287,7 +289,7 @@ object Tokens extends TokensCommon { final val closingParens = BitSet(RPAREN, RBRACKET, RBRACE) - final val softModifierNames = Set(nme.inline, nme.opaque, nme.open, nme.transparent, nme.infix) + final val softModifierNames = Set(nme.inline, nme.into, nme.opaque, nme.open, nme.transparent, nme.infix) def showTokenDetailed(token: Int): String = debugString(token) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 62e1cd5baec8..7e27ef6a311a 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -223,6 +223,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case _ => val tsym = tycon.typeSymbol if tycon.isRepeatedParam then toTextLocal(args.head) ~ "*" + else if tp.isConvertibleParam then "into " ~ toText(args.head) else if defn.isFunctionSymbol(tsym) then toTextFunction(args, tsym.name.isContextFunction, tsym.name.isErasedFunction, isPure = Feature.pureFunsEnabled && !tsym.name.isImpureFunction) diff --git a/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala b/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala index 5b6e36343379..a897503ef275 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala @@ -76,7 +76,7 @@ object TypeUtils { case AndType(tp1, tp2) => // We assume that we have the following property: // (T1, T2, ..., Tn) & (U1, U2, ..., Un) = (T1 & U1, T2 & U2, ..., Tn & Un) - tp1.tupleElementTypes.zip(tp2.tupleElementTypes).map { case (t1, t2) => t1 & t2 } + tp1.tupleElementTypes.zip(tp2.tupleElementTypes).map { case (t1, t2) => t1.intersect(t2) } case OrType(tp1, tp2) => None // We can't combine the type of two tuples case _ => diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 99399832085f..862e2070032b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -979,14 +979,15 @@ trait Checking { sym.srcPos) /** If `tree` is an application of a new-style implicit conversion (using the apply - * method of a `scala.Conversion` instance), check that implicit conversions are - * enabled. + * method of a `scala.Conversion` instance), check that the expected type is + * a convertible formal parameter type or that implicit conversions are enabled. */ - def checkImplicitConversionUseOK(tree: Tree)(using Context): Unit = + def checkImplicitConversionUseOK(tree: Tree, expected: Type)(using Context): Unit = val sym = tree.symbol if sym.name == nme.apply && sym.owner.derivesFrom(defn.ConversionClass) && !sym.info.isErroneous + && !expected.isConvertibleParam then def conv = methPart(tree) match case Select(qual, _) => qual.symbol.orElse(sym.owner) @@ -1536,7 +1537,7 @@ trait NoChecking extends ReChecking { override def checkStable(tp: Type, pos: SrcPos, kind: String)(using Context): Unit = () override def checkClassType(tp: Type, pos: SrcPos, traitReq: Boolean, stablePrefixReq: Boolean)(using Context): Type = tp override def checkImplicitConversionDefOK(sym: Symbol)(using Context): Unit = () - override def checkImplicitConversionUseOK(tree: Tree)(using Context): Unit = () + override def checkImplicitConversionUseOK(tree: Tree, expected: Type)(using Context): Unit = () override def checkFeasibleParent(tp: Type, pos: SrcPos, where: => String = "")(using Context): Type = tp override def checkAnnotArgs(tree: Tree)(using Context): tree.type = tree override def checkNoTargetNameConflict(stats: List[Tree])(using Context): Unit = () diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 6aab561c44b7..6e0131f1a0b1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1618,12 +1618,14 @@ class Namer { typer: Typer => def typedAheadExpr(tree: Tree, pt: Type = WildcardType)(using Context): tpd.Tree = typedAhead(tree, typer.typedExpr(_, pt)) - def typedAheadAnnotationClass(tree: Tree)(using Context): Symbol = tree match { + def typedAheadAnnotationClass(tree: Tree)(using Context): Symbol = tree match case Apply(fn, _) => typedAheadAnnotationClass(fn) case TypeApply(fn, _) => typedAheadAnnotationClass(fn) case Select(qual, nme.CONSTRUCTOR) => typedAheadAnnotationClass(qual) case New(tpt) => typedAheadType(tpt).tpe.classSymbol - } + case TypedSplice(_) => + val sym = tree.symbol + if sym.isConstructor then sym.owner else sym /** Enter and typecheck parameter list */ def completeParams(params: List[MemberDef])(using Context): Unit = { diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 032bed38482c..93903843f822 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2098,6 +2098,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer && checkedArgs(1).tpe.derivesFrom(defn.RuntimeExceptionClass) then report.error(em"throws clause cannot be defined for RuntimeException", checkedArgs(1).srcPos) + else if tycon == defn.IntoType then + // is defined in package scala but this should be hidden from user programs + report.error(em"not found: ", tpt1.srcPos) else if (ctx.isJava) if tycon eq defn.ArrayClass then checkedArgs match { @@ -3352,7 +3355,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case SearchSuccess(found, _, _, isExtension) => if isExtension then return found else - checkImplicitConversionUseOK(found) + checkImplicitConversionUseOK(found, selProto) return withoutMode(Mode.ImplicitsEnabled)(typedSelect(tree, pt, found)) case failure: SearchFailure => if failure.isAmbiguous then @@ -3996,7 +3999,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case SearchSuccess(found, _, _, isExtension) => if isExtension then found else - checkImplicitConversionUseOK(found) + checkImplicitConversionUseOK(found, pt) withoutMode(Mode.ImplicitsEnabled)(readapt(found)) case failure: SearchFailure => if (pt.isInstanceOf[ProtoType] && !failure.isAmbiguous) then diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index c1b465ad4a88..500cc6cbe17f 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -142,13 +142,10 @@ class CompilationTests { compileFilesInDir("tests/neg-custom-args/erased", defaultOptions.and("-language:experimental.erasedDefinitions")), compileFilesInDir("tests/neg-custom-args/allow-double-bindings", allowDoubleBindings), compileFilesInDir("tests/neg-custom-args/allow-deep-subtypes", allowDeepSubtypes), + compileFilesInDir("tests/neg-custom-args/feature", defaultOptions.and("-Xfatal-warnings", "-feature")), compileFilesInDir("tests/neg-custom-args/no-experimental", defaultOptions.and("-Yno-experimental")), compileFilesInDir("tests/neg-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking")), - compileDir("tests/neg-custom-args/impl-conv", defaultOptions.and("-Xfatal-warnings", "-feature")), - compileDir("tests/neg-custom-args/i13946", defaultOptions.and("-Xfatal-warnings", "-feature")), compileFile("tests/neg-custom-args/avoid-warn-deprecation.scala", defaultOptions.and("-Xfatal-warnings", "-feature")), - compileFile("tests/neg-custom-args/implicit-conversions.scala", defaultOptions.and("-Xfatal-warnings", "-feature")), - compileFile("tests/neg-custom-args/implicit-conversions-old.scala", defaultOptions.and("-Xfatal-warnings", "-feature")), compileFile("tests/neg-custom-args/i3246.scala", scala2CompatMode), compileFile("tests/neg-custom-args/overrideClass.scala", scala2CompatMode), compileFile("tests/neg-custom-args/ovlazy.scala", scala2CompatMode.and("-Xfatal-warnings")), @@ -189,7 +186,6 @@ class CompilationTests { compileFile("tests/neg-custom-args/matchable.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")), compileFile("tests/neg-custom-args/i7314.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")), compileFile("tests/neg-custom-args/capt-wf.scala", defaultOptions.and("-language:experimental.captureChecking", "-Xfatal-warnings")), - compileFile("tests/neg-custom-args/feature-shadowing.scala", defaultOptions.and("-Xfatal-warnings", "-feature")), compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")), compileFile("tests/neg-custom-args/i13026.scala", defaultOptions.and("-print-lines")), compileFile("tests/neg-custom-args/i13838.scala", defaultOptions.and("-Ximplicit-search-limit", "1000")), diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 7fce82cbebbc..76664569bb17 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -211,7 +211,8 @@ FunArgType ::= Type | ‘=>’ Type PrefixOp(=>, t) FunArgTypes ::= FunArgType { ‘,’ FunArgType } ParamType ::= [‘=>’] ParamValueType -ParamValueType ::= Type [‘*’] PostfixOp(t, "*") +ParamValueType ::= [‘into’] ExactParamType Into(t) +ExactParamType ::= ParamValueType [‘*’] PostfixOp(t, "*") TypeArgs ::= ‘[’ Types ‘]’ ts Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> ds TypeBounds ::= [‘>:’ Type] [‘<:’ Type] TypeBoundsTree(lo, hi) diff --git a/docs/_docs/reference/experimental/into-modifier.md b/docs/_docs/reference/experimental/into-modifier.md new file mode 100644 index 000000000000..c50b8fa182d1 --- /dev/null +++ b/docs/_docs/reference/experimental/into-modifier.md @@ -0,0 +1,81 @@ +--- +layout: doc-page +title: "The `into` Type Modifier" +redirectFrom: /docs/reference/other-new-features/into-modifier.html +nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/into-modifier.html +--- + +Scala 3's implicit conversions of the `scala.Conversion` class require a language import +``` +import scala.language.implicitConversions +``` +in any code that uses them as implicit conversions (code that calls conversions explicitly is not affected). If the import is missing, a feature warning is currently issued, and this will become an error in a future version of Scala 3. The motivation for this restriction is that code with hidden implicit conversions is hard to understand and might have correctness or performance problems that go undetected. + +There is one broad use case, however, where implicit conversions are very hard to replace. This is the case where an implicit conversion is used to adapt a method argument to its formal parameter type. An example from the standard library: +```scala +scala> val xs = List(0, 1) +scala> val ys = Array(2, 3) +scala> xs ++ ys +val res0: List[Int] = List(0, 1, 2, 3) +``` +The last input made use of an implicit conversion from `Array[Int]` to `IterableOnce[Int]` which is defined as a Scala 2 style implicit conversion in the standard library. Once the standard library is rewritten with Scala 3 conversions, this will +require a language import at the use site, which is clearly unacceptable. It is possible to avoid the need for implicit conversions using method overloading or type classes, but this often leads to longer and more complicated code, and neither of these alternatives work for vararg parameters. + +This is where the `into` modifier on parameter types comes in. Here is a signature of the `++` method on `List[A]` that uses it: +```scala + def ++ (elems: into IterableOnce[A]): List[A] +``` +The `into` modifier on the type of `elems` means that implicit conversions can be applied to convert the actual argument to an `IterableOnce` value, and this without needing a language import. + +## Function arguments + +`into` also allows conversions on the results of function arguments. For instance, consider the new proposed signature of the `flatMap` method on `List[A]`: + +```scala + def flatMap[B](f: into A => IterableOnce[B]): List[B] +``` +This allows a conversion of the actual argument to the function type `A => IterableOnce[B]`. Crucially, it also allows that conversion to be applied to +the function result. So the following would work: +```scala +scala> val xs = List(1, 2, 3) +scala> xs.flatMap(x => x.toString * x) +val res2: List[Char] = List(1, 2, 2, 3, 3, 3) +``` +Here, the conversion from `String` to `Iterable[Char]` is applied on the results of `flatMap`'s function argument when it is applied to the elements of `xs`. + +## Vararg arguments + +When applied to a vararg parameter, `into` allows a conversion on each argument value individually. For example, consider a method `concatAll` that concatenates a variable +number of `Seq[Char]` arguments, and also allows implicit conversions into `Seq[Char`: + +```scala +def concatAll(xss: into IterableOnce[Char]*): List[Char] = + xss.foldLeft(List[Char]())(_ ++ _) +``` +Here, the call +```scala +concatAll(List('a'), "bc", Array('d', 'e')) +``` +would apply two _different_ implicit conversions: the conversion from `String` to `List[Char]` gets applied to the second argument and the conversion from `Array[Char]` to `Iterable[Char]` gets applied to the third argument. + +## Retrofitting Scala 2 libraries + +A new annotation `allowConversions` has the same effect as an `into` modifier. It is defined as an `@experimental` class in package `scala.annotation`. It is intended to be used for retrofitting Scala 2 library code so that Scala 3 conversions can be applied to arguments without language imports. For instance, the definitions of +`++` and `flatMap` in the Scala 2.13 `List` class could be retrofitted as follows. +```scala + def ++ (@allowConversions elems: IterableOnce[A]): List[A] + def flatMap[B](@allowConversions f: A => IterableOnce[B]): List[B] +``` +For Scala 3 code, the `into` modifier is preferred. First, because it is shorter, +and second, because it adheres to the principle that annotations should not influence +typing and type inference in Scala. + +## Syntax changes + +The addition to the grammar is: +``` +ParamType ::= [‘=>’] ParamValueType +ParamValueType ::= [‘into‘] ExactParamType +ExactParamType ::= Type [‘*’] +``` +As the grammar shows, `into` can only applied to the type of a parameter; it is illegal in other positions. diff --git a/docs/sidebar.yml b/docs/sidebar.yml index e959e31e87de..2300e5cc624f 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -151,6 +151,7 @@ subsection: - page: reference/experimental/numeric-literals.md - page: reference/experimental/explicit-nulls.md - page: reference/experimental/main-annotation.md + - page: reference/experimental/into-modifier.md - page: reference/experimental/cc.md - page: reference/experimental/purefuns.md - page: reference/experimental/tupled-function.md diff --git a/library/src/scala/annotation/allowConversions.scala b/library/src/scala/annotation/allowConversions.scala new file mode 100644 index 000000000000..9d752ee26d21 --- /dev/null +++ b/library/src/scala/annotation/allowConversions.scala @@ -0,0 +1,10 @@ +package scala.annotation +import annotation.experimental + +/** An annotation on a parameter type that allows implicit conversions + * for its arguments. Intended for use by Scala 2, to annotate Scala 2 + * libraries. Scala 3 uses the `into` modifier on the parameter + * type instead. + */ +@experimental +class allowConversions extends scala.annotation.StaticAnnotation diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 53fbc15269c9..24b71a0ff0bd 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -74,6 +74,14 @@ object language: */ @compileTimeOnly("`captureChecking` can only be used at compile time in import statements") object captureChecking + + /** Experimental support for automatic conversions of arguments, without requiring + * a langauge import `import scala.language.implicitConversions`. + * + * @see [[https://dotty.epfl.ch/docs/reference/experimental/cc]] + */ + @compileTimeOnly("`into` can only be used at compile time in import statements") + object into end experimental /** The deprecated object contains features that are no longer officially suypported in Scala. diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 408930bbeee9..4a66d8671cd8 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -14,6 +14,8 @@ object MiMaFilters { ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.LazyVals.Evaluating"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.LazyVals.NullValue"), + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.into"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$into$"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.pureFunctions"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.captureChecking"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$pureFunctions$"), diff --git a/tests/neg-custom-args/feature/convertible.scala b/tests/neg-custom-args/feature/convertible.scala new file mode 100644 index 000000000000..1b9e1c79f011 --- /dev/null +++ b/tests/neg-custom-args/feature/convertible.scala @@ -0,0 +1,29 @@ +import language.experimental.into + +class Text(val str: String) + +object Test: + + given Conversion[String, Text] = Text(_) + + def f(x: Text, y: => Text, zs: Text*) = + println(s"${x.str} ${y.str} ${zs.map(_.str).mkString(" ")}") + + f("abc", "def") // error // error + f("abc", "def", "xyz", "uvw") // error // error // error // error + f("abc", "def", "xyz", Text("uvw")) // error // error // error + + def g(x: into Text) = + println(x.str) + + + g("abc") // OK + val gg = g + gg("abc") // straight eta expansion is also OK + + def h1[X](x: X)(y: X): Unit = () + + def h(x: into Text) = + val y = h1(x) + y("abc") // error, inference through type variable does not propagate + diff --git a/tests/neg-custom-args/feature-shadowing.scala b/tests/neg-custom-args/feature/feature-shadowing.scala similarity index 100% rename from tests/neg-custom-args/feature-shadowing.scala rename to tests/neg-custom-args/feature/feature-shadowing.scala diff --git a/tests/neg-custom-args/i13946/BadPrinter.scala b/tests/neg-custom-args/feature/i13946/BadPrinter.scala similarity index 100% rename from tests/neg-custom-args/i13946/BadPrinter.scala rename to tests/neg-custom-args/feature/i13946/BadPrinter.scala diff --git a/tests/neg-custom-args/i13946/Printer.scala b/tests/neg-custom-args/feature/i13946/Printer.scala similarity index 100% rename from tests/neg-custom-args/i13946/Printer.scala rename to tests/neg-custom-args/feature/i13946/Printer.scala diff --git a/tests/neg-custom-args/impl-conv/A.scala b/tests/neg-custom-args/feature/impl-conv/A.scala similarity index 100% rename from tests/neg-custom-args/impl-conv/A.scala rename to tests/neg-custom-args/feature/impl-conv/A.scala diff --git a/tests/neg-custom-args/impl-conv/B.scala b/tests/neg-custom-args/feature/impl-conv/B.scala similarity index 100% rename from tests/neg-custom-args/impl-conv/B.scala rename to tests/neg-custom-args/feature/impl-conv/B.scala diff --git a/tests/neg-custom-args/implicit-conversions-old.scala b/tests/neg-custom-args/feature/implicit-conversions-old.scala similarity index 100% rename from tests/neg-custom-args/implicit-conversions-old.scala rename to tests/neg-custom-args/feature/implicit-conversions-old.scala diff --git a/tests/neg-custom-args/implicit-conversions.scala b/tests/neg-custom-args/feature/implicit-conversions.scala similarity index 100% rename from tests/neg-custom-args/implicit-conversions.scala rename to tests/neg-custom-args/feature/implicit-conversions.scala diff --git a/tests/run-custom-args/fatal-warnings/convertible.check b/tests/run-custom-args/fatal-warnings/convertible.check new file mode 100644 index 000000000000..9cf235b70fda --- /dev/null +++ b/tests/run-custom-args/fatal-warnings/convertible.check @@ -0,0 +1,4 @@ +abc def +abc def xyz uvw +abc def xyz uvw +hi diff --git a/tests/run-custom-args/fatal-warnings/convertible.scala b/tests/run-custom-args/fatal-warnings/convertible.scala new file mode 100644 index 000000000000..3479f53df3c8 --- /dev/null +++ b/tests/run-custom-args/fatal-warnings/convertible.scala @@ -0,0 +1,32 @@ +import language.experimental.into + +class Text(val str: String) + +given Conversion[String, Text] = Text(_) + +@main def Test = + + def f(x: into Text, y: => into Text, zs: into Text*) = + println(s"${x.str} ${y.str} ${zs.map(_.str).mkString(" ")}") + + f("abc", "def") // ok + f("abc", "def", "xyz", "uvw") // ok + f("abc", "def", "xyz", Text("uvw")) // ok + + def g(x: into () => Text) = + println(x().str) + + g(() => "hi") + +trait A[X]: + def f(x: X): Unit = () + +trait B[X] extends A[X]: + override def f(x: X) = super.f(x) + +trait C[X] extends A[X]: + override def f(x: into X) = super.f(x) + +class D[X] extends B[X], C[X] + +def f = new D[Text].f("abc") diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index a7c437242ebe..9ce85f6c9b6b 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -55,6 +55,9 @@ val experimentalDefinitionInLibrary = Set( "scala.caps", "scala.caps$", + //// New festure: into + "scala.annotation.allowConversions", + //// New APIs: Mirror // Can be stabilized in 3.3.0 or later. "scala.deriving.Mirror$.fromProductTyped", // This API is a bit convoluted. We may need some more feedback before we can stabilize it. diff --git a/tests/run/Parser.scala b/tests/run/Parser.scala new file mode 100644 index 000000000000..48c3af73ecec --- /dev/null +++ b/tests/run/Parser.scala @@ -0,0 +1,97 @@ +import language.experimental.into + +type Input = List[String] + +trait ParseResult[+T] +case class Success[+T](result: T, rest: Input) extends ParseResult[T] +case class Failure(msg: String) extends ParseResult[Nothing] + +class Parser[+T](val parse: Input => ParseResult[T]) + +def empty[T](x: T) = Parser(in => Success(x, in)) +def fail(msg: String) = Parser(in => Failure(msg)) + +class ParserOps[T](p: Parser[T]): + def ~ [U](q: => into Parser[U]): Parser[(T, U)] = Parser(in => + p.parse(in) match + case Success(x, in1) => + q.parse(in1) match + case Success(y, in2) => Success((x, y), in2) + case fail: Failure => fail + case fail: Failure => fail + ) + def | [U](q: => into Parser[T]): Parser[T] = Parser(in => + p.parse(in) match + case s: Success[_] => s + case fail: Failure => q.parse(in) + ) + def map[U](f: T => U): Parser[U] = Parser(in => + p.parse(in) match + case Success(x, in1) => Success(f(x), in1) + case fail: Failure => fail + ) + def ~> [U](q: => into Parser[U]): Parser[U] = + (p ~ q).map(_(1)) + def <~ [U](q: => into Parser[U]): Parser[T] = + (p ~ q).map(_(0)) + def parseAll(in: Input): ParseResult[T] = + p.parse(in) match + case succ @ Success(x, in1) => + if in1.isEmpty then succ + else Failure( + s"""Could not parse all of input + |parse result : $x + |remaining input: ${in1.mkString(" ")}""".stripMargin) + case fail: Failure => + fail + +given strToToken: Conversion[String, Parser[String]] = token(_) + +extension [T](p: Parser[T]) + private def ops = new ParserOps(p) + export ops.* + +extension (str: String) + private def ops = new ParserOps(token(str)) + export ops.* + +def token(p: String => Boolean, expected: String): Parser[String] = Parser { + case first :: rest => + if p(first) then Success(first, rest) + else Failure(s"$expected expected but `$first` found") + case _ => Failure(s"premature end of input where $expected was expected") +} + +def token(str: String): Parser[String] = token(str == _, s"`$str`") + +def opt[T](p: into Parser[T]): Parser[Option[T]] = + p.map(Some(_)) | empty(None) + +def rep[T](p: into Parser[T]): Parser[List[T]] = + (p ~ rep(p)).map(_ :: _) + | empty(Nil) + +object `~~`: + def unapply[A, B](x: (A, B)): Some[(A, B)] = Some(x) + +def reduce(x: Double, ops: List[(String, Double)]): Double = (ops: @unchecked) match + case Nil => x + case ("+", y) :: ys => reduce(x + y, ys) + case ("-", y) :: ys => reduce(x - y, ys) + case ("*", y) :: ys => reduce(x * y, ys) + case ("/", y) :: ys => reduce(x / y, ys) + +def Expr: Parser[Double] = + (Term ~ rep("+" ~ Term | "-" ~ Term)).map(reduce) +def Term: Parser[Double] = + (Factor ~ rep("*" ~ Factor | "/" ~ Factor)).map(reduce) +def Factor: Parser[Double] = + Number | "(" ~> Expr <~ ")" +def Number: Parser[Double] = + token(_.toDoubleOption.isDefined, "number").map(_.toDouble) + +def ops: Parser[String] = "+" | "-" | "*" | "/" + +@main def Test = + println(Expr.parseAll("2 * ( 3 + 4 - 2 / 1 )".split(" ").toList)) + println(Expr.parseAll("2 * ( 3 + 4 - 2 / 1".split(" ").toList)) From b89c10acc0717785f0041695a04d9e01a57592be Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 18 Nov 2022 14:49:26 +0100 Subject: [PATCH 2/4] Update docs/_docs/reference/experimental/into-modifier.md Co-authored-by: Jamie Thompson --- docs/_docs/reference/experimental/into-modifier.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/reference/experimental/into-modifier.md b/docs/_docs/reference/experimental/into-modifier.md index c50b8fa182d1..8635cca24b0f 100644 --- a/docs/_docs/reference/experimental/into-modifier.md +++ b/docs/_docs/reference/experimental/into-modifier.md @@ -56,7 +56,7 @@ Here, the call ```scala concatAll(List('a'), "bc", Array('d', 'e')) ``` -would apply two _different_ implicit conversions: the conversion from `String` to `List[Char]` gets applied to the second argument and the conversion from `Array[Char]` to `Iterable[Char]` gets applied to the third argument. +would apply two _different_ implicit conversions: the conversion from `String` to `Iterable[Char]` gets applied to the second argument and the conversion from `Array[Char]` to `Iterable[Char]` gets applied to the third argument. ## Retrofitting Scala 2 libraries From 9795f12c7264b99f86ff10e242f94bb5f8e8b1a5 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 18 Nov 2022 15:34:35 +0100 Subject: [PATCH 3/4] Update docs/_docs/reference/experimental/into-modifier.md Co-authored-by: Jamie Thompson --- docs/_docs/reference/experimental/into-modifier.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/reference/experimental/into-modifier.md b/docs/_docs/reference/experimental/into-modifier.md index 8635cca24b0f..2ee4c74539b3 100644 --- a/docs/_docs/reference/experimental/into-modifier.md +++ b/docs/_docs/reference/experimental/into-modifier.md @@ -46,7 +46,7 @@ Here, the conversion from `String` to `Iterable[Char]` is applied on the results ## Vararg arguments When applied to a vararg parameter, `into` allows a conversion on each argument value individually. For example, consider a method `concatAll` that concatenates a variable -number of `Seq[Char]` arguments, and also allows implicit conversions into `Seq[Char`: +number of `IterableOnce[Char]` arguments, and also allows implicit conversions into `IterableOnce[Char]`: ```scala def concatAll(xss: into IterableOnce[Char]*): List[Char] = From 015f5cb9781a36d0841651ba829be27414b8cb7e Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 21 Nov 2022 12:18:09 +0100 Subject: [PATCH 4/4] Address review comments --- compiler/src/dotty/tools/dotc/core/Types.scala | 4 ++-- library/src/scala/runtime/stdLibPatches/language.scala | 2 +- .../tasty-inspector/stdlibExperimentalDefinitions.scala | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index f859fc787544..b4cc94b8c807 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3976,9 +3976,9 @@ object Types { tp.derivedAppliedType(tycon, addInto(args.head) :: Nil) case tp @ AppliedType(tycon, args) if defn.isFunctionType(tp) => wrapConvertible(tp.derivedAppliedType(tycon, args.init :+ addInto(args.last))) - case tp @ RefinedType(parent, rname, rinfo) if defn.isFunctionType(tp) => + case tp @ RefinedType(parent, rname, rinfo) if defn.isFunctionOrPolyType(tp) => wrapConvertible(tp.derivedRefinedType(parent, rname, addInto(rinfo))) - case tp: MethodType => + case tp: MethodOrPoly => tp.derivedLambdaType(resType = addInto(tp.resType)) case ExprType(resType) => ExprType(addInto(resType)) diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 24b71a0ff0bd..401926dbab4d 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -78,7 +78,7 @@ object language: /** Experimental support for automatic conversions of arguments, without requiring * a langauge import `import scala.language.implicitConversions`. * - * @see [[https://dotty.epfl.ch/docs/reference/experimental/cc]] + * @see [[https://dotty.epfl.ch/docs/reference/experimental/into-modifier]] */ @compileTimeOnly("`into` can only be used at compile time in import statements") object into diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index 9ce85f6c9b6b..e46d73c70b2b 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -55,7 +55,7 @@ val experimentalDefinitionInLibrary = Set( "scala.caps", "scala.caps$", - //// New festure: into + //// New feature: into "scala.annotation.allowConversions", //// New APIs: Mirror