From c9679f6c0f3c8200e1b1f537e89488094cfc2576 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 8 Jul 2013 11:05:55 +0200 Subject: Added functionality to deal with function applications. - Added Applications class to represent applications - Added Constraint class to represent type constraints - Added TyperState class to represent typer state - Added Diagnostic class to buffer errors and warnings - Added Inferencing class that contains some common functionality for type inferencing (this one's still rudimentary). - Added extractor for FunctionType in Definitions - Added desugaring of default parameters to default getters in Desugar - Added flags to deal with default parameters - Added substitutions that replace bound parameters --- src/dotty/tools/dotc/typer/Applications.scala | 645 ++++++++++++++++++++++++++ 1 file changed, 645 insertions(+) create mode 100644 src/dotty/tools/dotc/typer/Applications.scala (limited to 'src/dotty/tools/dotc/typer/Applications.scala') diff --git a/src/dotty/tools/dotc/typer/Applications.scala b/src/dotty/tools/dotc/typer/Applications.scala new file mode 100644 index 000000000..9fdf5084c --- /dev/null +++ b/src/dotty/tools/dotc/typer/Applications.scala @@ -0,0 +1,645 @@ +package dotty.tools +package dotc +package typer + +import core._ +import ast.{Trees, untpd, tpd, TreeInfo} +import util.Positions._ +import Trees.Untyped +import Contexts._ +import Types._ +import Flags._ +import Denotations._ +import NameOps._ +import Symbols._ +import Types._ +import Decorators._ +import Names._ +import StdNames._ +import Constants._ +import Inferencing._ +import collection.mutable +import language.implicitConversions + +object Applications { + + private val isNamedArg = (arg: Any) => arg.isInstanceOf[Trees.NamedArg[_]] + def hasNamedArg(args: List[Any]) = args exists isNamedArg +} + +trait Applications { self: Typer => + + import Applications._ + import Trees._ + + private def state(implicit ctx: Context) = ctx.typerState + + def lift(defs: mutable.ListBuffer[tpd.Tree], expr: tpd.Tree, prefix: String = "")(implicit ctx: Context): tpd.Tree = + if (TreeInfo.isIdempotentExpr(expr)) expr + else { + val name = ctx.freshName(prefix).toTermName + val sym = ctx.newSymbol(ctx.owner, name, EmptyFlags, expr.tpe, coord = positionCoord(expr.pos)) + defs += tpd.ValDef(sym, expr) + tpd.Ident(sym.symRef) + } + + def liftArgs(defs: mutable.ListBuffer[tpd.Tree], methType: Type, args: List[tpd.Tree])(implicit ctx: Context) = { + val paramPrefixes = methType match { + case MethodType(paramNames, _) => paramNames map (_.toString) + case _ => args map (_ => "") + } + for ((arg, prefix) <- args zip paramPrefixes) yield lift(defs, arg, prefix) + } + + def liftApp(defs: mutable.ListBuffer[tpd.Tree], tree: tpd.Tree)(implicit ctx: Context): tpd.Tree = tree match { + case Apply(fn, args) => + tree.derivedApply(liftApp(defs, fn), liftArgs(defs, fn.tpe, args)) + case TypeApply(fn, targs) => + tree.derivedTypeApply(liftApp(defs, fn), targs) + case Select(pre, name) => + tree.derivedSelect(lift(defs, pre), name) + case Ident(name) => + lift(defs, tree) + case Block(stats, expr) => + liftApp(defs ++= stats, expr) + case _ => + tree + } + + def isCompatible(tp: Type, pt: Type): Boolean = ??? + + /** + * @param Arg the type of arguments, could be tpd.Tree, untpd.Tree, or Type + * @param methRef the reference to the method of the application + * @param funType the type of the function part of the application + * @param args the arguments of the application + * @param resultType the expected result type of the application + */ + abstract class Application[Arg](methRef: TermRef, funType: Type, args: List[Arg], resultType: Type)(implicit ctx: Context) { + + /** The type of typed arguments: either tpd.Tree or Type */ + type TypedArg + + /** Given an original argument and the type of the corresponding formal + * parameter, produce a typed argument. + */ + protected def typedArg(arg: Arg, formal: Type): TypedArg + + /** Turn a typed tree into an argument */ + protected def treeToArg(arg: tpd.Tree): Arg + + /** Check that argument corresponds to type `formal` and + * possibly add it to the list of adapted arguments + */ + protected def addArg(arg: TypedArg, formal: Type): Unit + + /** Is this an argument of the form `expr: _*` or a RepeatedParamType + * derived from such an argument? + */ + protected def isVarArg(arg: Arg): Boolean + + /** If constructing trees, turn last `n` processed arguments into a + * `SeqLiteral` tree with element type `elemFormal`. + */ + protected def makeVarArg(n: Int, elemFormal: Type): Unit + + /** Signal failure with given message at position of given argument */ + protected def fail(msg: => String, arg: Arg): Unit + + /** Signal failure with given message at position of the application itself */ + protected def fail(msg: => String): Unit + + /** If constructing trees, the current function part, which might be + * affected by lifting. EmptyTree otherwise. + */ + protected def normalizedFun: tpd.Tree + + /** If constructing trees, pull out all parts of the function + * which are not idempotent into separate prefix definitions + */ + protected def liftFun(): Unit = () + + /** A flag signalling that the application was so far succesful */ + protected var ok = true + + /** The function's type after widening and instantiating polytypes + * with polyparams or typevars in constraint set + */ + val methType = funType.widen match { + case funType: MethodType => funType + case funType: PolyType => polyResult(ctx.track(funType)) + case _ => funType + } + + /** The arguments re-ordered so that each named argument matches the + * same-named formal parameter. + */ + val orderedArgs = + if (hasNamedArg(args)) + reorder(args.asInstanceOf[List[untpd.Tree]]).asInstanceOf[List[Arg]] + else + args + + methType match { + case methType: MethodType => + // apply the result type constraint, unless method type is dependent + if (!methType.isDependent) + ok = ok && constrainResult(methType.resultType, resultType) + // match all arguments with corresponding formal parameters + matchArgs(orderedArgs, methType.paramTypes, 0) + case _ => + if (methType.isError) ok = false + else fail(s"$methString does not take parameters") + } + + /** The application was succesful */ + def success = ok + + private def state = ctx.typerState + + protected def methodType = methType.asInstanceOf[MethodType] + private def methString: String = s"method ${methRef.name}: ${methType.show}" + + /** The result type of a polytype; overridden in TypedApplication */ + protected def polyResult(polytpe: PolyType): Type = polytpe.resultType + + /** Re-order arguments to correctly align named arguments */ + def reorder[T >: Untyped](args: List[Tree[T]]): List[Tree[T]] = { + var namedToArg: Map[Name, Tree[T]] = + (for (NamedArg(name, arg1) <- args) yield (name, arg1)).toMap + + def badNamedArg(arg: Tree[_ >: Untyped]): Unit = { + val NamedArg(name, _) = arg + def msg = + if (methodType.paramNames contains name) + s"parameter $name of $methString is already instantiated" + else + s"$methString does not have a parameter $name" + fail(msg, arg.asInstanceOf[Arg]) + } + + def recur(pnames: List[Name], args: List[Tree[T]]): List[Tree[T]] = pnames match { + case pname :: pnames1 => + namedToArg get pname match { + case Some(arg) => + namedToArg -= pname + arg :: recur(pnames1, args) + case None => + args match { + case (arg @ NamedArg(aname, _)) :: args1 => + if (namedToArg contains aname) + emptyTree[T]() :: recur(pnames1, args) + else { + badNamedArg(arg) + recur(pnames1, args1) + } + case arg :: args1 => + arg :: recur(pnames1, args1) + case Nil => + recur(pnames1, args) + } + } + case nil => + if (hasNamedArg(args)) { + val (namedArgs, otherArgs) = args partition isNamedArg + namedArgs foreach badNamedArg + otherArgs + } + else args + } + + recur(methodType.paramNames, args) + } + + /** Splice new method reference into existing application */ + def spliceMeth(meth: tpd.Tree, app: tpd.Tree): tpd.Tree = app match { + case Apply(fn, args) => tpd.Apply(spliceMeth(meth, fn), args) + case TypeApply(fn, targs) => tpd.TypeApply(spliceMeth(meth, fn), targs) + case _ => meth + } + + /** Find reference to default parameter getter for parameter #n in current + * parameter list, or NoType if none was found + */ + def findDefaultGetter(n: Int)(implicit ctx: Context): Type = { + def getterName = methRef.name.toTermName.defaultGetterName(n) + def ref(pre: Type, sym: Symbol): Type = + if (pre.exists && sym.isTerm) TermRef.withSym(pre, sym.asTerm) else NoType + val meth = methRef.symbol + if (meth.hasDefaultParams) + methRef.prefix match { + case NoPrefix => + def findDefault(cx: Context): Type = { + if (cx eq NoContext) NoType + else if (cx.scope != cx.outer.scope && + cx.lookup(methRef.name) + .filterWithPredicate(_.symbol == meth).exists) { + val denot = cx.lookup(getterName).toDenot(NoPrefix) + NamedType(NoPrefix, getterName).withDenot(denot) + } else findDefault(cx.outer) + } + findDefault(ctx) + case mpre => + val cls = meth.owner + val pre = + if (meth.isClassConstructor) { + mpre.baseType(cls) match { + case TypeRef(clspre, _) => ref(clspre, cls.companionModule) + case _ => NoType + } + } else mpre + ref(pre, pre.member(getterName).symbol) + } + else NoType + } + + /** Match re-ordered arguments against formal parameters + * @param n The position of the first parameter in formals in `methType`. + */ + def matchArgs(args: List[Arg], formals: List[Type], n: Int): Unit = { + if (success) formals match { + case formal :: formals1 => + + def addTyped(arg: Arg, formal: Type) = + addArg(typedArg(arg, formal), formal) + + def missingArg(n: Int): Unit = { + val pname = methodType.paramNames(n) + fail( + if (pname contains '$') s"not enough arguments for $methString" + else s"missing argument for parameter $pname of $methString") + } + + def tryDefault(n: Int, args1: List[Arg]): Unit = { + findDefaultGetter(n + TreeInfo.numArgs(normalizedFun)) match { + case dref: NamedType => + liftFun() + addTyped(treeToArg(spliceMeth(tpd.Ident(dref), normalizedFun)), formal) + matchArgs(args1, formals1, n + 1) + case _ => + missingArg(n) + } + } + + if (formal.isRepeatedParam) + args match { + case arg :: Nil if isVarArg(arg) => + addTyped(arg, formal) + case _ => + val elemFormal = formal.typeArgs.head + args foreach (addTyped(_, elemFormal)) + makeVarArg(args.length, elemFormal) + } + else args match { + case EmptyTree :: args1 => + tryDefault(n, args1) + case arg :: args1 => + addTyped(arg, formal) + matchArgs(args1, formals1, n + 1) + case nil => + tryDefault(n, args) + } + + case nil => + args match { + case arg :: args1 => fail(s"too many arguments for $methString", arg) + case nil => + } + } + } + + /** Take into account that the result type of the current method + * must fit the given expected result type. + */ + def constrainResult(mt: Type, pt: Type): Boolean = pt match { + case FunProtoType(_, result) => + mt match { + case mt: MethodType if !mt.isDependent => + constrainResult(mt.resultType, pt.resultType) + case _ => + true + } + case pt: ValueType => + mt match { + case mt: ImplicitMethodType if !mt.isDependent => + constrainResult(mt.resultType, pt) + case _ => + isCompatible(mt, pt) + } + case _ => + true + } + } + + /** Subclass of Application for the cases where we are interested only + * in a "can/cannot apply" answer, without needing to construct trees or + * issue error messages. + */ + abstract class TestApplication[Arg](methRef: TermRef, funType: Type, args: List[Arg], resultType: Type)(implicit ctx: Context) + extends Application[Arg](methRef, funType, args, resultType) { + type TypedArg = Arg + type Result = Unit + + /** The type of the given argument */ + protected def argType(arg: Arg): Type + + def typedArg(arg: Arg, formal: Type): Arg = arg + def addArg(arg: TypedArg, formal: Type) = + ok = ok & isCompatible(argType(arg), formal) + def makeVarArg(n: Int, elemFormal: Type) = {} + def fail(msg: => String, arg: Arg) = + ok = false + def fail(msg: => String) = + ok = false + def normalizedFun = tpd.EmptyTree + } + + /** Subtrait of Application for the cases where arguments are (typed or + * untyped) trees. + */ + trait TreeApplication[T >: Untyped] extends Application[Tree[T]] { + type TypeArg = tpd.Tree + def isVarArg(arg: Tree[T]): Boolean = TreeInfo.isWildcardStarArg(arg) + } + + /** Subclass of Application for applicability tests with trees as arguments. */ + class ApplicableToTrees(methRef: TermRef, args: List[tpd.Tree], resultType: Type)(implicit ctx: Context) + extends TestApplication(methRef, methRef, args, resultType) with TreeApplication[Type] { + def argType(arg: tpd.Tree): Type = arg.tpe + def treeToArg(arg: tpd.Tree): tpd.Tree = arg + } + + /** Subclass of Application for applicability tests with types as arguments. */ + class ApplicableToTypes(methRef: TermRef, args: List[Type], resultType: Type)(implicit ctx: Context) + extends TestApplication(methRef, methRef, args, resultType) { + def argType(arg: Type): Type = arg + def treeToArg(arg: tpd.Tree): Type = arg.tpe + def isVarArg(arg: Type): Boolean = arg.isRepeatedParam + } + + /** Subclass of Application for type checking an Apply node, where + * types of arguments are either known or unknown. + */ + abstract class TypedApply[T >: Untyped]( + app: untpd.Apply, fun: tpd.Tree, methRef: TermRef, args: List[Tree[T]], resultType: Type)(implicit ctx: Context) + extends Application(methRef, fun.tpe, args, resultType) with TreeApplication[T] { + type TypedArg = tpd.Tree + private var typedArgBuf = new mutable.ListBuffer[tpd.Tree] + private var liftedDefs: mutable.ListBuffer[tpd.Tree] = null + private var myNormalizedFun: tpd.Tree = fun + + def addArg(arg: tpd.Tree, formal: Type): Unit = + typedArgBuf += adapt(arg, Mode.Expr, formal) + + def makeVarArg(n: Int, elemFormal: Type): Unit = { + val args = typedArgBuf.takeRight(n).toList + typedArgBuf.trimEnd(n) + typedArgBuf += SeqLiteral(TypeTree(elemFormal), args) + } + + def fail(msg: => String, arg: Tree[T]) = { + ctx.error(msg, arg.pos) + ok = false + } + + def fail(msg: => String) = { + ctx.error(msg, app.pos) + ok = false + } + + def normalizedFun = myNormalizedFun + + override def liftFun(): Unit = + if (liftedDefs == null) { + liftedDefs = new mutable.ListBuffer[tpd.Tree] + myNormalizedFun = liftApp(liftedDefs, myNormalizedFun) + } + + /** Replace all parameters of tracked polytype by fresh type vars, + * and make function a TypeApply node with these type vars as arguments. + */ + override def polyResult(poly: PolyType): Type = { + val tvars = ctx.newTypeVars(poly) + myNormalizedFun = tpd.TypeApply(normalizedFun, tvars map (tpd.TypeTree(_))) + poly.substParams(poly, tvars) + } + + /** The index of the first difference between lists of trees `xs` and `ys`, + * where `EmptyTree`s in the second list are skipped. + * -1 if there are no differences. + */ + private def firstDiff[T <: Tree[_]](xs: List[T], ys: List[T], n: Int = 0): Int = xs match { + case x :: xs1 => + ys match { + case EmptyTree :: ys1 => firstDiff(xs1, ys1, n) + case y :: ys1 => if (x ne y) n else firstDiff(xs1, ys1, n + 1) + case nil => n + } + case nil => + ys match { + case EmptyTree :: ys1 => firstDiff(xs, ys1, n) + case y :: ys1 => n + case nil => -1 + } + } + def sameSeq[T <: Tree[_]](xs: List[T], ys: List[T]): Boolean = firstDiff(xs, ys) < 0 + + val result: tpd.Tree = + if (!success) app withType ErrorType + else { + var typedArgs = typedArgBuf.toList + if (!sameSeq(app.args, orderedArgs)) { + // need to lift arguments to maintain evaluation order in the + // presence of argument reorderings. + liftFun() + val eqSuffixLength = firstDiff(app.args.reverse, orderedArgs.reverse) + val (liftable, rest) = typedArgs splitAt (typedArgs.length - eqSuffixLength) + typedArgs = liftArgs(liftedDefs, methType, liftable) ++ rest + } + if (sameSeq(typedArgs, args)) // trick to cut down on tree copying + typedArgs = args.asInstanceOf[List[tpd.Tree]] + val app1 = app.withType(methodType.instantiate(typedArgs map (_.tpe))) + .derivedApply(normalizedFun, typedArgs) + if (liftedDefs != null && liftedDefs.nonEmpty) tpd.Block(liftedDefs.toList, app1) + else app1 + } + } + + /** Subclass of Application for type checking an Apply node with untyped arguments. */ + class ApplyToUntyped(app: untpd.Apply, fun: tpd.Tree, methRef: TermRef, args: List[untpd.Tree], resultType: Type)(implicit ctx: Context) + extends TypedApply(app, fun, methRef, args, resultType) { + def typedArg(arg: untpd.Tree, formal: Type): TypedArg = typed(arg, Mode.Expr, formal) + def treeToArg(arg: tpd.Tree): untpd.Tree = untpd.TypedSplice(arg) + } + + /** Subclass of Application for type checking an Apply node with typed arguments. */ + class ApplyToTyped(app: untpd.Apply, fun: tpd.Tree, methRef: TermRef, args: List[tpd.Tree], resultType: Type)(implicit ctx: Context) + extends TypedApply(app, fun, methRef, args, resultType) { + def typedArg(arg: tpd.Tree, formal: Type): TypedArg = arg + def treeToArg(arg: tpd.Tree): tpd.Tree = arg + } + + /** Is given method reference applicable to argument types `args`? + * @param resultType The expected result type of the application + */ + def isApplicableToTrees(methRef: TermRef, args: List[tpd.Tree], resultType: Type)(implicit ctx: Context) = + new ApplicableToTrees(methRef, args, resultType)(ctx.fresh.withNewTyperState).success + + /** Is given method reference applicable to arguments `args`? + * @param resultType The expected result type of the application + */ + def isApplicableToTypes(methRef: TermRef, args: List[Type], resultType: Type = WildcardType)(implicit ctx: Context) = + new ApplicableToTypes(methRef, args, resultType)(ctx.fresh.withNewTyperState).success + + /** Is `tp` a subtype of `pt`? */ + def isSubType(tp: Type, pt: Type)(implicit ctx: Context) = (tp <:< pt)(ctx.fresh.withNewTyperState) + + /** In a set of overloaded applicable alternatives, is `alt1` at least as good as + * `alt2`? `alt1` and `alt2` are nonoverloaded references. + */ + def isAsGood(alt1: TermRef, alt2: TermRef)(implicit ctx: Context): Boolean = { + + /** Is class or module class `sym1` derived from class or module class `sym2`? */ + def isDerived(sym1: Symbol, sym2: Symbol): Boolean = + if (sym1 isSubClass sym2) true + else if (sym2 is Module) isDerived(sym1, sym2.companionClass) + else (sym1 is Module) && isDerived(sym1.companionClass, sym2) + + /** Is alternative `alt1` with type `tp1` as specific as alternative + * `alt2` with type `tp2` ? This is the case if `tp2` can be applied to + * `tp1` or `tp2` is a supertype of `tp1`. + */ + def isAsSpecific(alt1: TermRef, tp1: Type, alt2: TermRef, tp2: Type): Boolean = tp1 match { + case tp1: PolyType => + def bounds(tparamRefs: List[TypeRef]) = tp1.paramBounds map (_.substParams(tp1, tparamRefs)) + val tparams = ctx.newTypeParams(alt1.symbol.owner, tp1.paramNames, EmptyFlags, bounds) + isAsSpecific(alt1, tp1.instantiate(tparams map (_.symRef)), alt2, tp2) + case tp1: MethodType => + isApplicableToTypes(alt2, tp1.paramTypes) + case _ => + isSubType(tp1, tp2) + } + + val owner1 = alt1.symbol.owner + val owner2 = alt2.symbol.owner + val tp1 = alt1.widen + val tp2 = alt2.widen + + def winsOwner1 = isDerived(owner1, owner2) + def winsType1 = isAsSpecific(alt1, tp1, alt2, tp2) + def winsOwner2 = isDerived(owner2, owner1) + def winsType2 = isAsSpecific(alt2, tp2, alt1, tp1) + + // Assume the following probabilities: + // + // P(winsOwnerX) = 2/3 + // P(winsTypeX) = 1/3 + // + // Then the call probabilities of the 4 basic operations are as follows: + // + // winsOwner1: 1/1 + // winsOwner2: 1/1 + // winsType1 : 7/9 + // winsType2 : 4/9 + + if (winsOwner1) /* 6/9 */ !winsOwner2 || /* 4/9 */ winsType1 || /* 8/27 */ !winsType2 + else if (winsOwner2) /* 2/9 */ winsType1 && /* 2/27 */ !winsType2 + else /* 1/9 */ winsType1 || /* 2/27 */ !winsType2 + } + + /** Resolve overloaded alternative `alts`, given expected type `pt`. */ + def resolveOverloaded(alts: List[TermRef], pt: Type)(implicit ctx: Context): List[TermRef] = { + + def isDetermined(alts: List[TermRef]) = alts.isEmpty || alts.tail.isEmpty + + /** The shape of given tree as a type; cannot handle named arguments. */ + def typeShape(tree: untpd.Tree): Type = tree match { + case untpd.Function(args, body) => + defn.FunctionType(args map Function.const(defn.AnyType), typeShape(body)) + case _ => + defn.NothingType + } + + /** The shape of given tree as a type; is more expensive than + * typeShape but can can handle named arguments. + */ + def treeShape(tree: untpd.Tree): tpd.Tree = tree match { + case NamedArg(name, arg) => + val argShape = treeShape(arg) + tree.withType(argShape.tpe).derivedNamedArg(name, argShape) + case _ => + Literal(Constant(null)) withType typeShape(tree) + } + + def narrowByTypes(alts: List[TermRef], argTypes: List[Type], resultType: Type): List[TermRef] = + alts filter (isApplicableToTypes(_, argTypes, resultType)) + + def narrowMostSpecific(alts: List[TermRef]): List[TermRef] = (alts: @unchecked) match { + case alt :: alts1 => + def winner(bestSoFar: TermRef, alts: List[TermRef]): TermRef = alts match { + case alt :: alts1 => + winner(if (isAsGood(alt, bestSoFar)) alt else bestSoFar, alts1) + case nil => + bestSoFar + } + val best = winner(alt, alts1) + def asGood(alts: List[TermRef]): List[TermRef] = alts match { + case alt :: alts1 => + if ((alt eq best) || !isAsGood(alt, best)) asGood(alts1) + else alt :: asGood(alts1) + case nil => + Nil + } + best :: asGood(alts1) + } + + val candidates = pt match { + case pt @ FunProtoType(args, resultType) => + val numArgs = args.length + + def sizeFits(alt: TermRef, tp: Type): Boolean = tp match { + case tp: PolyType => sizeFits(alt, tp.resultType) + case MethodType(_, ptypes) => + val numParams = ptypes.length + def isVarArgs = ptypes.nonEmpty && ptypes.last.isRepeatedParam + def hasDefault = alt.symbol.hasDefaultParams + if (numParams == numArgs) true + else if (numParams < numArgs) isVarArgs + else if (numParams > numArgs + 1) hasDefault + else isVarArgs || hasDefault + } + + def narrowBySize(alts: List[TermRef]): List[TermRef] = + alts filter (alt => sizeFits(alt, alt.widen)) + + def narrowByShapes(alts: List[TermRef]): List[TermRef] = + if (args exists (_.isInstanceOf[untpd.Function])) + if (args exists (_.isInstanceOf[NamedArg[_]])) + narrowByTrees(alts, args map treeShape, resultType) + else + narrowByTypes(alts, args map typeShape, resultType) + else + alts + + def narrowByTrees(alts: List[TermRef], args: List[tpd.Tree], resultType: Type): List[TermRef] = + alts filter (isApplicableToTrees(_, args, resultType)) + + val alts1 = narrowBySize(alts) + if (isDetermined(alts1)) alts1 + else { + val alts2 = narrowByShapes(alts1) + if (isDetermined(alts2)) alts2 + else narrowByTrees(alts2, pt.typedArgs, resultType) + } + + case defn.FunctionType(args, resultType) => + narrowByTypes(alts, args, resultType) + + case tp => + alts filter (isSubType(_, tp)) + } + + if (isDetermined(candidates)) candidates + else narrowMostSpecific(candidates) + } +} \ No newline at end of file -- cgit v1.2.3