diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index ff8f08f0249e..6b8ff40f9d2e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -233,6 +233,11 @@ class Definitions { @tu lazy val CompiletimeTesting_ErrorKind: Symbol = ctx.requiredModule("scala.compiletime.testing.ErrorKind") @tu lazy val CompiletimeTesting_ErrorKind_Parser: Symbol = CompiletimeTesting_ErrorKind.requiredMethod("Parser") @tu lazy val CompiletimeTesting_ErrorKind_Typer: Symbol = CompiletimeTesting_ErrorKind.requiredMethod("Typer") + @tu lazy val CompiletimeOpsPackageObject: Symbol = ctx.requiredModule("scala.compiletime.ops.package") + @tu lazy val CompiletimeOpsPackageObjectAny: Symbol = ctx.requiredModule("scala.compiletime.ops.package.any") + @tu lazy val CompiletimeOpsPackageObjectInt: Symbol = ctx.requiredModule("scala.compiletime.ops.package.int") + @tu lazy val CompiletimeOpsPackageObjectString: Symbol = ctx.requiredModule("scala.compiletime.ops.package.string") + @tu lazy val CompiletimeOpsPackageObjectBoolean: Symbol = ctx.requiredModule("scala.compiletime.ops.package.boolean") /** The `scalaShadowing` package is used to safely modify classes and * objects in scala so that they can be used from dotty. They will @@ -898,6 +903,26 @@ class Definitions { final def isCompiletime_S(sym: Symbol)(implicit ctx: Context): Boolean = sym.name == tpnme.S && sym.owner == CompiletimePackageObject.moduleClass + private val compiletimePackageAnyTypes: Set[Name] = Set(tpnme.Equals, tpnme.NotEquals) + private val compiletimePackageIntTypes: Set[Name] = Set( + tpnme.Plus, tpnme.Minus, tpnme.Times, tpnme.Div, tpnme.Mod, + tpnme.Lt, tpnme.Gt, tpnme.Ge, tpnme.Le, + tpnme.Abs, tpnme.Negate, tpnme.Min, tpnme.Max, tpnme.ToString, + ) + private val compiletimePackageBooleanTypes: Set[Name] = Set(tpnme.Not, tpnme.Xor, tpnme.And, tpnme.Or) + private val compiletimePackageStringTypes: Set[Name] = Set(tpnme.Plus) + + final def isCompiletimeAppliedType(sym: Symbol)(implicit ctx: Context): Boolean = { + def isOpsPackageObjectAppliedType: Boolean = + sym.owner == CompiletimeOpsPackageObjectAny.moduleClass && compiletimePackageAnyTypes.contains(sym.name) || + sym.owner == CompiletimeOpsPackageObjectInt.moduleClass && compiletimePackageIntTypes.contains(sym.name) || + sym.owner == CompiletimeOpsPackageObjectBoolean.moduleClass && compiletimePackageBooleanTypes.contains(sym.name) || + sym.owner == CompiletimeOpsPackageObjectString.moduleClass && compiletimePackageStringTypes.contains(sym.name) + + sym.isType && (isCompiletime_S(sym) || isOpsPackageObjectAppliedType) + } + + // ----- Symbol sets --------------------------------------------------- @tu lazy val AbstractFunctionType: Array[TypeRef] = mkArityArray("scala.runtime.AbstractFunction", MaxImplementedFunctionArity, 0) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index adb5a13c43ec..ed69b2a9ddbc 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -201,13 +201,34 @@ object StdNames { final val Product: N = "Product" final val PartialFunction: N = "PartialFunction" final val PrefixType: N = "PrefixType" - final val S: N = "S" final val Serializable: N = "Serializable" final val Singleton: N = "Singleton" final val Throwable: N = "Throwable" final val IOOBException: N = "IndexOutOfBoundsException" final val FunctionXXL: N = "FunctionXXL" + final val Abs: N = "Abs" + final val And: N = "&&" + final val Div: N = "/" + final val Equals: N = "==" + final val Ge: N = ">=" + final val Gt: N = ">" + final val Le: N = "<=" + final val Lt: N = "<" + final val Max: N = "Max" + final val Min: N = "Min" + final val Minus: N = "-" + final val Mod: N = "%" + final val Negate: N = "Negate" + final val Not: N = "!" + final val NotEquals: N = "!=" + final val Or: N = "||" + final val Plus: N = "+" + final val S: N = "S" + final val Times: N = "*" + final val ToString: N = "ToString" + final val Xor: N = "^" + final val ClassfileAnnotation: N = "ClassfileAnnotation" final val ClassManifest: N = "ClassManifest" final val Enum: N = "Enum" diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index 954cd9d45c66..000125c25d62 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -371,12 +371,16 @@ class TypeApplications(val self: Type) extends AnyVal { // just eta-reduction (ignoring variance annotations). // See i2201*.scala for examples where more aggressive // reduction would break type inference. - dealiased.paramRefs == dealiasedArgs + dealiased.paramRefs == dealiasedArgs || + defn.isCompiletimeAppliedType(tyconBody.typeSymbol) case _ => false } } if ((dealiased eq stripped) || followAlias) - try dealiased.instantiate(args) + try { + val instantiated = dealiased.instantiate(args) + if (followAlias) instantiated.normalized else instantiated + } catch { case ex: IndexOutOfBoundsException => AppliedType(self, args) } else AppliedType(self, args) } diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index f680e13f75e0..ebb896d019ca 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -965,7 +965,7 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w compareLower(bounds(param2), tyconIsTypeRef = false) case tycon2: TypeRef => isMatchingApply(tp1) || - defn.isCompiletime_S(tycon2.symbol) && compareS(tp2, tp1, fromBelow = true) || { + defn.isCompiletimeAppliedType(tycon2.symbol) && compareCompiletimeAppliedType(tp2, tp1, fromBelow = true) || { tycon2.info match { case info2: TypeBounds => compareLower(info2, tyconIsTypeRef = true) @@ -1005,7 +1005,7 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w case tycon1: TypeRef => val sym = tycon1.symbol !sym.isClass && { - defn.isCompiletime_S(sym) && compareS(tp1, tp2, fromBelow = false) || + defn.isCompiletimeAppliedType(sym) && compareCompiletimeAppliedType(tp1, tp2, fromBelow = false) || recur(tp1.superType, tp2) || tryLiftedToThis1 } @@ -1015,7 +1015,7 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w false } - /** Compare `tp` of form `S[arg]` with `other`, via ">:>` if fromBelow is true, "<:<" otherwise. + /** Compare `tp` of form `S[arg]` with `other`, via ">:>" if fromBelow is true, "<:<" otherwise. * If `arg` is a Nat constant `n`, proceed with comparing `n + 1` and `other`. * Otherwise, if `other` is a Nat constant `n`, proceed with comparing `arg` and `n - 1`. */ @@ -1037,6 +1037,18 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] w case _ => false } + /** Compare `tp` of form `tycon[...args]`, where `tycon` is a scala.compiletime type, + * with `other` via ">:>" if fromBelow is true, "<:<" otherwise. + * Delegates to compareS if `tycon` is scala.compiletime.S. Otherwise, constant folds if possible. + */ + def compareCompiletimeAppliedType(tp: AppliedType, other: Type, fromBelow: Boolean): Boolean = { + if (defn.isCompiletime_S(tp.tycon.typeSymbol)) compareS(tp, other, fromBelow) + else { + val folded = tp.tryCompiletimeConstantFold + if (fromBelow) recur(other, folded) else recur(folded, other) + } + } + /** Like tp1 <:< tp2, but returns false immediately if we know that * the case was covered previously during subtyping. */ diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 7f1554c3dbec..8af9e2d57ba8 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3595,19 +3595,97 @@ object Types { case _ => NoType } - if (defn.isCompiletime_S(tycon.symbol) && args.length == 1) - trace(i"normalize S $this", typr, show = true) { - args.head.normalized match { - case ConstantType(Constant(n: Int)) if n >= 0 && n < Int.MaxValue => - ConstantType(Constant(n + 1)) - case none => tryMatchAlias - } - } - else tryMatchAlias + + tryCompiletimeConstantFold.orElse(tryMatchAlias) + case _ => NoType } + def tryCompiletimeConstantFold(implicit ctx: Context): Type = tycon match { + case tycon: TypeRef if defn.isCompiletimeAppliedType(tycon.symbol) => + def constValue(tp: Type): Option[Any] = tp match { + case ConstantType(Constant(n)) => Some(n) + case _ => None + } + + def boolValue(tp: Type): Option[Boolean] = tp match { + case ConstantType(Constant(n: Boolean)) => Some(n) + case _ => None + } + + def intValue(tp: Type): Option[Int] = tp match { + case ConstantType(Constant(n: Int)) => Some(n) + case _ => None + } + + def stringValue(tp: Type): Option[String] = tp match { + case ConstantType(Constant(n: String)) => Some(n) + case _ => None + } + + def natValue(tp: Type): Option[Int] = intValue(tp).filter(n => n >= 0 && n < Int.MaxValue) + + def constantFold1[T](extractor: Type => Option[T], op: T => Any): Option[Type] = + extractor(args.head.normalized).map(a => ConstantType(Constant(op(a)))) + + def constantFold2[T](extractor: Type => Option[T], op: (T, T) => Any): Option[Type] = + for { + a <- extractor(args.head.normalized) + b <- extractor(args.tail.head.normalized) + } yield ConstantType(Constant(op(a, b))) + + trace(i"compiletime constant fold $this", typr, show = true) { + val name = tycon.symbol.name + val owner = tycon.symbol.owner + val nArgs = args.length + val constantType = + if (owner == defn.CompiletimePackageObject.moduleClass) name match { + case tpnme.S if nArgs == 1 => constantFold1(natValue, _ + 1) + case _ => None + } else if (owner == defn.CompiletimeOpsPackageObjectAny.moduleClass) name match { + case tpnme.Equals if nArgs == 2 => constantFold2(constValue, _ == _) + case tpnme.NotEquals if nArgs == 2 => constantFold2(constValue, _ != _) + case _ => None + } else if (owner == defn.CompiletimeOpsPackageObjectInt.moduleClass) name match { + case tpnme.Abs if nArgs == 1 => constantFold1(intValue, _.abs) + case tpnme.Negate if nArgs == 1 => constantFold1(intValue, x => -x) + case tpnme.ToString if nArgs == 1 => constantFold1(intValue, _.toString) + case tpnme.Plus if nArgs == 2 => constantFold2(intValue, _ + _) + case tpnme.Minus if nArgs == 2 => constantFold2(intValue, _ - _) + case tpnme.Times if nArgs == 2 => constantFold2(intValue, _ * _) + case tpnme.Div if nArgs == 2 => constantFold2(intValue, { + case (_, 0) => throw new TypeError("Division by 0") + case (a, b) => a / b + }) + case tpnme.Mod if nArgs == 2 => constantFold2(intValue, { + case (_, 0) => throw new TypeError("Modulo by 0") + case (a, b) => a % b + }) + case tpnme.Lt if nArgs == 2 => constantFold2(intValue, _ < _) + case tpnme.Gt if nArgs == 2 => constantFold2(intValue, _ > _) + case tpnme.Ge if nArgs == 2 => constantFold2(intValue, _ >= _) + case tpnme.Le if nArgs == 2 => constantFold2(intValue, _ <= _) + case tpnme.Min if nArgs == 2 => constantFold2(intValue, _ min _) + case tpnme.Max if nArgs == 2 => constantFold2(intValue, _ max _) + case _ => None + } else if (owner == defn.CompiletimeOpsPackageObjectString.moduleClass) name match { + case tpnme.Plus if nArgs == 2 => constantFold2(stringValue, _ + _) + case _ => None + } else if (owner == defn.CompiletimeOpsPackageObjectBoolean.moduleClass) name match { + case tpnme.Not if nArgs == 1 => constantFold1(boolValue, x => !x) + case tpnme.And if nArgs == 2 => constantFold2(boolValue, _ && _) + case tpnme.Or if nArgs == 2 => constantFold2(boolValue, _ || _) + case tpnme.Xor if nArgs == 2 => constantFold2(boolValue, _ ^ _) + case _ => None + } else None + + constantType.getOrElse(NoType) + } + + case _ => NoType + } + def lowerBound(implicit ctx: Context): Type = tycon.stripTypeVar match { case tycon: TypeRef => tycon.info match { @@ -3974,7 +4052,7 @@ object Types { myReduced = trace(i"reduce match type $this $hashCode", typr, show = true) { try - typeComparer.matchCases(scrutinee, cases)(trackingCtx) + typeComparer.matchCases(scrutinee.normalized, cases)(trackingCtx) catch { case ex: Throwable => handleRecursive("reduce type ", i"$scrutinee match ...", ex) diff --git a/docs/docs/reference/metaprogramming/inline.md b/docs/docs/reference/metaprogramming/inline.md index 4c8dcdae6028..32f49cae6983 100644 --- a/docs/docs/reference/metaprogramming/inline.md +++ b/docs/docs/reference/metaprogramming/inline.md @@ -295,7 +295,7 @@ val intTwo: 2 = natTwo The `scala.compiletime` package contains helper definitions that provide support for compile time operations over values. They are described in the following. -#### `constValue`, `constValueOpt`, and the `S` combinator +### `constValue`, `constValueOpt`, and the `S` combinator `constvalue` is a function that produces the constant value represented by a type. @@ -317,7 +317,7 @@ enabling us to handle situations where a value is not present. Note that `S` is the type of the successor of some singleton type. For example the type `S[1]` is the singleton type `2`. -#### `erasedValue` +### `erasedValue` So far we have seen inline methods that take terms (tuples and integers) as parameters. What if we want to base case distinctions on types instead? For @@ -381,7 +381,7 @@ final val two = toIntT[Succ[Succ[Zero.type]]] behavior. Since `toInt` performs static checks over the static type of `N` we can safely use it to scrutinize its return type (`S[S[Z]]` in this case). -#### `error` +### `error` The `error` method is used to produce user-defined compile errors during inline expansion. It has the following signature: @@ -411,6 +411,54 @@ inline def fail(p1: => Any) = { fail(identity("foo")) // error: failed on: identity("foo") ``` +### The `scala.compiletime.ops` package + +The `scala.compiletime.ops` package contains types that provide support for +primitive operations on singleton types. For example, +`scala.compiletime.ops.int.*` provides support for multiplying two singleton +`Int` types, and `scala.compiletime.ops.boolean.&&` for the conjunction of two +`Boolean` types. When all arguments to a type in `scala.compiletime.ops` are +singleton types, the compiler can evaluate the result of the operation. + +```scala +import scala.compiletime.ops.int._ +import scala.compiletime.ops.boolean._ + +val conjunction: true && true = true +val multiplication: 3 * 5 = 15 +``` + +Many of these singleton operation types are meant to be used infix (as in [SLS ยง +3.2.8](https://www.scala-lang.org/files/archive/spec/2.12/03-types.html#infix-types)), +and are annotated with [`@infix`](scala.annotation.infix) accordingly. + +Since type aliases have the same precedence rules as their term-level +equivalents, the operations compose with the expected precedence rules: + +```scala +import scala.compiletime.ops.int._ +val x: 1 + 2 * 3 = 7 +``` + +The operation types are located in packages named after the type of the +left-hand side parameter: for instance, `scala.compiletime.int.+` represents +addition of two numbers, while `scala.compiletime.string.+` represents string +concatenation. To use both and distinguish the two types from each other, a +match type can dispatch to the correct implementation: + +```scala +import scala.compiletime.ops._ +import scala.annotation.infix + +@infix type +[X <: Int | String, Y <: Int | String] = (X, Y) match { + case (Int, Int) => int.+[X, Y] + case (String, String) => string.+[X, Y] +} + +val concat: "a" + "b" = "ab" +val addition: 1 + 1 = 2 +``` + ## Summoning Implicits Selectively It is foreseen that many areas of typelevel programming can be done with rewrite diff --git a/library/src/scala/compiletime/ops/package.scala b/library/src/scala/compiletime/ops/package.scala new file mode 100644 index 000000000000..d7714f63e487 --- /dev/null +++ b/library/src/scala/compiletime/ops/package.scala @@ -0,0 +1,174 @@ +package scala.compiletime + +import scala.annotation.infix + +package object ops { + object any { + /** Equality comparison of two singleton types. + * ```scala + * val eq1: 1 == 1 = true + * val eq2: 1 == "1" = false + * val eq3: "1" == "1" = true + * ``` + */ + @infix type ==[X <: AnyVal, Y <: AnyVal] <: Boolean + + /** Inequality comparison of two singleton types. + * ```scala + * val eq1: 1 != 1 = false + * val eq2: 1 != "1" = true + * val eq3: "1" != "1" = false + * ``` + */ + @infix type !=[X <: AnyVal, Y <: AnyVal] <: Boolean + } + + object string { + /** Concatenation of two `String` singleton types. + * ```scala + * val hello: "hello " + "world" = "hello world" + * ``` + */ + @infix type +[X <: String, Y <: String] <: String + } + + object int { + /** Addition of two `Int` singleton types. + * ```scala + * val sum: 2 + 2 = 4 + * ``` + */ + @infix type +[X <: Int, Y <: Int] <: Int + + /** Subtraction of two `Int` singleton types. + * ```scala + * val sub: 4 - 2 = 2 + * ``` + */ + @infix type -[X <: Int, Y <: Int] <: Int + + /** Multiplication of two `Int` singleton types. + * ```scala + * val mul: 4 * 2 = 8 + * ``` + */ + @infix type *[X <: Int, Y <: Int] <: Int + + /** Integer division of two `Int` singleton types. + * ```scala + * val div: 5 / 2 = 2 + * ``` + */ + @infix type /[X <: Int, Y <: Int] <: Int + + /** Remainder of the division of `X` by `Y`. + * ```scala + * val mod: 5 % 2 = 1 + * ``` + */ + @infix type %[X <: Int, Y <: Int] <: Int + + /** Less-than comparison of two `Int` singleton types. + * ```scala + * val lt1: 4 < 2 = false + * val lt2: 2 < 4 = true + * ``` + */ + @infix type <[X <: Int, Y <: Int] <: Boolean + + /** Greater-than comparison of two `Int` singleton types. + * ```scala + * val gt1: 4 > 2 = true + * val gt2: 2 > 2 = false + * ``` + */ + @infix type >[X <: Int, Y <: Int] <: Boolean + + /** Greater-or-equal comparison of two `Int` singleton types. + * ```scala + * val ge1: 4 >= 2 = true + * val ge2: 2 >= 3 = false + * ``` + */ + @infix type >=[X <: Int, Y <: Int] <: Boolean + + /** Less-or-equal comparison of two `Int` singleton types. + * ```scala + * val lt1: 4 <= 2 = false + * val lt2: 2 <= 2 = true + * ``` + */ + @infix type <=[X <: Int, Y <: Int] <: Boolean + + /** Absolute value of an `Int` singleton type. + * ```scala + * val abs: Abs[-1] = 1 + * ``` + */ + type Abs[X <: Int] <: Int + + /** Negation of an `Int` singleton type. + * ```scala + * val neg1: Neg[-1] = 1 + * val neg2: Neg[1] = -1 + * ``` + */ + type Negate[X <: Int] <: Int + + /** Minimum of two `Int` singleton types. + * ```scala + * val min: Min[-1, 1] = -1 + * ``` + */ + type Min[X <: Int, Y <: Int] <: Int + + /** Maximum of two `Int` singleton types. + * ```scala + * val abs: Abs[-1] = 1 + * ``` + */ + type Max[X <: Int, Y <: Int] <: Int + + /** String conversion of an `Int` singleton type. + * ```scala + * val abs: ToString[1] = "1" + * ``` + */ + type ToString[X <: Int] <: String + } + + object boolean { + + /** Negation of a `Boolean` singleton type. + * ```scala + * val notFalse: ![false] = true + * val notTrue: ![true] = false + * ``` + */ + type ![X <: Boolean] <: Boolean + + /** Exclusive disjunction of two `Boolean` singleton types. + * ```scala + * val a: true ^ true = false + * val b: false ^ true = true + * ``` + */ + @infix type ^[X <: Boolean, Y <: Boolean] <: Boolean + + /** Conjunction of two `Boolean` singleton types. + * ```scala + * val a: true && true = true + * val b: false && true = false + * ``` + */ + @infix type &&[X <: Boolean, Y <: Boolean] <: Boolean + + /** Disjunction of two `Boolean` singleton types. + * ```scala + * val a: true || false = true + * val b: false || false = false + * ``` + */ + @infix type ||[X <: Boolean, Y <: Boolean] <: Boolean + } +} diff --git a/tests/neg/singleton-ops-any.scala b/tests/neg/singleton-ops-any.scala new file mode 100644 index 000000000000..959393bbc820 --- /dev/null +++ b/tests/neg/singleton-ops-any.scala @@ -0,0 +1,13 @@ +import scala.compiletime.ops.any._ + +object Test { + val t32: 1 == 1 = true + val t33: 0 == false = false + val t34: 10 == "5" = true // error + val t35: 10 == 10 = false // error + + val t36: 1 != 1 = false + val t37: 0 != 1 = true + val t38: false != 5 = false // error + val t39: 10 != 10 = true // error +} diff --git a/tests/neg/singleton-ops-boolean.scala b/tests/neg/singleton-ops-boolean.scala new file mode 100644 index 000000000000..407560cfde57 --- /dev/null +++ b/tests/neg/singleton-ops-boolean.scala @@ -0,0 +1,23 @@ +import scala.compiletime.ops.boolean._ + +object Test { + val t0: ![true] = false + val t1: ![false] = true + val t2: ![true] = true // error + val t3: ![false] = false // error + + val t4: true && true = true + val t5: true && false = false + val t6: false && true = true // error + val t7: false && false = true // error + + val t8: true ^ true = false + val t9: false ^ true = true + val t10: false ^ false = true // error + val t11: true ^ false = false // error + + val t12: true || true = true + val t13: true || false = true + val t14 false || true = false // error + val t15: false || false = true // error +} diff --git a/tests/neg/singleton-ops-int.scala b/tests/neg/singleton-ops-int.scala new file mode 100644 index 000000000000..5f67ab1d68f1 --- /dev/null +++ b/tests/neg/singleton-ops-int.scala @@ -0,0 +1,75 @@ +import scala.compiletime.ops.int._ + +object Test { + summon[2 + 3 =:= 6 - 1] + summon[1763 =:= 41 * 43] + summon[2 + 2 =:= 3] // error + summon[29 * 31 =:= 900] // error + summon[Int <:< Int + 1] // error + summon[1 + Int <:< Int] + + val t0: 2 + 3 = 5 + val t1: 2 + 2 = 5 // error + val t2: -1 + 1 = 0 + val t3: -5 + -5 = -11 // error + + val t4: 10 * 20 = 200 + val t5: 30 * 10 = 400 // error + val t6: -10 * 2 = -20 + val t7: -2 * -2 = 4 + + val t8: 10 / 2 = 5 + val t9: 11 / -2 = -5 // Integer division + val t10: 2 / 4 = 2 // error + val t11: -1 / 0 = 1 // error + + val t12: 10 % 3 = 1 + val t13: 12 % 2 = 1 // error + val t14: 1 % -3 = 1 + val t15: -3 % 0 = 0 // error + + val t16: 1 < 0 = false + val t17: 0 < 1 = true + val t18: 10 < 5 = true // error + val t19: 5 < 10 = false // error + + val t20: 1 <= 0 = false + val t21: 1 <= 1 = true + val t22: 10 <= 5 = true // error + val t23: 5 <= 10 = false // error + + val t24: 1 > 0 = true + val t25: 0 > 1 = false + val t26: 10 > 5 = false // error + val t27: 5 > 10 = true // error + + val t28: 1 >= 1 = true + val t29: 0 >= 1 = false + val t30: 10 >= 5 = false // error + val t31: 5 >= 10 = true // error + + val t32: Abs[0] = 0 + val t33: Abs[-1] = 1 + val t34: Abs[-1] = -1 // error + val t35: Abs[1] = -1 // error + + val t36: Negate[-10] = 10 + val t37: Negate[10] = -10 + val t38: Negate[1] = 1 // error + val t39: Negate[-1] = -1 // error + + val t40: Max[-1, 10] = 10 + val t41: Max[4, 2] = 4 + val t42: Max[2, 2] = 1 // error + val t43: Max[-1, -1] = 0 // error + + val t44: Min[-1, 10] = -1 + val t45: Min[4, 2] = 2 + val t46: Min[2, 2] = 1 // error + val t47: Min[-1, -1] = 0 // error + + val t48: ToString[213] = "213" + val t49: ToString[-1] = "-1" + val t50: ToString[0] = "-0" // error + val t51: ToString[200] = "100" // error +} diff --git a/tests/neg/singleton-ops-match-type-scrutinee.scala b/tests/neg/singleton-ops-match-type-scrutinee.scala new file mode 100644 index 000000000000..ed749fa4718d --- /dev/null +++ b/tests/neg/singleton-ops-match-type-scrutinee.scala @@ -0,0 +1,12 @@ +import scala.compiletime.ops.int._ + +object Test { + type Max2[A <: Int, B <: Int] <: Int = (A < B) match { + case true => B + case false => A + } + val t0: Max2[-1, 10] = 10 + val t1: Max2[4, 2] = 4 + val t2: Max2[2, 2] = 1 // error + val t3: Max2[-1, -1] = 0 // error +} diff --git a/tests/neg/singleton-ops-recursive-match-type.scala b/tests/neg/singleton-ops-recursive-match-type.scala new file mode 100644 index 000000000000..ca7161e8fcbf --- /dev/null +++ b/tests/neg/singleton-ops-recursive-match-type.scala @@ -0,0 +1,12 @@ +import scala.compiletime.ops.int._ + +object Test { + type GCD[A <: Int, B <: Int] <: Int = B match { + case 0 => A + case _ => GCD[B, A % B] + } + val t0: GCD[10, 0] = 10 + val t1: GCD[252, 105] = 21 + val t3: GCD[105, 147] = 10 // error + val t4: GCD[1, 1] = -1 // error +} diff --git a/tests/neg/singleton-ops-string.scala b/tests/neg/singleton-ops-string.scala new file mode 100644 index 000000000000..9d3b83808092 --- /dev/null +++ b/tests/neg/singleton-ops-string.scala @@ -0,0 +1,8 @@ +import scala.compiletime.ops.string._ + +object Test { + val t0: "Hello " + "world" = "Hello world" + val t1: "" + "" = "" + val t2: "3" + "" = "33" // error + val t3: "Hello " + "world" = "error" // error +} diff --git a/tests/neg/singleton-ops-type-alias.scala b/tests/neg/singleton-ops-type-alias.scala new file mode 100644 index 000000000000..6b32ca93e338 --- /dev/null +++ b/tests/neg/singleton-ops-type-alias.scala @@ -0,0 +1,9 @@ +import scala.compiletime.ops.boolean._ + +object Test { + type Xor[A <: Boolean, B <: Boolean] = (A && ![B]) || (![A] && B) + val t0: Xor[true, true] = false + val t1: Xor[false, true] = true + val t2: Xor[true, false] = false // error + val t3: Xor[false, false] = true // error +} diff --git a/tests/pos/singleton-ops-composition.scala b/tests/pos/singleton-ops-composition.scala new file mode 100644 index 000000000000..86f280a61b4e --- /dev/null +++ b/tests/pos/singleton-ops-composition.scala @@ -0,0 +1,9 @@ +import scala.compiletime.ops.boolean._ +import scala.compiletime.ops.int._ + +object Test { + val t0: 1 + 2 * 3 = 7 + val t1: (2 * 7 + 1) % 10 = 5 + val t3: 1 * 1 + 2 * 2 + 3 * 3 + 4 * 4 = 30 + val t4: true && false || true && true || false ^ false = true +} diff --git a/tests/pos/singleton-ops-dispatch.scala b/tests/pos/singleton-ops-dispatch.scala new file mode 100644 index 000000000000..724d00126ddd --- /dev/null +++ b/tests/pos/singleton-ops-dispatch.scala @@ -0,0 +1,16 @@ +import scala.compiletime.ops._ +import scala.annotation.infix + +object Test { + @infix type +[X <: Int | String, Y <: Int | String] = (X, Y) match { + case (Int, Int) => int.+[X, Y] + case (String, String) => string.+[X, Y] + case (String, Int) => string.+[X, int.ToString[Y]] + case (Int, String) => string.+[int.ToString[X], Y] + } + + val t0: "a" + 1 = "a1" + val t1: "a" + "b" = "ab" + val t2: 1 + "b" = "1b" + val t3: 1 + 1 = 2 +}