diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala index f8185098390a..5eb7d571a710 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala @@ -415,6 +415,9 @@ trait BCodeSkelBuilder extends BCodeHelpers { ) cnode.fields.add(jfield) emitAnnotations(jfield, f.annotations) + + if(f.denot.info.isValhallaValueClassType) + cnode.visitLoadableDescriptors(jfield.desc) } } // end of method addClassFields() diff --git a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala index 68c6add4ef13..2125bc4b2e9b 100644 --- a/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala +++ b/compiler/src/dotty/tools/backend/jvm/BTypesFromSymbols.scala @@ -304,7 +304,7 @@ class BTypesFromSymbols[I <: DottyBackendInterface](val int: I, val frontendAcce .addFlagIf(sym.isStaticMember, ACC_STATIC) .addFlagIf(sym.is(Bridge), ACC_BRIDGE | ACC_SYNTHETIC) .addFlagIf(sym.is(Artifact), ACC_SYNTHETIC) - .addFlagIf(sym.isClass && !sym.isInterface, ACC_SUPER) + .addFlagIf(sym.isClass && !sym.isInterface && !sym.isValhallaValueClass, ACC_SUPER) .addFlagIf(sym.isAllOf(JavaEnum), ACC_ENUM) .addFlagIf(sym.is(JavaVarargs), ACC_VARARGS) .addFlagIf(sym.is(Synchronized), ACC_SYNCHRONIZED) @@ -319,5 +319,6 @@ class BTypesFromSymbols[I <: DottyBackendInterface](val int: I, val frontendAcce .addFlagIf(sym.hasAnnotation(TransientAttr), ACC_TRANSIENT) .addFlagIf(sym.hasAnnotation(VolatileAttr), ACC_VOLATILE) .addFlagIf(!sym.is(Mutable), ACC_FINAL) + .addFlagIf(sym.denot.owner.isValhallaValueClass, ACC_STRICT) } } diff --git a/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala b/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala index 4322a306f411..22fce72d948c 100644 --- a/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala +++ b/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala @@ -19,7 +19,7 @@ class BackendUtils(val postProcessor: PostProcessor) { import bTypes.* import coreBTypes.jliLambdaMetaFactoryAltMetafactoryHandle - lazy val classfileVersion: Int = BackendUtils.classfileVersionMap(compilerSettings.target.toInt) + lazy val classfileVersion: Int = if compilerSettings.experimental then (69 + (65535 << 16)) else BackendUtils.classfileVersionMap(compilerSettings.target.toInt) lazy val extraProc: Int = { import GenBCodeOps.addFlagIf diff --git a/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala b/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala index 3e28d2a949cb..f1bad91abce6 100644 --- a/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala +++ b/compiler/src/dotty/tools/backend/jvm/PostProcessorFrontendAccess.scala @@ -52,6 +52,7 @@ object PostProcessorFrontendAccess { sealed trait CompilerSettings { def debug: Boolean def target: String // javaOutputVersion + def experimental: Boolean def dumpClassesDirectory: Option[String] def outputDirectory: AbstractFile @@ -120,6 +121,8 @@ object PostProcessorFrontendAccess { release case (None, None) => "8" // least supported version by default + override val experimental = s.YvalueClasses.value + override val debug: Boolean = ctx.debug override val dumpClassesDirectory: Option[String] = s.Xdumpclasses.valueSetByUser override val outputDirectory: AbstractFile = s.outputDir.value diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index ab77350be26c..2c79dfd9975d 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -703,6 +703,10 @@ object desugar { def isEnumCase = mods.isEnumCase def isNonEnumCase = !isEnumCase && (isCaseClass || isCaseObject) val isValueClass = parents.nonEmpty && isAnyVal(parents.head) + val isValhallaVC = isValueClass && mods.annotations.exists{ annot => annot match + case Apply(Select(New(Ident(annotName)), _), _) => annotName eq tpnme.valhalla + case _ => false + } // This is not watertight, but `extends AnyVal` will be replaced by `inline` later. val caseClassInScala2Library = isCaseClass && Feature.shouldBehaveAsScala2 @@ -1028,7 +1032,7 @@ object desugar { } else if (companionMembers.nonEmpty || companionDerived.nonEmpty || isEnum) companionDefs(anyRef, companionMembers) - else if isValueClass && !isObject then + else if isValueClass && !isObject && !isValhallaVC then companionDefs(anyRef, Nil) else Nil diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 23dde1139c03..71d538544204 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -552,7 +552,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def wrapArrayMethodName(elemtp: Type)(using Context): TermName = { val elemCls = elemtp.classSymbol if (elemCls.isPrimitiveValueClass) nme.wrapXArray(elemCls.name) - else if (elemCls.derivesFrom(defn.ObjectClass) && !elemCls.isNotRuntimeClass) nme.wrapRefArray + else if ((elemCls.derivesFrom(defn.ObjectClass) || elemCls.isValhallaValueClass) && !elemCls.isNotRuntimeClass) nme.wrapRefArray else nme.genericWrapArray } diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 129a77e7e43f..586d7fd3f55e 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -474,6 +474,7 @@ private sealed trait YSettings: val YccLog: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-log", "Used in conjunction with captureChecking language import, print tracing and debug info") val YccVerbose: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-verbose", "Print root capabilities with more details") val YccPrintSetup: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycc-print-setup", "Used in conjunction with captureChecking language import, print trees after cc.Setup phase") + val YvalueClasses: Setting[Boolean] = BooleanSetting(ForkSetting, "Yvalue-classes", "value classes") /** Area-specific debug output */ val YexplainLowlevel: Setting[Boolean] = BooleanSetting(ForkSetting, "Yexplain-lowlevel", "When explaining type errors, show types at a lower level.") diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 8d305eef16e1..8dbafd6b240d 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1113,6 +1113,7 @@ class Definitions { @tu lazy val PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary") @tu lazy val WitnessNamesAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WitnessNames") @tu lazy val StableNullAnnot: ClassSymbol = requiredClass("scala.annotation.stableNull") + @tu lazy val ValhallaAnnot: ClassSymbol = requiredClass("scala.annotation.valhalla") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") diff --git a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala index 0426665eaca1..588550812dbd 100644 --- a/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala +++ b/compiler/src/dotty/tools/dotc/core/ImplicitNullInterop.scala @@ -115,7 +115,7 @@ object ImplicitNullInterop: case tp: TypeRef => // We don't modify value types because they're non-nullable even in Java. val isValueOrSpecialClass = - tp.symbol.isValueClass + (tp.symbol.isValueClass && !tp.symbol.isValhallaValueClass) || tp.isRef(defn.NullClass) || tp.isRef(defn.NothingClass) || tp.isRef(defn.UnitClass) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 3213be389c9d..c229b3bc026f 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -664,6 +664,7 @@ object StdNames { val valueOf : N = "valueOf" val fromOrdinal: N = "fromOrdinal" val values: N = "values" + val valhalla: N = "valhalla" val view_ : N = "view" val varargGetter : N = "varargGetter" val wait_ : N = "wait" diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 3b198ea4dbaa..1bb30a500f69 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -893,6 +893,9 @@ object SymDenotations { /** Is this symbol a class that extends `AnyVal`? Overridden in ClassDenotation */ def isValueClass(using Context): Boolean = false + /** Is this symbol a class that ...? Overridden in ClassDenotation */ + def isValhallaValueClass(using Context): Boolean = false + /** Is this symbol a class of which `null` is a value? */ final def isNullableClass(using Context): Boolean = if ctx.mode.is(Mode.SafeNulls) && !ctx.phase.erasedTypes @@ -904,7 +907,7 @@ object SymDenotations { * but it becomes nullable after erasure. */ final def isNullableClassAfterErasure(using Context): Boolean = - isClass && !isValueClass && !is(ModuleClass) && symbol != defn.NothingClass + isClass && (!isValueClass || isValhallaValueClass) && !is(ModuleClass) && symbol != defn.NothingClass /** Is `pre` the same as C.this, where C is exactly the owner of this symbol, * or, if this symbol is protected, a subclass of the owner? @@ -2106,6 +2109,9 @@ object SymDenotations { // after Erasure and to avoid cyclic references caused by forcing denotations atPhase(di.validFor.firstPhaseId)(di.derivesFrom(anyVal)) + final override def isValhallaValueClass(using Context): Boolean = + hasAnnotation(defn.ValhallaAnnot) && (symbol.isUniversalTrait || isValueClass) + /** Enter a symbol in current scope, and future scopes of same denotation. * Note: We require that this does not happen after the first time * someone does a findMember on a subclass. diff --git a/compiler/src/dotty/tools/dotc/core/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index 435320fec568..bf49c4274fbb 100644 --- a/compiler/src/dotty/tools/dotc/core/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/SymUtils.scala @@ -84,7 +84,8 @@ class SymUtils: !d.isRefinementClass && d.isValueClass && (d.initial.symbol ne defn.AnyValClass) && // Compare the initial symbol because AnyVal does not exist after erasure - !d.isPrimitiveValueClass + !d.isPrimitiveValueClass && + !d.isValhallaValueClass } def isContextBoundCompanion(using Context): Boolean = diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index 852c6ce2cc04..e5694f024a46 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -217,6 +217,10 @@ object Symbols extends SymUtils { final def isStatic(using Context): Boolean = lastDenot.initial.isStatic + /** Overridden by ClassSymbol */ + def isValhallaValueClass(using Context): Boolean = + false + /** This symbol entered into owner's scope (owner must be a class). */ final def entered(using Context): this.type = { if (this.owner.isClass) { @@ -537,6 +541,9 @@ object Symbols extends SymUtils { mySource } + override final def isValhallaValueClass(using Context): Boolean = + classDenot.isValhallaValueClass + final def classDenot(using Context): ClassDenotation = denot.asInstanceOf[ClassDenotation] diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index c29b971f1a5a..2644ddb78d40 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -411,9 +411,9 @@ object TypeErasure { def erasedLub(tp1: Type, tp2: Type)(using Context): Type = { // We need to short-circuit the following 2 case because the regular lub logic in the else relies on // the class hierarchy, which doesn't properly capture `Nothing`/`Null` subtyping behaviour. - if tp1.isRef(defn.NothingClass) || (tp1.isRef(defn.NullClass) && tp2.derivesFrom(defn.ObjectClass)) then + if tp1.isRef(defn.NothingClass) || (tp1.isRef(defn.NullClass) && (tp2.derivesFrom(defn.ObjectClass) || tp2.isValhallaValueClassType)) then tp2 // After erasure, Nothing | T is just T and Null | C is just C, if C is a reference type. - else if tp2.isRef(defn.NothingClass) || (tp2.isRef(defn.NullClass) && tp1.derivesFrom(defn.ObjectClass)) then + else if tp2.isRef(defn.NothingClass) || (tp2.isRef(defn.NullClass) && (tp1.derivesFrom(defn.ObjectClass) || tp1.isValhallaValueClassType)) then tp1 // After erasure, T | Nothing is just T and C | Null is just C, if C is a reference type. else tp1 match { case JavaArrayType(elem1) => diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index dc54d14b0d4b..3ca1b8a2953a 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -315,6 +315,14 @@ object Types extends TypeUtils { loop(this) } + def isValhallaValueClassType(using Context): Boolean = + this match + case tp: TypeRef => + val sym = tp.symbol + if (sym.isClass) sym.isValhallaValueClass else false + case _ => + false + def isFromJavaObject(using Context): Boolean = isRef(defn.ObjectClass) && (typeSymbol eq defn.FromJavaObjectSymbol) @@ -650,7 +658,7 @@ object Types extends TypeUtils { def tp2Null = tp.tp2.hasClassSymbol(defn.NullClass) if ctx.erasedTypes && (tp1Null || tp2Null) then val otherSide = if tp1Null then tp.tp2.classSymbol else tp.tp1.classSymbol - if otherSide.isValueClass then defn.AnyClass else otherSide + if (otherSide.isValueClass && !otherSide.isValhallaValueClass) then defn.AnyClass else otherSide else tp.join.classSymbol case _: JavaArrayType => diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 5f5a0c01db17..fa26ccaf863d 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -236,6 +236,10 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case DefaultShadowsGivenID // errorNumber: 220 case RecurseWithDefaultID // errorNumber: 221 case EncodedPackageNameID // errorNumber: 222 + case ValueClassCannotExtendIdentityClassID // errorNumber: 223 + case ValueClassMustNotExtendTraitWithMutableFieldID // errorNumber: 224 + case IncorrectValueClassDeclarationID // errorNumber: 225 + case ValhallaTraitsMayNotHaveSelfTypesWithVarsID // errorNumber: 226 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 159b3c9a905e..7bf5839f6e38 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3749,3 +3749,27 @@ final class EncodedPackageName(name: Name)(using Context) extends SyntaxMsg(Enco |or `myfile-test.scala` can produce encoded names for the generated package objects. | |In this case, the name `$name` is encoded as `${name.encode}`.""" + +class ValueClassCannotExtendIdentityClass(valueClass: Symbol, parent: Symbol)(using Context) + extends SyntaxMsg(ValueClassCannotExtendIdentityClassID) { + def msg(using Context) = i"""A Valhalla value class cannot extend Identity Class ($parent)}""" + def explain(using Context) = "" +} + +class ValueClassMustNotExtendTraitWithMutableField(parentClass: Symbol, field: Symbol)(using Context) + extends SyntaxMsg(ValueClassMustNotExtendTraitWithMutableFieldID) { + def msg(using Context) = i"""A Valhalla value class/trait cannot extend $parentClass with mutable field ($field)""" + def explain(using Context) = "" +} + +class IncorrectValueClassDeclaration(isClass: Boolean)(using Context) + extends SyntaxMsg(IncorrectValueClassDeclarationID) { + def msg(using Context) = i"""Incorrect Valhalla value class declaration: Valhalla ${if isClass then "value classes" else "traits"} must extend ${if isClass then "AnyVal" else "Any"}.""" + def explain(using Context) = "Valhalla Value Classes and Traits need to extend AnyVal or Any respectively." +} + +class ValhallaTraitsMayNotHaveSelfTypesWithVars(selfTypeSym: Symbol, field: Symbol)(using Context) + extends SyntaxMsg(ValhallaTraitsMayNotHaveSelfTypesWithVarsID) { + def msg(using Context) = i"""A Valhalla trait may not have a self type ($selfTypeSym) with mutable field ($field).""" + def explain(using Context) = "" +} \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 62561ad2d467..595fbb2f629e 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -444,7 +444,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => val builder = if defn.ScalaValueClasses().contains(elemCls) then makeBuilder(s"of${elemCls.name}") - else if elemCls.derivesFrom(defn.ObjectClass) then + else if elemCls.derivesFrom(defn.ObjectClass) || elemCls.isValhallaValueClass then makeBuilder("ofRef").appliedToType(elemType) else makeBuilder("generic").appliedToType(elemType) @@ -607,6 +607,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => val reference = ctx.settings.sourceroot.value val relativePath = util.SourceFile.relativePath(ctx.compilationUnit.source, reference) sym.addAnnotation(Annotation(defn.SourceFileAnnot, Literal(Constants.Constant(relativePath)), tree.span)) + Checking.checkValhallaValueClass(tree, sym, tree.rhs.asInstanceOf[Template].body) else if !sym.is(Param) && !sym.owner.isOneOf(AbstractOrTrait) then Checking.checkGoodBounds(tree.symbol) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index d975e2341111..359a57fd0d16 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -898,6 +898,46 @@ object Checking { } } + // Verify classes and traits with the valhalla annotation meet the requirements + def checkValhallaValueClass(cdef: TypeDef, clazz: Symbol, stats: List[Tree])(using Context): Unit = { + def checkValueClassMember(stat: Tree) = stat match { + case _: ValDef => + if !stat.symbol.is(ParamAccessor) then + report.error(ValueClassesMayNotDefineNonParameterField(clazz, stat.symbol), stat.srcPos) + if stat.symbol.is(Mutable) then + report.error(ValueClassParameterMayNotBeAVar(clazz, stat.symbol), stat.srcPos) + case _: DefDef if stat.symbol.isConstructor => + report.error(ValueClassesMayNotDefineASecondaryConstructor(clazz, stat.symbol), stat.srcPos) + case _ => + // ok + } + + inline def checkParents(): Unit = { + clazz.asClass.baseClasses.foreach(c => { + val parentSym = if(c.isConstructor) then c.owner else c + + if (parentSym.asClass.baseClasses.contains(defn.ObjectClass)) + report.error(ValueClassCannotExtendIdentityClass(clazz, parentSym), cdef.srcPos) + if (((clazz.isClass && (parentSym ne defn.AnyValClass)) || (clazz.is(Trait) && (parentSym ne defn.AnyClass))) && !parentSym.isValhallaValueClass) + parentSym.asClass.classInfo.decls.foreach(f => if f.isMutableVar then report.error(ValueClassMustNotExtendTraitWithMutableField(parentSym, f), cdef.srcPos)) + }) + } + + inline def checkSelfType(): Unit = { + if(clazz.asClass.givenSelfType.exists) + val selfTypeSym = clazz.asClass.givenSelfType.classSymbol + + if(selfTypeSym.exists && !selfTypeSym.isValhallaValueClass) + selfTypeSym.asClass.classInfo.decls.foreach(f => if f.isMutableVar then report.error(ValhallaTraitsMayNotHaveSelfTypesWithVars(selfTypeSym, f), cdef.srcPos)) + } + if(clazz.hasAnnotation(defn.ValhallaAnnot)) + if (clazz.asClass.parentSyms.contains(defn.ObjectClass)) + report.error(IncorrectValueClassDeclaration(clazz.isClass), cdef.srcPos) + checkParents() + checkSelfType() + stats.foreach(checkValueClassMember) + } + /** Check the inline override methods only use inline parameters if they override an inline parameter. */ def checkInlineOverrideParameters(sym: Symbol)(using Context): Unit = lazy val params = sym.paramSymss.flatten diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 82c25fbed242..b08a6bfd5ceb 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1771,7 +1771,7 @@ class Namer { typer: Typer => tempInfo = null // The temporary info can now be garbage-collected Checking.checkWellFormed(cls) - if (isDerivedValueClass(cls)) cls.setFlag(Final) + if (isDerivedValueClass(cls) || (cls.isValhallaValueClass && !cls.is(Abstract) && !cls.is(Trait))) cls.setFlag(Final) cls.info = avoidPrivateLeaks(cls) cls.baseClasses.foreach(_.invalidateBaseTypeCache()) // we might have looked before and found nothing cls.invalidateMemberCaches() // we might have checked for a member when parents were not known yet. diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index 7bfda4e4b5fa..f5753c09f81a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -204,9 +204,9 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): else if cls2.isPrimitiveValueClass then cmpWithBoxed(cls2, cls1) else if cls1 == defn.NullClass then - cls1 == cls2 || cls2.derivesFrom(defn.ObjectClass) + cls1 == cls2 || cls2.derivesFrom(defn.ObjectClass) || cls2.isValhallaValueClass else if cls2 == defn.NullClass then - cls1.derivesFrom(defn.ObjectClass) + cls1.derivesFrom(defn.ObjectClass) || cls1.isValhallaValueClass else cls1 == defn.NothingClass || cls2 == defn.NothingClass end canComparePredefinedClasses diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 210104c4ab93..f8024074c333 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -3446,6 +3446,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if cls.is(ModuleClass) && effectiveOwner.is(Trait) && !effectiveOwner.derivesFrom(defn.ObjectClass) + && !effectiveOwner.isValhallaValueClass then report.error(em"$cls cannot be defined in universal $effectiveOwner", cdef.srcPos) diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index d2f6b8265c6f..5c286ab60104 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -377,6 +377,16 @@ class CompilationTests { ).checkRuns() } + + // Valhalla Value Classes tests + @Test def checkValhallaValueClasses: Unit = { + implicit val testGroup: TestGroup = TestGroup("checkValhallaVC") + val options = defaultOptions.and("-Yvalue-classes") + val valhallaAnnotationPath = "library/target/scala-library-nonbootstrapped/scala-library-3.8.1-RC1-bin-SNAPSHOT-nonbootstrapped.jar" + + compileFilesInDir("tests/valhalla/pos", options.withClasspath(valhallaAnnotationPath)).checkCompile() + compileFilesInDir("tests/valhalla/neg", options.withClasspath(valhallaAnnotationPath)).checkExpectedErrors() + } } object CompilationTests extends ParallelTesting { diff --git a/docs/_docs/reference/experimental/valhalla.md b/docs/_docs/reference/experimental/valhalla.md new file mode 100644 index 000000000000..6e5aa8a7f7cd --- /dev/null +++ b/docs/_docs/reference/experimental/valhalla.md @@ -0,0 +1,46 @@ +--- +layout: doc-page +title: "Valhalla Value Classes and Traits" +nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/valhalla.html +--- + +## Valhalla Value Classes + +Valhalla value classes are the Scala equivalence of Java's Project Valhalla's value classes (see [JEP 401](https://openjdk.org/jeps/401)). When used with the Project Valhalla JVM, Valhalla value classes are optimized. + +Valhalla value classes extend AnyVal and have a `valhalla` annotation. Valhalla value classes cannot have non-parameter fields and cannot have auxilliary constructors. + +Valhalla value classes do not have object identity -- two valhalla value classes are equal when their fields are the same. + +```scala +import scala.annotation.valhalla + +@valhalla class ValhallaValueClass(val x: Int, val y: Int) extends AnyVal +``` + +Valhalla value classes are implicitly final and cannot be extended unless it is abstract. Its fields are immutable and cannot be lazy. + +Valhalla value classes can extend `AnyVal`, universal traits, or abstract valhalla value classes. + +## Valhalla Traits + +Valhalla Traits are Universal Traits (traits that extend Any) with a `valhalla` annotation. + +Like Valhalla value classes, any Valhalla trait must have immutable fields only. + +Valhalla traits can extend `Any` or universal traits. + +```scala +import scala.annotation.valhalla + +@valhalla trait(val x: Int, val y: Int) ValhallaTrait extends Any + +``` + +## Using Explicit Self with Valhalla + +Valhalla traits can have self-type of any trait without mutable fields. + +## CanEqual with Valhalla + +Valhalla value classes can be null, so the CanEqual of `null` and a valhalla value class returns `true`. \ No newline at end of file diff --git a/library/src/scala/annotation/valhalla.scala b/library/src/scala/annotation/valhalla.scala new file mode 100644 index 000000000000..4067e0aa882b --- /dev/null +++ b/library/src/scala/annotation/valhalla.scala @@ -0,0 +1,13 @@ +package scala.annotation + +import scala.language.`2.13` + +/** A class annotation which verifies that the class will be a + * Project Valhalla value class. + * + * If it is present, the compiler will issue an error if the class + * does not satisfy the requirements of the Project Valhalla value + * class as specified in https://openjdk.org/jeps/401. + */ + +final class valhalla extends StaticAnnotation diff --git a/project/Build.scala b/project/Build.scala index 13c22fed541a..a70d66dfd00f 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -199,7 +199,8 @@ object Build { "-language:implicitConversions", s"--java-output-version:${Versions.minimumJVMVersion}", "-Yexplicit-nulls", - "-Wsafe-init" + "-Wsafe-init", + "-Yvalue-classes" ), (Compile / compile / javacOptions) ++= Seq( diff --git a/tests/valhalla/neg/ExplicitSelf.scala b/tests/valhalla/neg/ExplicitSelf.scala new file mode 100644 index 000000000000..3945143d0dac --- /dev/null +++ b/tests/valhalla/neg/ExplicitSelf.scala @@ -0,0 +1,9 @@ +import scala.annotation.valhalla + +trait RecordLabel: + var name: String = "Sony" + +@valhalla trait SoundProduction extends Any { // error + this: RecordLabel => + val prodName = "prod " + name +} \ No newline at end of file diff --git a/tests/valhalla/neg/ExtendingConcreteVVC.scala b/tests/valhalla/neg/ExtendingConcreteVVC.scala new file mode 100644 index 000000000000..5a311d2f1296 --- /dev/null +++ b/tests/valhalla/neg/ExtendingConcreteVVC.scala @@ -0,0 +1,11 @@ +import scala.annotation.valhalla + +@valhalla +class ConcreteVVC extends AnyVal { + def addOne(x: Int): Int = x + 1 +} + +@valhalla +class VVC extends ConcreteVVC // error + +class Ident extends ConcreteVVC // error \ No newline at end of file diff --git a/tests/valhalla/neg/ExtendsAnyRef.scala b/tests/valhalla/neg/ExtendsAnyRef.scala new file mode 100644 index 000000000000..815e137c7c42 --- /dev/null +++ b/tests/valhalla/neg/ExtendsAnyRef.scala @@ -0,0 +1,29 @@ +import scala.annotation.valhalla + +/** + * Valhalla Class Extends AnyRef Class + * */ +abstract class Abs { + def addOne(x : Int) : Int +} + +@valhalla +class ValhallaAnnotWithoutExtendingAnyVal extends Abs{ // error + def addOne(x : Int) : Int = x + 1 +} + +/** + * Valhalla Trait Extends AnyRef 2 + * */ +@valhalla +trait Uni extends Any + +@valhalla +trait AnyRefTrait extends Uni // error + +trait Trait: + def add(x:Int, y:Int): Int + +@valhalla +class VVC extends AnyVal with Trait: // error + def add(x:Int, y:Int): Int = x + y \ No newline at end of file diff --git a/tests/valhalla/neg/IncorrectDeclaration.scala b/tests/valhalla/neg/IncorrectDeclaration.scala new file mode 100644 index 000000000000..f746f5afdc84 --- /dev/null +++ b/tests/valhalla/neg/IncorrectDeclaration.scala @@ -0,0 +1,7 @@ +import scala.annotation.valhalla + +@valhalla +class A // error + +@valhalla +trait T // error \ No newline at end of file diff --git a/tests/valhalla/neg/SecondaryCtor.scala b/tests/valhalla/neg/SecondaryCtor.scala new file mode 100644 index 000000000000..0075c64bfe26 --- /dev/null +++ b/tests/valhalla/neg/SecondaryCtor.scala @@ -0,0 +1,5 @@ +class SecondaryCtor(val x: Int) extends AnyVal { + def this(a: Int, b: Int) = { // error + this(a + b) + } +} diff --git a/tests/valhalla/neg/VVCMutableParam.scala b/tests/valhalla/neg/VVCMutableParam.scala new file mode 100644 index 000000000000..bce018a325f5 --- /dev/null +++ b/tests/valhalla/neg/VVCMutableParam.scala @@ -0,0 +1,7 @@ +import scala.annotation.valhalla + +@valhalla +class VVCMutableField(var a: Int) extends AnyVal // error + +@valhalla +trait VTMutableField(var a: Int) extends Any // error \ No newline at end of file diff --git a/tests/valhalla/neg/ValhallaExtendsNonValhallaAnyTraitWithVar.scala b/tests/valhalla/neg/ValhallaExtendsNonValhallaAnyTraitWithVar.scala new file mode 100644 index 000000000000..be080ef495d9 --- /dev/null +++ b/tests/valhalla/neg/ValhallaExtendsNonValhallaAnyTraitWithVar.scala @@ -0,0 +1,19 @@ +import scala.annotation.valhalla + +/** + * Valhalla classes and traits cannot extend non-Valhalla classes or traits + */ + +trait AnyTrait extends Any: + var a : Int = 2 + def add(x:Int, y:Int): Int + + +@valhalla +trait TraitExtendsAnyTrait extends Any with AnyTrait: // error + def addOne(x: Int): Int + +@valhalla +class VVC extends AnyVal with AnyTrait: // error + def add(x:Int, y:Int): Int = x + y + def addOne(x: Int): Int = x + 1 \ No newline at end of file diff --git a/tests/valhalla/neg/ValhallaMutableField.scala b/tests/valhalla/neg/ValhallaMutableField.scala new file mode 100644 index 000000000000..a907e770968c --- /dev/null +++ b/tests/valhalla/neg/ValhallaMutableField.scala @@ -0,0 +1,11 @@ +import scala.annotation.valhalla + +@valhalla +trait VTMutableField extends Any { + var a = 5 // error +} + +@valhalla +class VVCMutableField extends AnyVal { + var a = 5 // error +} \ No newline at end of file diff --git a/tests/valhalla/neg/ValhallaWithField.scala b/tests/valhalla/neg/ValhallaWithField.scala new file mode 100644 index 000000000000..b982c4f4e191 --- /dev/null +++ b/tests/valhalla/neg/ValhallaWithField.scala @@ -0,0 +1,11 @@ +import scala.annotation.valhalla + +@valhalla +trait VTMutableField extends Any { + val a = 5 // error +} + +@valhalla +class VVCMutableField extends AnyVal { + val a = 5 // error +} \ No newline at end of file diff --git a/tests/valhalla/pos/AbstractVVC.scala b/tests/valhalla/pos/AbstractVVC.scala new file mode 100644 index 000000000000..7dd0cc6d4fb8 --- /dev/null +++ b/tests/valhalla/pos/AbstractVVC.scala @@ -0,0 +1,6 @@ +import scala.annotation.valhalla + +@valhalla +abstract class AbstractVVC(val a: Int, val b: Int) extends AnyVal { + def addOne(x: Int):Int = x +} \ No newline at end of file diff --git a/tests/valhalla/pos/CanEqual.scala b/tests/valhalla/pos/CanEqual.scala new file mode 100644 index 000000000000..ae782d2f64af --- /dev/null +++ b/tests/valhalla/pos/CanEqual.scala @@ -0,0 +1,16 @@ +import scala.language.strictEquality +import scala.annotation.valhalla + +// Motivation: +// val x = ... // of type T +// val y = ... // of type S, but should be T +// x == y // typechecks, will always yield false + +@valhalla +class T(val a: Int) extends AnyVal + +class Main: + def main = { + val t = new T(1) + t == null + } \ No newline at end of file diff --git a/tests/valhalla/pos/ConcreteVVC.scala b/tests/valhalla/pos/ConcreteVVC.scala new file mode 100644 index 000000000000..db52ea97394c --- /dev/null +++ b/tests/valhalla/pos/ConcreteVVC.scala @@ -0,0 +1,6 @@ +import scala.annotation.valhalla + +@valhalla +class ConcreteVVC(val a: Int, val b: Int) extends AnyVal { + def addOne(x: Int): Int = x + 1 +} \ No newline at end of file diff --git a/tests/valhalla/pos/ExplicitSelf.scala b/tests/valhalla/pos/ExplicitSelf.scala new file mode 100644 index 000000000000..b38972733945 --- /dev/null +++ b/tests/valhalla/pos/ExplicitSelf.scala @@ -0,0 +1,8 @@ +import scala.annotation.valhalla + +trait Animal(val cuteness: Int): + val happiness = 100 + def speak: Unit + +@valhalla trait Mammal(val furColour: Tuple) extends Any: + this: Animal => diff --git a/tests/valhalla/pos/ExplicitSelf2.scala b/tests/valhalla/pos/ExplicitSelf2.scala new file mode 100644 index 000000000000..0a05bc5e1317 --- /dev/null +++ b/tests/valhalla/pos/ExplicitSelf2.scala @@ -0,0 +1,7 @@ +import scala.annotation.valhalla + +@valhalla trait Animal(val cuteness: Int) extends Any: + def speak: Unit + +@valhalla trait Mammal(val furColour: Tuple) extends Any: + this: Animal => diff --git a/tests/valhalla/pos/IdentClassExtendsValhallaTrait.scala b/tests/valhalla/pos/IdentClassExtendsValhallaTrait.scala new file mode 100644 index 000000000000..0a50b57db23d --- /dev/null +++ b/tests/valhalla/pos/IdentClassExtendsValhallaTrait.scala @@ -0,0 +1,8 @@ +import scala.annotation.valhalla + +@valhalla +trait ValhallaTrait extends Any: + def add(x:Int, y:Int): Int + +class IdentClass extends ValhallaTrait: + def add(x:Int, y:Int): Int = x + y \ No newline at end of file diff --git a/tests/valhalla/pos/NestedClasses.scala b/tests/valhalla/pos/NestedClasses.scala new file mode 100644 index 000000000000..b6e98e884e73 --- /dev/null +++ b/tests/valhalla/pos/NestedClasses.scala @@ -0,0 +1,16 @@ +import scala.annotation.valhalla + +@valhalla +class NestedClasses extends AnyVal{ + class inner + + @valhalla + class innerValhalla extends AnyVal +} + +class NestedIdentityClasses { + class inner + + @valhalla + class innerValhalla extends AnyVal +} \ No newline at end of file diff --git a/tests/valhalla/pos/TraitExtendsAny.scala b/tests/valhalla/pos/TraitExtendsAny.scala new file mode 100644 index 000000000000..5d69d533e99b --- /dev/null +++ b/tests/valhalla/pos/TraitExtendsAny.scala @@ -0,0 +1,4 @@ +import scala.annotation.valhalla + +@valhalla +trait TraitExtendsAny extends Any diff --git a/tests/valhalla/pos/VVCExtendsAbstractVVC.scala b/tests/valhalla/pos/VVCExtendsAbstractVVC.scala new file mode 100644 index 000000000000..34c88c057ede --- /dev/null +++ b/tests/valhalla/pos/VVCExtendsAbstractVVC.scala @@ -0,0 +1,11 @@ +import scala.annotation.valhalla + +@valhalla +abstract class AbstractVVC extends AnyVal { + def addOne(x: Int): Int +} + +@valhalla +class VVC extends AbstractVVC { + def addOne(x: Int): Int = x + 1 +} \ No newline at end of file diff --git a/tests/valhalla/pos/VVCExtendsValhallaTrait.scala b/tests/valhalla/pos/VVCExtendsValhallaTrait.scala new file mode 100644 index 000000000000..16adf8f3001b --- /dev/null +++ b/tests/valhalla/pos/VVCExtendsValhallaTrait.scala @@ -0,0 +1,9 @@ +import scala.annotation.valhalla + +@valhalla +trait ValhallaTrait extends Any: + def add(x:Int, y:Int): Int + +@valhalla +class VVC extends AnyVal with ValhallaTrait: + def add(x:Int, y:Int): Int = x + y \ No newline at end of file diff --git a/tests/valhalla/pos/ValhallaExtendsNonValhallaAnyTrait.scala b/tests/valhalla/pos/ValhallaExtendsNonValhallaAnyTrait.scala new file mode 100644 index 000000000000..52956c7a9ce2 --- /dev/null +++ b/tests/valhalla/pos/ValhallaExtendsNonValhallaAnyTrait.scala @@ -0,0 +1,19 @@ +import scala.annotation.valhalla + +/** + * Valhalla classes and traits cannot extend non-Valhalla classes or traits + */ + +trait AnyTrait extends Any: + val a: Int = 2 + def add(x:Int, y:Int): Int + + +@valhalla +trait TraitExtendsAnyTrait extends Any with AnyTrait: // error + def addOne(x: Int): Int + +@valhalla +class VVC extends AnyVal with AnyTrait: // error + def add(x:Int, y:Int): Int = x + y + def addOne(x: Int): Int = x + 1 \ No newline at end of file