diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 0ae11faf502b..22ff9c8a67ec 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -819,12 +819,12 @@ object desugar { * * If `inlineable` is true, tag $anonfun with an @inline annotation. */ - def makeClosure(params: List[ValDef], body: Tree, tpt: Tree = TypeTree(), inlineable: Boolean)(implicit ctx: Context) = { + def makeClosure(params: List[ValDef], body: Tree, tpt: Tree = TypeTree(), isInlineable: Boolean, isImplicit: Boolean)(implicit ctx: Context) = { var mods = synthetic | Artifact - if (inlineable) mods |= Inline + if (isInlineable) mods |= Inline Block( DefDef(nme.ANON_FUN, Nil, params :: Nil, tpt, body).withMods(mods), - Closure(Nil, Ident(nme.ANON_FUN), EmptyTree)) + Closure(Nil, Ident(nme.ANON_FUN), if (isImplicit) ImplicitEmptyTree else EmptyTree)) } /** If `nparams` == 1, expand partial function @@ -869,7 +869,7 @@ object desugar { def makeImplicitFunction(formals: List[Type], body: Tree)(implicit ctx: Context): Tree = { val params = makeImplicitParameters(formals.map(TypeTree)) - new NonEmptyFunction(params, body, Modifiers(Implicit)) + new FunctionWithMods(params, body, Modifiers(Implicit)) } /** Add annotation to tree: diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index bd9195f77ab3..b817131c0ce6 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -298,11 +298,15 @@ trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] /** Is `tree` an implicit function or closure, possibly nested in a block? */ def isImplicitClosure(tree: Tree)(implicit ctx: Context): Boolean = unsplice(tree) match { + case tree: FunctionWithMods => tree.mods.is(Implicit) case Function((param: untpd.ValDef) :: _, _) => param.mods.is(Implicit) case Closure(_, meth, _) => true case Block(Nil, expr) => isImplicitClosure(expr) - case Block(DefDef(nme.ANON_FUN, _, (param :: _) :: _, _, _) :: Nil, _: Closure) => - param.mods.is(Implicit) + case Block(DefDef(nme.ANON_FUN, _, params :: _, _, _) :: Nil, cl: Closure) => + params match { + case param :: _ => param.mods.is(Implicit) + case Nil => cl.tpt.eq(untpd.ImplicitEmptyTree) || defn.isImplicitFunctionType(cl.tpt.typeOpt) + } case _ => false } diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 32235e35e904..12992bc5dfb2 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -882,6 +882,7 @@ object Trees { @sharable val EmptyTree: Thicket = genericEmptyTree @sharable val EmptyValDef: ValDef = genericEmptyValDef + @sharable val ImplicitEmptyTree: Thicket = Thicket(Nil) // an empty tree marking an implicit closure // ----- Auxiliary creation methods ------------------ diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index f10670c42345..95049bcea6fd 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -56,13 +56,15 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { override def isType = body.isType } - /** A function type that should have non empty args */ - class NonEmptyFunction(args: List[Tree], body: Tree, val mods: Modifiers) extends Function(args, body) + /** A function type with `implicit` or `erased` modifiers */ + class FunctionWithMods(args: List[Tree], body: Tree, val mods: Modifiers) extends Function(args, body) /** A function created from a wildcard expression * @param placeholderParams a list of definitions of synthetic parameters. * @param body the function body where wildcards are replaced by * references to synthetic parameters. + * This is equivalent to Function, except that forms a special case for the overlapping + * positions tests. */ class WildcardFunction(placeholderParams: List[ValDef], body: Tree) extends Function(placeholderParams, body) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index ef5d0d1f7022..c35cf3eef62d 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -855,22 +855,17 @@ class Definitions { lazy val TupleType = mkArityArray("scala.Tuple", MaxTupleArity, 2) - def FunctionClass(n: Int, isImplicit: Boolean = false, isErased: Boolean = false)(implicit ctx: Context) = { - if (isImplicit && isErased) { - require(n > 0) + def FunctionClass(n: Int, isImplicit: Boolean = false, isErased: Boolean = false)(implicit ctx: Context) = + if (isImplicit && isErased) ctx.requiredClass("scala.ErasedImplicitFunction" + n.toString) - } - else if (isImplicit) { - require(n > 0) + else if (isImplicit) ctx.requiredClass("scala.ImplicitFunction" + n.toString) - } - else if (isErased) { - require(n > 0) + else if (isErased) ctx.requiredClass("scala.ErasedFunction" + n.toString) - } - else if (n <= MaxImplementedFunctionArity) FunctionClassPerRun()(ctx)(n) - else ctx.requiredClass("scala.Function" + n.toString) - } + else if (n <= MaxImplementedFunctionArity) + FunctionClassPerRun()(ctx)(n) + else + ctx.requiredClass("scala.Function" + n.toString) lazy val Function0_applyR = ImplementedFunctionType(0).symbol.requiredMethodRef(nme.apply) def Function0_apply(implicit ctx: Context) = Function0_applyR.symbol @@ -901,14 +896,14 @@ class Definitions { /** Is a function class. * - FunctionN for N >= 0 - * - ImplicitFunctionN for N > 0 + * - ImplicitFunctionN for N >= 0 * - ErasedFunctionN for N > 0 * - ErasedImplicitFunctionN for N > 0 */ def isFunctionClass(cls: Symbol) = scalaClassName(cls).isFunction /** Is an implicit function class. - * - ImplicitFunctionN for N > 0 + * - ImplicitFunctionN for N >= 0 * - ErasedImplicitFunctionN for N > 0 */ def isImplicitFunctionClass(cls: Symbol) = scalaClassName(cls).isImplicitFunction @@ -942,7 +937,7 @@ class Definitions { * - FunctionN for N > 22 becomes FunctionXXL * - FunctionN for 22 > N >= 0 remains as FunctionN * - ImplicitFunctionN for N > 22 becomes FunctionXXL - * - ImplicitFunctionN for 22 > N >= 0 becomes FunctionN + * - ImplicitFunctionN for N <= 22 becomes FunctionN * - ErasedFunctionN becomes Function0 * - ImplicitErasedFunctionN becomes Function0 * - anything else becomes a NoSymbol @@ -959,7 +954,7 @@ class Definitions { * - FunctionN for N > 22 becomes FunctionXXL * - FunctionN for 22 > N >= 0 remains as FunctionN * - ImplicitFunctionN for N > 22 becomes FunctionXXL - * - ImplicitFunctionN for 22 > N >= 0 becomes FunctionN + * - ImplicitFunctionN for N <= 22 becomes FunctionN * - ErasedFunctionN becomes Function0 * - ImplicitErasedFunctionN becomes Function0 * - anything else becomes a NoType diff --git a/compiler/src/dotty/tools/dotc/core/NameOps.scala b/compiler/src/dotty/tools/dotc/core/NameOps.scala index 584139248959..52a0cbb735b5 100644 --- a/compiler/src/dotty/tools/dotc/core/NameOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NameOps.scala @@ -167,59 +167,42 @@ object NameOps { } } - /** Is a synthetic function name - * - N for FunctionN - * - N for ImplicitFunctionN (N >= 1) - * - (-1) otherwise - */ def functionArity: Int = - functionArityFor(str.Function) max { + functionArityFor(str.Function) max + functionArityFor(str.ImplicitFunction) max { val n = - functionArityFor(str.ImplicitFunction) max functionArityFor(str.ErasedFunction) max functionArityFor(str.ErasedImplicitFunction) if (n == 0) -1 else n } - /** Is a function name - * - FunctionN for N >= 0 - * - ImplicitFunctionN for N >= 1 - * - ErasedFunctionN for N >= 1 - * - ErasedImplicitFunctionN for N >= 1 - * - false otherwise + /** Is a function name, i.e one of FunctionN, ImplicitFunctionN for N >= 0 or ErasedFunctionN, ErasedImplicitFunctionN for N > 0 */ def isFunction: Boolean = functionArity >= 0 - /** Is a implicit function name - * - ImplicitFunctionN for N >= 1 - * - ErasedImplicitFunctionN for N >= 1 - * - false otherwise + /** Is an implicit function name, i.e one of ImplicitFunctionN for N >= 0 or ErasedImplicitFunctionN for N > 0 */ def isImplicitFunction: Boolean = { - functionArityFor(str.ImplicitFunction) >= 1 || - functionArityFor(str.ErasedImplicitFunction) >= 1 + functionArityFor(str.ImplicitFunction) >= 0 || + functionArityFor(str.ErasedImplicitFunction) > 0 } - /** Is a implicit function name - * - ErasedFunctionN for N >= 1 - * - ErasedImplicitFunctionN for N >= 1 - * - false otherwise + /** Is an erased function name, i.e. one of ErasedFunctionN, ErasedImplicitFunctionN for N > 0 */ def isErasedFunction: Boolean = { - functionArityFor(str.ErasedFunction) >= 1 || - functionArityFor(str.ErasedImplicitFunction) >= 1 + functionArityFor(str.ErasedFunction) > 0 || + functionArityFor(str.ErasedImplicitFunction) > 0 } - /** Is a synthetic function name + /** Is a synthetic function name, i.e. one of * - FunctionN for N > 22 - * - ImplicitFunctionN for N >= 1 - * - ErasedFunctionN for N >= 1 - * - ErasedImplicitFunctionN for N >= 1 - * - false otherwise + * - ImplicitFunctionN for N >= 0 + * - ErasedFunctionN for N > 0 + * - ErasedImplicitFunctionN for N > 0 */ def isSyntheticFunction: Boolean = { functionArityFor(str.Function) > MaxImplementedFunctionArity || - functionArityFor(str.ImplicitFunction) >= 1 || + functionArityFor(str.ImplicitFunction) >= 0 || isErasedFunction } diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 79e4a469b00a..8e36471d9391 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -2892,7 +2892,6 @@ object Types { final override def isImplicitMethod: Boolean = companion.eq(ImplicitMethodType) || companion.eq(ErasedImplicitMethodType) final override def isErasedMethod: Boolean = companion.eq(ErasedMethodType) || companion.eq(ErasedImplicitMethodType) - def computeSignature(implicit ctx: Context): Signature = { val params = if (isErasedMethod) Nil else paramInfos resultSignature.prepend(params, isJavaMethod) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index f3a1218f69aa..bd28ac98c416 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -732,7 +732,7 @@ object Parsers { def functionRest(params: List[Tree]): Tree = atPos(start, accept(ARROW)) { val t = typ() - if (imods.is(Implicit) || imods.is(Erased)) new NonEmptyFunction(params, t, imods) + if (imods.is(Implicit) || imods.is(Erased)) new FunctionWithMods(params, t, imods) else Function(params, t) } def funArgTypesRest(first: Tree, following: () => Tree) = { @@ -800,7 +800,7 @@ object Parsers { case ARROW => functionRest(t :: Nil) case FORSOME => syntaxError(ExistentialTypesNoLongerSupported()); t case _ => - if (imods.is(Implicit) && !t.isInstanceOf[NonEmptyFunction]) + if (imods.is(Implicit) && !t.isInstanceOf[FunctionWithMods]) syntaxError("Types with implicit keyword can only be function types", Position(start, start + nme.IMPLICITkw.asSimpleName.length)) t } diff --git a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala index ef685b0dc0f2..14de70803de6 100644 --- a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala @@ -1722,26 +1722,6 @@ object messages { } } - case class FunctionTypeNeedsNonEmptyParameterList(isImplicit: Boolean, isErased: Boolean)(implicit ctx: Context) - extends Message(FunctionTypeNeedsNonEmptyParameterListID) { - assert(isImplicit || isErased) - val kind = "Syntax" - val mods = ((isErased, "erased") :: (isImplicit, "implicit") :: Nil).collect { case (true, mod) => mod }.mkString(" ") - val msg = mods + " function type needs non-empty parameter list" - val explanation = { - val code1 = s"type Transactional[T] = $mods Transaction => T" - val code2 = "val cl: implicit A => B" - hl"""It is not allowed to leave $mods function parameter list empty. - |Possible ways to define $mods function type: - | - |$code1 - | - |or - | - |$code2""".stripMargin - } - } - case class WrongNumberOfParameters(expected: Int)(implicit ctx: Context) extends Message(WrongNumberOfParametersID) { val kind = "Syntax" diff --git a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala index ca652e157cd5..201609078802 100644 --- a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala +++ b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala @@ -15,12 +15,14 @@ import dotty.tools.dotc.util.Positions.Position * These fall into five categories * * 1. Partial function closures, we need to generate isDefinedAt and applyOrElse methods for these. - * 2. Closures implementing non-trait classes. + * 2. Closures implementing non-trait classes * 3. Closures implementing classes that inherit from a class other than Object * (a lambda cannot not be a run-time subtype of such a class) * 4. Closures that implement traits which run initialization code. * 5. Closures that get synthesized abstract methods in the transformation pipeline. These methods can be * (1) superaccessors, (2) outer references, (3) accessors for fields. + * + * However, implicit function types do not count as SAM types. */ class ExpandSAMs extends MiniPhase { override def phaseName = "expandSAMs" @@ -34,7 +36,10 @@ class ExpandSAMs extends MiniPhase { override def transformBlock(tree: Block)(implicit ctx: Context): Tree = tree match { case Block(stats @ (fn: DefDef) :: Nil, Closure(_, fnRef, tpt)) if fnRef.symbol == fn.symbol => tpt.tpe match { - case NoType => tree // it's a plain function + case NoType => + tree // it's a plain function + case tpe if defn.isImplicitFunctionType(tpe) => + tree case tpe @ SAMType(_) if tpe.isRef(defn.PartialFunctionClass) => val tpe1 = checkRefinements(tpe, fn.pos) toPartialFunction(tree, tpe1) diff --git a/compiler/src/dotty/tools/dotc/transform/ShortcutImplicits.scala b/compiler/src/dotty/tools/dotc/transform/ShortcutImplicits.scala index 3cc8b68233d0..eb08329ec3e6 100644 --- a/compiler/src/dotty/tools/dotc/transform/ShortcutImplicits.scala +++ b/compiler/src/dotty/tools/dotc/transform/ShortcutImplicits.scala @@ -73,7 +73,7 @@ class ShortcutImplicits extends MiniPhase with IdentityDenotTransformer { thisPh ctx.fresh.updateStore(DirectMeth, newMutableSymbolMap[Symbol]) /** Should `sym` get a ..$direct companion? - * This is the case if (1) `sym` is a method with an implicit function type as final result type. + * This is the case if `sym` is a method with a non-nullary implicit function type as final result type. * However if `specializeMonoTargets` is false, we exclude symbols that are known * to be only targets of monomorphic calls because they are effectively * final and don't override anything. @@ -81,6 +81,7 @@ class ShortcutImplicits extends MiniPhase with IdentityDenotTransformer { thisPh private def shouldBeSpecialized(sym: Symbol)(implicit ctx: Context) = sym.is(Method, butNot = Accessor) && defn.isImplicitFunctionType(sym.info.finalResultType) && + defn.functionArity(sym.info.finalResultType) > 0 && !sym.isAnonymousFunction && (specializeMonoTargets || !sym.isEffectivelyFinal || sym.allOverriddenSymbols.nonEmpty) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 24ed19112fb6..60ec32a778e2 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -761,14 +761,17 @@ class Typer extends Namer def typedFunctionType(tree: untpd.Function, pt: Type)(implicit ctx: Context) = { val untpd.Function(args, body) = tree val (isImplicit, isErased) = tree match { - case tree: untpd.NonEmptyFunction => - if (args.nonEmpty) (tree.mods.is(Implicit), tree.mods.is(Erased)) - else { - ctx.error(FunctionTypeNeedsNonEmptyParameterList(tree.mods.is(Implicit), tree.mods.is(Erased)), tree.pos) - (false, false) + case tree: untpd.FunctionWithMods => + val isImplicit = tree.mods.is(Implicit) + var isErased = tree.mods.is(Erased) + if (isErased && args.isEmpty) { + ctx.error("An empty function cannot not be erased", tree.pos) + isErased = false } + (isImplicit, isErased) case _ => (false, false) } + val funCls = defn.FunctionClass(args.length, isImplicit, isErased) /** Typechecks dependent function type with given parameters `params` */ @@ -803,6 +806,11 @@ class Typer extends Namer def typedFunctionValue(tree: untpd.Function, pt: Type)(implicit ctx: Context) = { val untpd.Function(params: List[untpd.ValDef] @unchecked, body) = tree + val isImplicit = tree match { + case tree: untpd.FunctionWithMods => tree.mods.is(Implicit) + case _ => false + } + pt match { case pt: TypeVar if untpd.isFunctionWithUnknownParamType(tree) => // try to instantiate `pt` if this is possible. If it does not @@ -921,8 +929,8 @@ class Typer extends Namer else cpy.ValDef(param)( tpt = untpd.TypeTree( inferredParamType(param, protoFormal(i)).underlyingIfRepeated(isJava = false))) - val inlineable = pt.hasAnnotation(defn.InlineParamAnnot) - desugar.makeClosure(inferredParams, fnBody, resultTpt, inlineable) + val isInlineable = pt.hasAnnotation(defn.InlineParamAnnot) + desugar.makeClosure(inferredParams, fnBody, resultTpt, isInlineable, isImplicit) } typed(desugared, pt) } @@ -947,7 +955,11 @@ class Typer extends Namer |because it has internal parameter dependencies, |position = ${tree.pos}, raw type = ${mt.toString}""") // !!! DEBUG. Eventually, convert to an error? } - else EmptyTree + else if ((tree.tpt `eq` untpd.ImplicitEmptyTree) && mt.paramNames.isEmpty) + // Note implicitness of function in target type sicne there are no method parameters that indicate it. + TypeTree(defn.FunctionOf(Nil, mt.resType, isImplicit = true, isErased = false)) + else + EmptyTree } case tp => throw new java.lang.Error(i"internal error: closing over non-method $tp, pos = ${tree.pos}") diff --git a/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala b/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala index 101d5664973c..e997b3170555 100644 --- a/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala +++ b/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala @@ -873,20 +873,6 @@ class ErrorMessagesTests extends ErrorMessagesTest { assertEquals(err, WildcardOnTypeArgumentNotAllowedOnNew()) } - @Test def implicitFunctionTypeNeedsNonEmptyParameterList = - checkMessagesAfter(RefChecks.name) { - """abstract class Foo { - | type Contextual[T] = implicit () => T - | val x: implicit () => Int - |}""".stripMargin - } - .expect { (ictx, messages) => - implicit val ctx: Context = ictx - - assertMessageCount(2, messages) - messages.foreach(assertEquals(_, FunctionTypeNeedsNonEmptyParameterList(isImplicit = true, isErased = false))) - } - @Test def wrongNumberOfParameters = checkMessagesAfter(RefChecks.name) { """object NumberOfParams { @@ -1310,48 +1296,6 @@ class ErrorMessagesTests extends ErrorMessagesTest { assertEquals(symbol.name.mangledString, "a") } - @Test def i4127a = - checkMessagesAfter(FrontEnd.name) { - """ - |class Foo { - | val x: implicit () => Int = () => 1 - |} - """.stripMargin - }.expect { (ictx, messages) => - implicit val ctx: Context = ictx - assertMessageCount(1, messages) - val (msg @ FunctionTypeNeedsNonEmptyParameterList(_, _)) :: Nil = messages - assertEquals(msg.mods, "implicit") - } - - @Test def i4127b = - checkMessagesAfter(FrontEnd.name) { - """ - |class Foo { - | val x: erased () => Int = () => 1 - |} - """.stripMargin - }.expect { (ictx, messages) => - implicit val ctx: Context = ictx - assertMessageCount(1, messages) - val (msg @ FunctionTypeNeedsNonEmptyParameterList(_, _)) :: Nil = messages - assertEquals(msg.mods, "erased") - } - - @Test def i4127c = - checkMessagesAfter(FrontEnd.name) { - """ - |class Foo { - | val x: erased implicit () => Int = () => 1 - |} - """.stripMargin - }.expect { (ictx, messages) => - implicit val ctx: Context = ictx - assertMessageCount(1, messages) - val (msg @ FunctionTypeNeedsNonEmptyParameterList(_, _)) :: Nil = messages - assertEquals(msg.mods, "erased implicit") - } - @Test def renameImportTwice = checkMessagesAfter(PostTyper.name) { """ diff --git a/tests/neg/i2642.scala b/tests/neg/i2642.scala index df6e0f599ac1..fce66d13b527 100644 --- a/tests/neg/i2642.scala +++ b/tests/neg/i2642.scala @@ -1,4 +1,10 @@ object Foo { - type X = implicit () => Int // error: implicit function needs parameters - def ff: X = () // error: found: Unit, expected: X + type X = implicit () => Int // now ok, used to be: implicit function needs parameters + def ff: X = () // error: found: Unit, expected: Int + + type Y = erased () => Int // error: empty function may not be erased + def gg: Y = () // error: found: Unit, expected: Y + + type Z = erased implicit () => Int // error: empty function may not be erased + def hh: Z = () // error: found: Unit, expected: Int } diff --git a/tests/run/i2642.scala b/tests/run/i2642.scala new file mode 100644 index 000000000000..6349f2e10d65 --- /dev/null +++ b/tests/run/i2642.scala @@ -0,0 +1,7 @@ +// Tests nullary implicit function types +object Test extends App { + class I + type X = implicit () => Int + def ff: X = 2 + assert(ff == 2) +}