diff --git a/effekt/shared/src/main/scala/effekt/Lexer.scala b/effekt/shared/src/main/scala/effekt/Lexer.scala index 73ba5828e..11848f1cf 100644 --- a/effekt/shared/src/main/scala/effekt/Lexer.scala +++ b/effekt/shared/src/main/scala/effekt/Lexer.scala @@ -130,6 +130,7 @@ enum TokenKind { case `...` case `^^` case `^` + case `?` // keywords case `let` @@ -472,6 +473,7 @@ class Lexer(source: Source) extends Iterator[Token] { case ('*', '=') => advanceWith(TokenKind.`*=`) case ('*', _) => advanceWith(TokenKind.`*`) + case ('?', _) => advanceWith(TokenKind.`?`) case ('$', '{') => interpolationDepths.push(depthTracker.braces + 1) diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index fbdbc3bca..a8109003e 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -7,14 +7,16 @@ package namer import effekt.context.{Annotations, Context, ContextOps} import effekt.context.assertions.* import effekt.typer.Substitutions -import effekt.source.{Def, Id, IdDef, IdRef, Many, MatchGuard, ModuleDecl, Term, Tree, sourceOf} +import effekt.source.{Def, Id, IdDef, IdRef, Many, MatchGuard, ModuleDecl, Term, Tree, sourceOf, GenerateImplicitArgs} import effekt.symbols.* import effekt.util.messages.ErrorMessageReifier import effekt.symbols.scopes.* +import effekt.context.Try import scala.annotation.tailrec import scala.collection.mutable import scala.util.DynamicVariable +import effekt.util.RequirementLevel /** * The output of this phase: a mapping from source identifier to symbol @@ -337,6 +339,7 @@ object Namer extends Phase[Parsed, NameResolved] { // FunDef and InterfaceDef have already been resolved as part of the module declaration case f @ source.FunDef(id, tparams, vparams, bparams, captures, ret, body, doc, span) => + checkImplicitParams(vparams.unspan); checkImplicitParams(bparams.unspan) val sym = f.symbol Context.scopedWithName(id.name) { sym.tparams.foreach { p => Context.bind(p) } @@ -347,6 +350,7 @@ object Namer extends Phase[Parsed, NameResolved] { } case f @ source.ExternDef(id, tparams, vparams, bparams, captures, ret, bodies, doc, span) => + checkImplicitParams(vparams.unspan); checkImplicitParams(bparams.unspan) val sym = f.symbol Context.scopedWithName(id.name) { sym.tparams.foreach { p => Context.bind(p) } @@ -364,6 +368,7 @@ object Namer extends Phase[Parsed, NameResolved] { val interface = Context.symbolOf(interfaceId).asInterface interface.operations = operations.map { case op @ source.Operation(id, tparams, vparams, bparams, ret, doc, span) => Context.at(op) { + checkImplicitParams(vparams); checkImplicitParams(bparams) val name = Context.nameFor(id) val opSym = Context.scopedWithName(id.name) { @@ -445,6 +450,47 @@ object Namer extends Phase[Parsed, NameResolved] { resolve(a.value) } + /** + * Checks that there are no implicit parameters if there shouldn't be (or there are if there must be). + * Also checks that there are no non-implicit parameters after implicit ones. + */ + @tailrec + def checkImplicitParams(l: List[source.Param], implicitsAllowed: RequirementLevel = RequirementLevel.Optional)(using Context): Unit = (l, implicitsAllowed) match { + case ((p@(source.ValueParam(_, _, true, _) | source.BlockParam(_, _, true, _))) :: tl, RequirementLevel.Forbidden) => + Context.at(p) { + Context.error(pretty"Implicit parameter ${l.head.span.text.getOrElse(l.head.id.name)} can never be passed implicitly to here.") + } + case ((p@(source.ValueParam(_, _, false, _) | source.BlockParam(_, _, false, _))) :: tl, RequirementLevel.Required) => + Context.at(p) { + Context.error(pretty"Parameter ${l.head.span.text.getOrElse(l.head.id.name)} needs to be implicit so earlier implicit parameters can be passed implicitly.") + } + case ((source.ValueParam(_, _, true, _) | source.BlockParam(_, _, true, _)) :: tl, RequirementLevel.Optional) => + checkImplicitParams(tl, RequirementLevel.Required) // require all arguments after an implicit one to be implicit + case (_ :: tl, _) => + checkImplicitParams(tl, implicitsAllowed) + case (Nil, _) => () + } + + /** + * Name-resolves all implicit arguments that could be interesting for calling the given function symbol. + * + * Errors while doing so are residualized into the respective [[ImplicitContext]] and only reported + * during overload resolution later. + */ + def resolveImplicits(id: source.Id)(using Context): Unit = { + Context.symbolOf(id) match { + case symbols.CallTarget(syms, cs) => + cs.foreach { + case (b, c@ImplicitContext(vimpls, bimpls)) if !c.resolved => + c.resolved = true // set as resolved first, so recursive calls do not continue doing so. + c.values = vimpls.map { case k -> mv => k -> GenerateImplicitArgs.runPhaseOn(k, mv){ v => Try { resolve(v) } } } + c.blocks = bimpls.map { case k -> mb => k -> GenerateImplicitArgs.runPhaseOn(k, mb){ b => Try { resolve(b) } } } + case _ => () + } + case _ => () + } + } + def resolve(t: source.Term)(using Context): Unit = Context.focusing(t) { case source.Literal(value, tpe, _) => () @@ -491,7 +537,7 @@ object Namer extends Phase[Parsed, NameResolved] { case tree @ source.Region(name, body, _) => val regionName = Name.local(name.name) - val reg = BlockParam(regionName, Some(builtins.TRegion), CaptureParam(regionName), tree) + val reg = BlockParam(regionName, Some(builtins.TRegion), CaptureParam(regionName), isImplicit = false, tree) Context.define(name, reg) Context scoped { Context.bindBlock(reg) @@ -499,6 +545,8 @@ object Namer extends Phase[Parsed, NameResolved] { } case f @ source.BlockLiteral(tparams, vparams, bparams, stmt, _) => + checkImplicitParams(vparams, RequirementLevel.Forbidden) + checkImplicitParams(bparams, RequirementLevel.Forbidden) Context scoped { val tps = tparams map resolve val vps = vparams map resolve @@ -551,19 +599,21 @@ object Namer extends Phase[Parsed, NameResolved] { then Context.abort(pp"Cannot resolve function ${target}, called on an expression.") } } + resolveImplicits(target) targs foreach resolveValueType vargs foreach resolve bargs foreach resolve case source.Do(target, targs, vargs, bargs, _) => Context.resolveEffectCall(target) + resolveImplicits(target) targs foreach resolveValueType vargs foreach resolve bargs foreach resolve case source.Call(target, targs, vargs, bargs, _) => Context.focusing(target) { - case source.IdTarget(id) => Context.resolveFunctionCalltarget(id) + case source.IdTarget(id) => Context.resolveFunctionCalltarget(id); resolveImplicits(id) case source.ExprTarget(expr) => resolve(expr) } targs foreach resolveValueType @@ -666,19 +716,19 @@ object Namer extends Phase[Parsed, NameResolved] { * Used for fields where "please wrap this in braces" is not good advice to be told by [[resolveValueType]]. */ def resolveNonfunctionValueParam(p: source.ValueParam)(using Context): ValueParam = { - val sym = ValueParam(Name.local(p.id), p.tpe.map(tpe => resolveValueType(tpe, isParam = false)), decl = p) + val sym = ValueParam(Name.local(p.id), p.tpe.map(tpe => resolveValueType(tpe, isParam = false)), p.isImplicit, decl = p) Context.assignSymbol(p.id, sym) sym } def resolve(p: source.ValueParam)(using Context): ValueParam = { - val sym = ValueParam(Name.local(p.id), p.tpe.map(tpe => resolveValueType(tpe, isParam = true)), decl = p) + val sym = ValueParam(Name.local(p.id), p.tpe.map(tpe => resolveValueType(tpe, isParam = true)), p.isImplicit, decl = p) Context.assignSymbol(p.id, sym) sym } def resolve(p: source.BlockParam)(using Context): BlockParam = { val name = Name.local(p.id) - val sym: BlockParam = BlockParam(name, p.tpe.map { tpe => resolveBlockType(tpe, isParam = true) }, CaptureParam(name), p) + val sym: BlockParam = BlockParam(name, p.tpe.map { tpe => resolveBlockType(tpe, isParam = true) }, CaptureParam(name), p.isImplicit, p) Context.assignSymbol(p.id, sym) sym } @@ -719,7 +769,7 @@ object Namer extends Phase[Parsed, NameResolved] { case source.IgnorePattern(_) => Nil case source.LiteralPattern(lit, _) => Nil case source.AnyPattern(id, _) => - val p = ValueParam(Name.local(id), None, decl = id) + val p = ValueParam(Name.local(id), None, isImplicit = false, decl = id) Context.assignSymbol(id, p) List(p) case source.TagPattern(id, patterns, _) => @@ -1081,7 +1131,11 @@ trait NamerOps extends ContextOps { Context: Context => val syms2 = if (syms.isEmpty) scope.lookupFunction(id.path, id.name) else syms - if (syms2.nonEmpty) { assignSymbol(id, CallTarget(syms2.asInstanceOf)); true } else { false } + if (syms2.nonEmpty) { + val syms3 = syms2.asInstanceOf[List[Set[BlockSymbol]]] + assignSymbol(id, CallTarget(syms3, GenerateImplicitArgs.lookupPotentialImplicits(syms3, scope.scope))) + true + } else { false } } private[namer] def resolveOverloadedFunction(id: IdRef): Boolean = at(id) { @@ -1092,7 +1146,11 @@ trait NamerOps extends ContextOps { Context: Context => // lookup first block param and do not collect multiple since we do not (yet?) permit overloading on block parameters val syms3 = if (syms2.isEmpty) List(scope.lookupFirstBlockParam(id.path, id.name)) else syms2 - if (syms3.nonEmpty) { assignSymbol(id, CallTarget(syms3.asInstanceOf)); true } else { false } + if (syms3.nonEmpty) { + val syms4 = syms3.asInstanceOf[List[Set[BlockSymbol]]] + assignSymbol(id, CallTarget(syms4, GenerateImplicitArgs.lookupPotentialImplicits(syms4, scope.scope))) + true + } else { false } } /** @@ -1116,7 +1174,7 @@ trait NamerOps extends ContextOps { Context: Context => // Always abort with the generic message abort(pretty"Cannot find a function named `${id}`.") } - assignSymbol(id, CallTarget(blocks)) + assignSymbol(id, CallTarget(blocks, GenerateImplicitArgs.lookupPotentialImplicits(blocks, scope.scope))) } } @@ -1170,7 +1228,8 @@ trait NamerOps extends ContextOps { Context: Context => abort(pretty"Cannot resolve field access ${id}") } - assignSymbol(id, CallTarget(syms.asInstanceOf)) + val bsyms = syms.asInstanceOf[List[Set[BlockSymbol]]] + assignSymbol(id, CallTarget(bsyms, GenerateImplicitArgs.lookupPotentialImplicits(bsyms, scope.scope))) } /** @@ -1184,7 +1243,8 @@ trait NamerOps extends ContextOps { Context: Context => abort(pretty"Cannot resolve effect operation ${id}") } - assignSymbol(id, CallTarget(syms.asInstanceOf)) + val bsyms = syms.asInstanceOf[List[Set[BlockSymbol]]] + assignSymbol(id, CallTarget(bsyms, GenerateImplicitArgs.lookupPotentialImplicits(bsyms, scope.scope))) } /** diff --git a/effekt/shared/src/main/scala/effekt/Parser.scala b/effekt/shared/src/main/scala/effekt/Parser.scala index 72bda0749..1bf2f42ad 100644 --- a/effekt/shared/src/main/scala/effekt/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/Parser.scala @@ -195,6 +195,9 @@ class Parser(tokens: Seq[Token], source: Source) { def peek(offset: Int, kind: TokenKind): Boolean = peek(offset).kind == kind + def skipIf(kind: TokenKind): Boolean = + if (peek.kind == kind) { skip(); true } else { false } + def hasNext(): Boolean = position < tokens.length def next(): Token = val t = tokens(position).failOnErrorToken(position) @@ -331,8 +334,8 @@ class Parser(tokens: Seq[Token], source: Source) { // Simple case: all patterns are just variable names (or ignored), no guards, no fallback // Desugar to: call { (x, y, _, ...) => body } val vparams: List[ValueParam] = patterns.unspan.map { - case AnyPattern(id, span) => ValueParam(id, None, span) - case IgnorePattern(span) => ValueParam(IdDef(s"__ignored", span.synthesized), None, span) + case AnyPattern(id, span) => ValueParam(id, None, isImplicit = false, span) + case IgnorePattern(span) => ValueParam(IdDef(s"__ignored", span.synthesized), None, isImplicit = false, span) case _ => sys.error("impossible: checked above") } BlockLiteral(Nil, vparams, Nil, body, body.span.synthesized) @@ -345,7 +348,7 @@ class Parser(tokens: Seq[Token], source: Source) { val argSpans = patternList.map(_.span) val vparams: List[ValueParam] = names.zip(argSpans).map { (name, span) => - ValueParam(IdDef(name, span.synthesized), None, span.synthesized) + ValueParam(IdDef(name, span.synthesized), None, isImplicit = false, span.synthesized) } val scrutinees = names.zip(argSpans).map { (name, span) => Var(IdRef(Nil, name, span.synthesized), span.synthesized) @@ -931,7 +934,7 @@ class Parser(tokens: Seq[Token], source: Source) { nonterminal: `with` ~> backtrack(idDef() <~ `:`) ~ implementation() match { case capabilityName ~ impl => - val capability = capabilityName map { name => BlockParam(name, Some(impl.interface), name.span.synthesized): BlockParam } + val capability = capabilityName map { name => BlockParam(name, Some(impl.interface), isImplicit = false, name.span.synthesized): BlockParam } Handler(capability.unspan, impl, span()) } @@ -1284,7 +1287,7 @@ class Parser(tokens: Seq[Token], source: Source) { val names = List.tabulate(argSpans.length){ n => s"__arg${n}" } BlockLiteral( Nil, - names.zip(argSpans).map { (name, span) => ValueParam(IdDef(name, span.synthesized), None, span.synthesized) }, + names.zip(argSpans).map { (name, span) => ValueParam(IdDef(name, span.synthesized), None, isImplicit = false, span.synthesized) }, Nil, Return( Match( @@ -1654,7 +1657,7 @@ class Parser(tokens: Seq[Token], source: Source) { def lambdaParams(): (List[Id], List[ValueParam], List[BlockParam]) = nonterminal: - if isVariable then (Nil, List(ValueParam(idDef(), None, span())), Nil) else paramsOpt() + if isVariable then (Nil, List(ValueParam(idDef(), None, isImplicit=false, span())), Nil) else paramsOpt() def params(): (Many[Id], Many[ValueParam], Many[BlockParam]) = nonterminal: @@ -1688,11 +1691,13 @@ class Parser(tokens: Seq[Token], source: Source) { def valueParam(): ValueParam = nonterminal: - ValueParam(idDef(), Some(valueTypeAnnotation()), span()) + val isImplicit = skipIf(`?`) + ValueParam(idDef(), Some(valueTypeAnnotation()), isImplicit, span()) def valueParamOpt(): ValueParam = nonterminal: - ValueParam(idDef(), maybeValueTypeAnnotation(), span()) + val isImplicit = skipIf(`?`) + ValueParam(idDef(), maybeValueTypeAnnotation(), isImplicit, span()) def maybeBlockParams(): Many[BlockParam] = nonterminal: @@ -1712,11 +1717,13 @@ class Parser(tokens: Seq[Token], source: Source) { def blockParam(): BlockParam = nonterminal: - BlockParam(idDef(), Some(blockTypeAnnotation()), span()) + val isImplicit = skipIf(`?`) + BlockParam(idDef(), Some(blockTypeAnnotation()), isImplicit, span()) def blockParamOpt(): BlockParam = nonterminal: - BlockParam(idDef(), when(`:`)(Some(blockType()))(None), span()) + val isImplicit = skipIf(`?`) + BlockParam(idDef(), when(`:`)(Some(blockType()))(None), isImplicit, span()) def maybeValueTypes(): Many[Type] = nonterminal: diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index f9c345af2..8433affdb 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -7,15 +7,15 @@ package typer import effekt.context.{Annotation, Annotations, Context, ContextOps} import effekt.context.assertions.* import effekt.source.{AnyPattern, Def, Effectful, IgnorePattern, Many, MatchGuard, MatchPattern, Maybe, ModuleDecl, NoSource, OpClause, Stmt, TagPattern, Term, Tree, resolve, resolveBlockRef, resolveBlockType, resolveValueType, symbol} -import effekt.source.Term.BlockLiteral import effekt.symbols.* import effekt.symbols.builtins.* import effekt.symbols.kinds.* import effekt.util.messages.* import effekt.util.foreachAborting +import effekt.context.Try import scala.language.implicitConversions -import effekt.source.Implementation +import scala.collection.mutable /** * Typechecking @@ -227,7 +227,7 @@ object Typer extends Phase[NameResolved, Typechecked] { case _ => Context.abort("Cannot infer function type for callee.") } - val Result(t, eff) = checkCallTo(c, "function", Nil, tpe, targs map { _.resolveValueType }, vargs, bargs, expected) + val Result(t, eff) = checkCallTo(c, "function", Nil, tpe, targs map { _.resolveValueType }, vargs, bargs, expected, ImplicitContext.empty) Result(t, eff ++ funEffs) // precondition: PreTyper translates all uniform-function calls to `Call`. @@ -1163,11 +1163,11 @@ object Typer extends Phase[NameResolved, Typechecked] { )(using Context, Captures): Result[ValueType] = { val sym = id.symbol - val methods = sym match { + val (methods, impls) = sym match { // an overloaded call target - case CallTarget(syms) => syms.flatten.collect { case op: Operation => op } + case CallTarget(syms, impls) => (syms.flatten.collect { case op: Operation => op }, impls) // already resolved by a previous attempt to typecheck - case sym: Operation => List(sym) + case sym: Operation => (List(sym), Map.empty[BlockSymbol, ImplicitContext]) case s => Context.panic(s"Not a valid method: ${s} : ${s.getClass.getSimpleName}") } @@ -1175,7 +1175,7 @@ object Typer extends Phase[NameResolved, Typechecked] { val interface = recvTpe.asInterfaceType // filter out operations that do not fit the receiver - val candidates = methods.filter(op => op.interface == interface.typeConstructor) + val candidates = methods.filter( op => op.interface == interface.typeConstructor ) val (successes, errors) = tryEach(candidates) { op => val (funTpe, capture) = findFunctionTypeFor(op) @@ -1207,7 +1207,7 @@ object Typer extends Phase[NameResolved, Typechecked] { // 1. make up and annotate unification variables, if no type arguments are there // 2. type check call with either existing or made up type arguments - checkCallTo(call, op.name.name, op.vparams.map { p => p.name.name }, funTpe, synthTargs, vargs, bargs, expected) + checkCallTo(call, op.name.name, op.vparams.map { p => p.name.name }, funTpe, synthTargs, vargs, bargs, expected, impls.getOrElse(op, ImplicitContext.empty)) } resolveOverload(id, List(successes), errors) } @@ -1228,11 +1228,11 @@ object Typer extends Phase[NameResolved, Typechecked] { expected: Option[ValueType] )(using Context, Captures): Result[ValueType] = { - val scopes = id.symbol match { + val (scopes, impls) = id.symbol match { // an overloaded call target - case CallTarget(syms) => syms + case CallTarget(syms, impls) => (syms, impls) // already resolved by a previous attempt to typecheck - case sym: BlockSymbol => List(Set(sym)) + case sym: BlockSymbol => (List(Set(sym)), Map.empty[BlockSymbol, ImplicitContext]) case id: ValueSymbol => Context.abort(pp"Cannot call value ${id}") } @@ -1260,7 +1260,7 @@ object Typer extends Phase[NameResolved, Typechecked] { case c: Callable => c.vparams.map(_.name.name) case _ => Nil } - val Result(tpe, effs) = checkCallTo(call, receiver.name.name, vpnames, funTpe, targs, vargs, bargs, expected) + val Result(tpe, effs) = checkCallTo(call, receiver.name.name, vpnames, funTpe, targs, vargs, bargs, expected, impls.getOrElse(receiver, ImplicitContext.empty)) // This is different, compared to method calls: usingCapture(capture) Result(tpe, effs) @@ -1293,7 +1293,7 @@ object Typer extends Phase[NameResolved, Typechecked] { // Ambiguous reference case results => - val successfulOverloads = results.map { (sym, res, st) => (sym, findFunctionTypeFor(sym)._1) } + val successfulOverloads = results.map { case (sym, res, st) => (sym, findFunctionTypeFor(sym)._1) } Context.abort(AmbiguousOverloadError(successfulOverloads, Context.rangeOf(id))) } @@ -1323,9 +1323,16 @@ object Typer extends Phase[NameResolved, Typechecked] { def gotCount: Int = matched.size + extra.size def expectedCount: Int = matched.size + missing.size def delta: Int = extra.size - missing.size // > 0 => too many + } object Aligned { + extension[A,B](self: Aligned[A, B]) + def fillImplicit[R <: A](by: (B, Int) => Option[R]): (Aligned[A,B], List[R]) = { + val pre = self.matched.length + val (remaining, impl) = self.missing.zipWithIndex.partitionMap{ (a,i) => by(a, i + pre).map((_, a)).toRight(a) } + (Aligned(self.matched ++ impl, self.extra, remaining), impl.map { (x, _) => x }) + } def apply[A, B](got: List[A], expected: List[B]): Aligned[A, B] = { @scala.annotation.tailrec def loop(got: List[A], expected: List[B], matched: List[(A, B)]): Aligned[A, B] = @@ -1350,9 +1357,9 @@ object Typer extends Phase[NameResolved, Typechecked] { */ private def assertArgsParamsAlign( name: Option[String], - types: Aligned[source.Id | ValueType, TypeParam], - values: Aligned[source.ValueParam | source.ValueArg, ValueType], - blocks: Aligned[source.BlockParam | source.Term, BlockType] + types: Aligned[source.Id | ValueType | source.GenerateImplicitArgs.ImplicitStencil, TypeParam], + values: Aligned[source.ValueParam | source.ValueArg | source.GenerateImplicitArgs.ImplicitStencil, ValueType], + blocks: Aligned[source.BlockParam | source.Term | source.GenerateImplicitArgs.ImplicitStencil, BlockType] )(using Context): Unit = { // Type args are OK iff nothing provided or perfectly aligned @@ -1451,12 +1458,16 @@ object Typer extends Phase[NameResolved, Typechecked] { targs: List[ValueType], vargs: List[source.ValueArg], bargs: List[source.Term], - expected: Option[ValueType] + expected: Option[ValueType], + potentialImplicits: ImplicitContext )(using Context, Captures): Result[ValueType] = { val callsite = currentCapture // (0) Check that arg & param counts align - assertArgsParamsAlign(name = Some(name), Aligned(targs, funTpe.tparams), Aligned(vargs, funTpe.vparams), Aligned(bargs, funTpe.bparams)) + val atargs = Aligned(targs, funTpe.tparams) + val (avargs, implicitVargs) = Aligned(vargs, funTpe.vparams).fillImplicit { (_, i) => potentialImplicits.values.get(i) } + val (abargs, implicitBargs) = Aligned(bargs, funTpe.bparams).fillImplicit { (_, i) => potentialImplicits.blocks.get(i) } + assertArgsParamsAlign(name = Some(name), atargs, avargs, abargs) // (1) Instantiate blocktype // e.g. `[A, B] (A, A) => B` becomes `(?A, ?A) => ?B` @@ -1486,15 +1497,32 @@ object Typer extends Phase[NameResolved, Typechecked] { case _ => () } - (vps zip vargs) foreach { case (tpe, expr) => + val instImplicitVargs: mutable.ListBuffer[source.ValueArg] = mutable.ListBuffer.empty + val instImplicitBargs: mutable.ListBuffer[source.Term] = mutable.ListBuffer.empty + val (explicitVps, implicitVps) = vps.splitAt(vargs.length) + + (explicitVps zip vargs) foreach { case (tpe, expr) => val Result(t, eff) = checkExpr(expr.value, Some(tpe)) effs = effs ++ eff.toEffects } + // implicit arguments work like normal ones, except that we first have to instantiate them, + // and later annotate them to be inserted + (implicitVps zip implicitVargs).zipWithIndex foreach { case ((tpe, expr), ii) => + val inst = source.GenerateImplicitArgs.instantiateImplicitValue(expr, tpe) + instImplicitVargs.append(inst) + val Result(t, eff) = source.GenerateImplicitArgs.recursionGuard(expr, "value argument", vargs.length + ii, inst, tpe) { + checkExpr(inst.value, Some(tpe)) + } + effs = effs ++ eff.toEffects + } + // To improve inference, we first type check block arguments that DO NOT subtract effects, // since those need to be fully known. + val (explicitBps, implicitBps) = bps.splitAt(bargs.length) + val (explicitCaptArgs, implicitCaptArgs) = captArgs.splitAt(bargs.length) - val (withoutEffects, withEffects) = (bps zip (bargs zip captArgs)).partitionMap { + val (withoutEffects, withEffects) = (explicitBps zip (bargs zip explicitCaptArgs)).partitionMap { // TODO refine and check that eff.args refers to (inferred) type arguments of this application (`typeArgs`) case (tpe : FunctionType, rest) if tpe.effects.exists { eff => eff.args.nonEmpty } => Right((tpe, rest)) case (tpe, rest) => Left((tpe, rest)) @@ -1509,6 +1537,24 @@ object Typer extends Phase[NameResolved, Typechecked] { } } + // implicit arguments work like normal ones, except that we first have to instantiate them, + // and... + (implicitBps zip (implicitBargs zip implicitCaptArgs)).zipWithIndex foreach { case ((tpe, (expr, capt)), ii) => + flowsInto(capt, callsite) + // capture of block <: ?C + flowingInto(capt) { + val inst = source.GenerateImplicitArgs.instantiateImplicitBlock(expr, tpe) + instImplicitBargs.append(inst) + val Result(t, eff) = source.GenerateImplicitArgs.recursionGuard(expr, "block argument", bargs.length + ii, inst, tpe){ + checkExprAsBlock(inst, Some(tpe)) + } + effs = effs ++ eff.toEffects + } + } + + // ... annotate them to be inserted by [[ExplicitCapabilites]] + Context.annotateImplicits(call, instImplicitVargs.toList, instImplicitBargs.toList) + // We add return effects last to have more information at this point to // concretize the effect. effs = effs ++ Effects(retEffs) @@ -1522,7 +1568,7 @@ object Typer extends Phase[NameResolved, Typechecked] { // since this might conflate State[A] and State[B] after A -> Int, B -> Int. val capabilities = Context.provideCapabilities(call, retEffs.map(Context.unification.apply)) - val captParams = captArgs.drop(bargs.size) + val captParams = captArgs.drop(bargs.size + implicitBargs.size) (captParams zip capabilities) foreach { case (param, cap) => flowsInto(CaptureSet(cap.capture), param) } @@ -1548,29 +1594,7 @@ object Typer extends Phase[NameResolved, Typechecked] { (successes, errors) } - /** - * Returns Left(Messages) if there are any errors - * - * In the case of nested calls, currently only the errors of the innermost failing call - * are reported - */ - private def Try[T](block: => T)(using C: Context): Either[EffektMessages, T] = { - import kiama.util.Severities.Error - - val (msgs, optRes) = Context withMessages { - try { Some(block) } catch { - case FatalPhaseError(msg) => - C.report(msg) - None - } - } - if (msgs.exists { m => m.severity == Error } || optRes.isEmpty) { - Left(msgs) - } else { - Right(optRes.get) - } - } // @@ -1816,7 +1840,7 @@ trait TyperOps extends ContextOps { self: Context => freshCapabilityFor(tpe, CaptureParam(tpe.name)) private [typer] def freshCapabilityFor(tpe: InterfaceType, capt: Capture): symbols.BlockParam = - val param: BlockParam = BlockParam(tpe.name, Some(tpe), capt, NoSource) + val param: BlockParam = BlockParam(tpe.name, Some(tpe), capt, isImplicit = false, NoSource) // TODO FIXME -- generated capabilities need to be ignored in LSP! // { // override def synthetic = true @@ -1895,12 +1919,12 @@ trait TyperOps extends ContextOps { self: Context => } private[typer] def bind(p: ValueParam): Unit = p match { - case s @ ValueParam(name, Some(tpe), _) => bind(s, tpe) + case s @ ValueParam(name, Some(tpe), isImplicit, _) => bind(s, tpe) case s => panic(pretty"Internal Error: Cannot add $s to typing context.") } private[typer] def bind(p: TrackedParam): Unit = p match { - case s @ BlockParam(name, tpe, capt, _) => bind(s, tpe.get, CaptureSet(capt)) + case s @ BlockParam(name, tpe, capt, isImplicit, _) => bind(s, tpe.get, CaptureSet(capt)) case s @ ExternResource(name, tpe, capt, _) => bind(s, tpe, CaptureSet(capt)) case s @ VarBinder(name, tpe, capt, _) => bind(s, CaptureSet(capt)) } @@ -1931,6 +1955,14 @@ trait TyperOps extends ContextOps { self: Context => annotations.update(Annotations.TypeArguments, call, targs map this.unification) } + private[typer] def annotateImplicits(call: source.CallLike, + vargs: List[source.ValueArg], + bargs: List[source.Term]): Unit = { + annotations.update(Annotations.ImplicitValueArguments, call, vargs) + annotations.update(Annotations.ImplicitBlockArguments, call, bargs) + } + + private[typer] def annotatedTypeArgs(call: source.CallLike): List[symbols.ValueType] = { annotations.apply(Annotations.TypeArguments, call) } @@ -1973,6 +2005,8 @@ trait TyperOps extends ContextOps { self: Context => annotations.updateAndCommit(Annotations.InferredEffect) { case (t, effs) => subst.substitute(effs) } annotations.updateAndCommit(Annotations.TypeArguments) { case (t, targs) => targs map subst.substitute } + annotations.updateAndCommit(Annotations.ImplicitValueArguments) { case (t, ivargs) => ivargs } + annotations.updateAndCommit(Annotations.ImplicitBlockArguments) { case (t, ibargs) => ibargs } annotations.updateAndCommit(Annotations.BoundCapabilities) { case (t, caps) => caps } annotations.updateAndCommit(Annotations.CapabilityArguments) { case (t, caps) => caps } diff --git a/effekt/shared/src/main/scala/effekt/context/Annotations.scala b/effekt/shared/src/main/scala/effekt/context/Annotations.scala index 108727975..ef2640f87 100644 --- a/effekt/shared/src/main/scala/effekt/context/Annotations.scala +++ b/effekt/shared/src/main/scala/effekt/context/Annotations.scala @@ -139,6 +139,23 @@ object Annotations { "the inferred or annotated type arguments of" ) + /** + * Generated implicit value arguments for this call. + * They are annotated by [[Typer]] and will be inserted by [[ExplicitCapbilities]] + */ + val ImplicitValueArguments = TreeAnnotation[source.CallLike, List[source.ValueArg]]( + "ImplicitValueArguments", + "the inferred implicit value arguments" + ) + /** + * Generated implicit block arguments for this call. + * They are annotated by [[Typer]] and will be inserted by [[ExplicitCapbilities]] + */ + val ImplicitBlockArguments = TreeAnnotation[source.CallLike, List[source.Term]]( + "ImplicitBlockArguments", + "the inferred implicit block arguments" + ) + /** * Existential type parameters inferred by the typer when type-checking pattern matches. */ diff --git a/effekt/shared/src/main/scala/effekt/context/Context.scala b/effekt/shared/src/main/scala/effekt/context/Context.scala index 47b8c84c3..7d8707bbd 100644 --- a/effekt/shared/src/main/scala/effekt/context/Context.scala +++ b/effekt/shared/src/main/scala/effekt/context/Context.scala @@ -132,3 +132,30 @@ abstract class Context * Helper method to find the currently implicit context */ def Context(using C: Context): C.type = C + + +/** + * Returns Left(Messages) if there are any errors + * + * In the case of nested calls, currently only the errors of the innermost failing call + * are reported + */ +def Try[T](block: => T)(using C: Context): Either[EffektMessages, T] = { + import kiama.util.Severities.Error + + val (msgs, optRes) = Context withMessages { + try { + Some(block) + } catch { + case util.messages.FatalPhaseError(msg) => + C.report(msg) + None + } + } + + if (msgs.exists { m => m.severity == Error } || optRes.isEmpty) { + Left(msgs) + } else { + Right(optRes.get) + } +} \ No newline at end of file diff --git a/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala b/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala index b7318ae8f..f774fb138 100644 --- a/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala +++ b/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala @@ -43,8 +43,11 @@ object ExplicitCapabilities extends Phase[Typechecked, Typechecked], Rewrite { // an effect call -- translate to method call on the inferred capability case c @ Do(id, targs, vargs, bargs, span) => - val transformedValueArgs = vargs.map(rewrite) - val transformedBlockArgs = bargs.map(rewrite) + val implicitValueArgs = Context.annotation(Annotations.ImplicitValueArguments, c) + val implicitBlockArgs = Context.annotation(Annotations.ImplicitBlockArguments, c) + + val transformedValueArgs = (vargs ++ implicitValueArgs).map(rewrite) + val transformedBlockArgs = (bargs ++ implicitBlockArgs).map(rewrite) // the receiver of this effect operation call val receiver = Context.annotation(Annotations.CapabilityReceiver, c) @@ -66,8 +69,11 @@ object ExplicitCapabilities extends Phase[Typechecked, Typechecked], Rewrite { Select(rewrite(receiver.value), fun.id, span.synthesized) case c @ MethodCall(receiver, id, targs, vargs, bargs, span) => - val valueArgs = vargs.map { a => rewrite(a) } - val blockArgs = bargs.map { a => rewrite(a) } + val implicitValueArgs = Context.annotation(Annotations.ImplicitValueArguments, c) + val implicitBlockArgs = Context.annotation(Annotations.ImplicitBlockArguments, c) + + val valueArgs = (vargs ++ implicitValueArgs).map { a => rewrite(a) } + val blockArgs = (bargs ++ implicitBlockArgs).map { a => rewrite(a) } val capabilities = Context.annotation(Annotations.CapabilityArguments, c) val capabilityArgs = capabilities.map(referenceToCapability) @@ -81,8 +87,10 @@ object ExplicitCapabilities extends Phase[Typechecked, Typechecked], Rewrite { case c @ Call(recv, targs, vargs, bargs, span) => val receiver = rewrite(recv) - val valueArgs = vargs.map { a => rewrite(a) } - val blockArgs = bargs.map { a => rewrite(a) } + val implicitValueArgs = Context.annotation(Annotations.ImplicitValueArguments, c) + val implicitBlockArgs = Context.annotation(Annotations.ImplicitBlockArguments, c) + val valueArgs = (vargs ++ implicitValueArgs).map { a => rewrite(a) } + val blockArgs = (bargs ++ implicitBlockArgs).map { a => rewrite(a) } val capabilities = Context.annotation(Annotations.CapabilityArguments, c) val capabilityArgs = capabilities.map(referenceToCapability) @@ -154,6 +162,6 @@ object ExplicitCapabilities extends Phase[Typechecked, Typechecked], Rewrite { def definitionFor(s: symbols.BlockParam)(using C: Context): source.BlockParam = val id = IdDef(s.name.name, Span.missing) C.assignSymbol(id, s) - val tree: source.BlockParam = source.BlockParam(id, s.tpe.map { source.BlockTypeTree(_, Span.missing) }, Span.missing) + val tree: source.BlockParam = source.BlockParam(id, s.tpe.map { source.BlockTypeTree(_, Span.missing) }, isImplicit=false, Span.missing) tree } diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala new file mode 100644 index 000000000..e32258634 --- /dev/null +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -0,0 +1,372 @@ +package effekt +package source + +import scala.collection.mutable +import scala.util.DynamicVariable + +import effekt.util.messages.EffektMessages +import effekt.context.Context +import effekt.symbols.scopes.Scope +import effekt.symbols.{BlockSymbol, BlockType, ValueType, Callable, ImplicitContext, builtins, Name} +import effekt.context.Annotations +import effekt.context.Try + +object GenerateImplicitArgs { + + // Stencils + // ======== + + sealed trait ImplicitStencil { + def name: String + def kind: String + def explanation: Option[String] = None + } + case class ImplicitBlockLiteral(name: String, content: source.BlockLiteral) extends ImplicitStencil { + def kind = "block argument" + } + case class BoxedStencil(name: String, block: ImplicitStencil) extends ImplicitStencil { + def kind = "value argument" + override def explanation: Option[String] = Some( + """An implicit argument of a boxed type will be instantiated by boxing the block + | that an implicit block argument of the same name would be instantiated to""".stripMargin + + (block.explanation match { + case Some(e) => ":\n" + e + case None => ".\n" + })) + } + case class ImplicitVar(kind: String, name: String, content: source.Var) extends ImplicitStencil + case class SourcePosition(content: source.Call) extends ImplicitStencil { + def name = "sourcePosition" + def kind = "value argument" + override def explanation = + Some( + """Implicit sourcePosition will call + | SourcePosition(file, start_line, start_col, end_line, end_col) + | with the respective values for the source position of the call. + |""".stripMargin) + } + case class CallId() extends ImplicitStencil { + def name = "callId" + def kind = "value argument" + override def explanation = Some("Implicit callId will generate a unique Int for each call to this function in the source code.") + } + case class Error(underlying: ImplicitStencil, + index: Int, msgs: EffektMessages) extends ImplicitStencil { + export underlying.{name, kind} + override def explanation: Option[String] = underlying.explanation + } + + + // Termination measure / type size + // =============================== + + def typeSize(tpe: symbols.Type): Int = tpe match { + case tpe: symbols.BlockType => typeSize(tpe) + case tpe: symbols.ValueType => typeSize(tpe) + } + def typeSize(tpe: symbols.BlockType): Int = tpe match { + case BlockType.FunctionType(tparams, cparams, vparams, bparams, result, effects) => + tparams.length + cparams.length + vparams.map(typeSize).sum + bparams.map(typeSize).sum + typeSize(result) + case BlockType.InterfaceType(typeConstructor, args) => + 1 + args.map(typeSize).sum + } + def typeSize(tpe: symbols.ValueType): Int = tpe match { + case ValueType.BoxedType(tpe, capture) => 1 + typeSize(tpe) + case ValueType.ValueTypeRef(tvar) => 5 + case ValueType.ValueTypeApp(constructor, args) => 1 + args.map(typeSize).sum + } + def typeSize(effs: Effects)(using Context): Int = + effs.effs.map{ r => 1 + r.args.unspan.map { t => + Context.resolvedType(t) match { + case v: ValueType => 1 + typeSize(v) + case b: BlockType => 1 + typeSize(b) + } + }.sum }.sum + + private val recursionStack: DynamicVariable[Map[String, (Int, Int)]] = DynamicVariable(Map.empty) + + val maxRecurse = 10 + /** + * Wrapper for recursive type-checking of generated implicits. + * Should fail for infinite recursion. + */ + def recursionGuard[R](stencil: ImplicitStencil, kind: String, index: Int, inst: source.Tree, tpe: symbols.Type)(body: => R)(using Context): R = { + val instTpe = tpe match { + case tpe: symbols.BlockType => Context.unification(tpe) + case tpe: symbols.ValueType => Context.unification(tpe) + } + val tpeSize = typeSize(instTpe) + val newValue = inst match { + case source.BlockLiteral(_, _, _, source.Return(source.Call(source.IdTarget(id), _, _, _, _), _), _) => + val (depth, lastSize) = recursionStack.value.getOrElse((id.name), (0, tpeSize)) + if (tpeSize >= lastSize && depth > maxRecurse) { + Context.abort(s"Aborted recursive generation of implicit parameter ${id.name} after ${maxRecurse} levels with non-decreasing types at the same name.") + } + recursionStack.value.updated((id.name), (if tpeSize >= lastSize then depth + 1 else depth, tpeSize)) + case _ => recursionStack.value + } + recursionStack.withValue(newValue) { + Try { + body + } match { + case Left(msgs) => + // Note: Recursion + Context.abort(util.messages.ImplicitInstantiationError( + Error(stencil, index, msgs), tpe, Context.rangeOf(Context.focus))) + case Right(r) => r + } + } + } + + // Initial generation (during namer) + // ================================= + + /** + * Map for caching the result of [[lookupPotentialImplicits]] (to prevent infinite recursion in Namer) + */ + val foundImplicits: mutable.HashMap[(Scope, BlockSymbol), ImplicitContext] = mutable.HashMap.empty + + + /** + * Generate source for the given implicit value parameter, matching on the name. + * + * Will usually return a source.Var with the same name, but can act differently for special cases. + * Special cases so far: + * - sourcePosition inserts a call to SourcePosition with the components of the current source position + */ + def generateImplicitValueArg(p: symbols.ValueParam)(using Context): ImplicitStencil = { + (p.name.name, p.tpe) match { + case ("sourcePosition", _) => + // This generates a dummy source to be name-resolved (the actual arguments will be generated later) + SourcePosition(Call(IdTarget(IdRef(Nil, "SourcePosition", Span.missing)), Nil, List( + ValueArg(None, Literal("", builtins.TString, Span.missing), Span.missing), + ValueArg(None, Literal(-1L, builtins.TInt, Span.missing), Span.missing), + ValueArg(None, Literal(-1L, builtins.TInt, Span.missing), Span.missing), + ValueArg(None, Literal(-1L, builtins.TInt, Span.missing), Span.missing), + ValueArg(None, Literal(-1L, builtins.TInt, Span.missing), Span.missing), + ), Nil, Span.missing)) + case ("callId", _) => CallId() + case (name, Some(symbols.BoxedType(t, capt))) => + // try filling boxed types by instantiating a block argument and boxing it + BoxedStencil(name, generateImplicitBlockArg(symbols.BlockParam(p.name, Some(t), symbols.Capture.CaptureParam(p.name), true, NoSource))) + case _ => ImplicitVar("value argument", p.name.name, Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) + } + } + + /** + * Generate source for the given implicit block parameter, matching on the name. + * + * Will usually return an eta-expanded (based on annotated type) call to a function with the same name. + */ + def generateImplicitBlockArg(p: symbols.BlockParam)(using Context): ImplicitStencil = + p.tpe.get match { + case BlockType.FunctionType(tparams, cparams, vparams, bparams, result, effects) => + val gtparams = tparams.map { p => IdDef(p.name.name, Span.missing) } + val gvparams: List[ValueParam] = + vparams.zipWithIndex.map { (p, i) => + ValueParam(IdDef(s"arg${i}", Span.missing), Some(source.ValueTypeTree(p, Span.missing)), false, Span.missing) + } + val gbparams: List[BlockParam] = + bparams.zipWithIndex.map { (p, i) => + BlockParam(IdDef(s"block_arg${i}", Span.missing), Some(source.BlockTypeTree(p, Span.missing)), false, Span.missing) + } + ImplicitBlockLiteral(p.name.name, BlockLiteral(gtparams, gvparams, gbparams, + Return(Call(IdTarget(IdRef(Nil, p.name.name, Span.missing)), Nil, + gvparams.map { x => ValueArg(None, Var(IdRef(Nil, x.id.name, Span.missing), Span.missing), Span.missing) }, + gbparams.map { x => Var(IdRef(Nil, x.id.name, Span.missing), Span.missing) }, + Span.missing), + Span.missing), Span.missing)) + case BlockType.InterfaceType(typeConstructor, args) => + // TODO eta-exapnd here, too ? + ImplicitVar("block argument", p.name.name, Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) + } + + /** + * Returns an initial [[ImplicitContext]] for each of the [[BlockSymbol]]s. + * + * Is called from [[Namer]] to still be in the correct context to name-resolve the implicit arguments + * in [[Namer.resolveImplicits]]. + */ + def lookupPotentialImplicits(forCandidates: List[Set[BlockSymbol]], scope: Scope)(using Context): Map[BlockSymbol, ImplicitContext] = { + forCandidates.flatMap { level => + level.flatMap { b => + def findCached(b: BlockSymbol, scope: Scope): Option[ImplicitContext] = { + foundImplicits.get((scope, b)).orElse { + scope match { + case Scope.Global(_, _) => None + case Scope.Named(_, _, outer) => findCached(b, outer) + case Scope.Local(_, _, _, outer) => findCached(b, outer) + } + } + } + + findCached(b, scope).map(b -> _).orElse { + b match { + // walks up scopes, because block parameters should be eta-expanded below + case c: Callable => + val r = ImplicitContext( + c.vparams.zipWithIndex.collect { case (p, i) if p.isImplicit => i -> generateImplicitValueArg(p) }.toMap, + c.bparams.zipWithIndex.collect { case (p, i) if p.isImplicit => i -> generateImplicitBlockArg(p) }.toMap) + foundImplicits.put((scope, b), r) + Some(b -> r) + case _ => None + } + } + } + }.toMap + } + + // Instantiation (during typer) + // ============================ + + /** + * Counter for integer ids for calls in the source code, to be passed as callId. + */ + private var nextCallId: Long = 0 + + private def generateResolvedId(sym: symbols.Symbol)(using Context): (source.IdDef, source.IdRef) = { + val d = source.IdDef(sym.name.name, source.Span.missing) + val u = source.IdRef(Nil, sym.name.name, source.Span.missing) + Context.annotate(Annotations.Symbol, d, sym) + Context.annotate(Annotations.Symbol, u, sym) + (d, u) + } + + /** + * Called from [[Typer]] to get a fresh instance of the given implicit block argument. + * + * Also annotates all symbols for the returned code correctly where necessary. + */ + def instantiateImplicitBlock(b: ImplicitStencil, tpe: symbols.BlockType)(using Context): source.Term = { + if(!Context.messaging.hasErrors) { + (b, tpe) match { + case (e @ Error(s, i, msgs), _) => + Context.abort(util.messages.ImplicitInstantiationError( + e, tpe, Context.rangeOf(Context.focus))) + + case (ImplicitBlockLiteral(name, source.BlockLiteral(tparams, vparams, bparams, source.Return(source.Call(fn, targs, vargs, bargs, _), _), _)), + symbols.BlockType.FunctionType(tps, cps, vps, bps, res, effs)) => + // We need to refresh the whole binding structure, so we don't have duplicate stuff in the tree. + // Doing this in a very specialized way here. + // It annotates the correct concrete types for *this* invocation. + val (ftpsyms, ftparams, ftargs) = tparams.map { x => + val sym = symbols.TypeParam(Name.local(x.name)) + val (p, a) = generateResolvedId(sym) + (sym, p, + source.TypeRef(a, Many(Nil, source.Span.missing), source.Span.missing)) + }.unzip3 + val (fvpsyms, fvparams, fvargs) = (vparams zip vps).map { (x, t) => + val sym = symbols.ValueParam(Name.local(x.id.name), Some(t), false, NoSource) + val (p, a) = generateResolvedId(sym) + (sym, + source.ValueParam(p, Some(source.ValueTypeTree(sym.tpe.get, source.Span.missing)), false, source.Span.missing): source.ValueParam, + source.ValueArg(None, source.Var(a, source.Span.missing), source.Span.missing)) + }.unzip3 + val (fbsyms, fbparams, fbargs) = (bparams zip bps).map { (x, t) => + val sym = symbols.BlockParam(Name.local(x.id.name), Some(t), x.symbol.capture, false, NoSource) + val (p, a) = generateResolvedId(sym) + (sym, + source.BlockParam(p, Some(source.BlockTypeTree(sym.tpe.get, source.Span.missing)), false, source.Span.missing): source.BlockParam, + source.Var(a, source.Span.missing)) + }.unzip3 + val ffn = fn match { + case source.IdTarget(id) => + val r = source.IdTarget(source.IdRef(Nil, id.name, source.Span.missing)) + Context.annotate(Annotations.Symbol, r.id, + id.symbol match { + case symbols.CallTarget(syms, impls) => + symbols.CallTarget(syms, impls) // needs to be refreshed for recursive uses + case x => x + }) + r + case _ => Context.panic("Implicit block argument should be an (eta-expanded) name, not an expression") + } + source.BlockLiteral(ftparams, fvparams, fbparams, + source.Return(source.Call(ffn, ftargs, fvargs, fbargs, + source.Span.missing), source.Span.missing), source.Span.missing) + + case (ImplicitVar(kind, name, b), _) => + b // TODO Is it a problem if this is used more than once? + + case _ => + Context.panic("Unexpected type for implicit stencil for a block argument.") + } + } else { + Context.abort("Not instantiating implicit block argument since there are errors.") + } + } + + /** + * Called from [[Typer]] to get a fresh instance of the given implicit value argument. + * + * Also annotates all symbols for the returned code correctly where necessary. + */ + def instantiateImplicitValue(v: ImplicitStencil, tpe: symbols.ValueType)(using Context): source.ValueArg = { + def intArg(v: Long, name: Option[String] = None): source.ValueArg = + ValueArg(name, Literal(v, builtins.TInt, Span.missing), Span.missing) + + v match { + case e @ Error(s, i, msgs) => + Context.abort(util.messages.ImplicitInstantiationError( + e, tpe, Context.rangeOf(Context.focus))) + + case ImplicitVar(kind, name, content) => + source.ValueArg(Some(name), content, Span.missing) // TODO Is it a problem if this is used more than once? + + case SourcePosition(content) => + // this generates the version with the correct current positions, + val pos = Context.focus.span + val from = pos.source.offsetToPosition(pos.from) + val to = pos.source.offsetToPosition(pos.to) + val code: Call = Call(IdTarget(IdRef(Nil, "SourcePosition", Span.missing)), Nil, List( + ValueArg(None, Literal(pos.source.name, builtins.TString, Span.missing), Span.missing), + intArg(from.line), intArg(from.column), intArg(to.line), intArg(to.column)), Nil, Span.missing) + + // copying over the annotations generated by Namer. + val (x: IdTarget, y: IdTarget) = (content.target, code.target): @unchecked + Context.copyAnnotations(x.id, y.id) + + // and returns the result + source.ValueArg(Some(v.name), code, Span.missing) + + case CallId() => + val id = nextCallId + nextCallId = nextCallId + 1 + intArg(id, Some(v.name)) + + case BoxedStencil(name, block) => + val symbols.BoxedType(btpe, _) = tpe: @unchecked + source.ValueArg(Some(v.name), + source.Box(Maybe.None(Span.missing), + instantiateImplicitBlock(block, btpe), Span.missing), + Span.missing) + + case ImplicitBlockLiteral(_, _) => Context.panic("Cannot instantiate block literal as an implicit value argument.") + } + } + + // Running other phases on it + // ========================== + + /** + * Run body on each of the stencil code parts, generating an Error context if errors occur. + * + * This is used to pass ImplicitStencils through other phases (currently, Namer). + */ + def runPhaseOn[A](i: Int, s: ImplicitStencil)(body: source.Term => Either[EffektMessages, Unit]): ImplicitStencil = { + def runOn(s: ImplicitStencil): Either[EffektMessages, Unit] = + s match { + case ImplicitBlockLiteral(name, content) => body(content) + case ImplicitVar(kind, name, content) => body(content) + case SourcePosition(content) => body(content) + case BoxedStencil(_, block) => runOn(block) + case CallId() => Right(()) + case Error(_, _, _) => Right(()) + } + runOn(s) match { + case Left(msgs) => Error(s, i, msgs) + case Right(()) => s + } + } + +} diff --git a/effekt/shared/src/main/scala/effekt/source/Tree.scala b/effekt/shared/src/main/scala/effekt/source/Tree.scala index 597036b2f..93e188649 100644 --- a/effekt/shared/src/main/scala/effekt/source/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/source/Tree.scala @@ -293,8 +293,8 @@ case class Include(path: String, span: Span) extends Tree * Parameters and arguments */ enum Param extends Definition { - case ValueParam(id: IdDef, tpe: Option[ValueType], span: Span) - case BlockParam(id: IdDef, tpe: Option[BlockType], span: Span) + case ValueParam(id: IdDef, tpe: Option[ValueType], isImplicit: Boolean, span: Span) + case BlockParam(id: IdDef, tpe: Option[BlockType], isImplicit: Boolean, span: Span) } export Param.* @@ -738,6 +738,7 @@ case class FunctionType(tparams: Many[Id], vparams: Many[ValueType], bparams: Ma */ case class Effectful(tpe: ValueType, eff: Effects, span: Span) extends Type + // These are just type aliases for documentation purposes. type BlockType = Type type ValueType = Type diff --git a/effekt/shared/src/main/scala/effekt/symbols/DeclPrinter.scala b/effekt/shared/src/main/scala/effekt/symbols/DeclPrinter.scala index 7ca6228b2..e5e86db77 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/DeclPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/DeclPrinter.scala @@ -75,12 +75,13 @@ object DeclPrinter extends ParenPrettyPrinter { pp"def ${ d.name }: ${ tpe }" } + def questionMarkIf(b: Boolean): String = if b then "?" else "" def format(kw: String, f: Callable, result: Option[ValueType], effects: Option[Effects]): Doc = { val tps = if (f.tparams.isEmpty) "" else s"[${f.tparams.mkString(", ")}]" - val valueParams = f.vparams.map { p => pp"${p.name}: ${p.tpe.get}" }.mkString(", ") + val valueParams = f.vparams.map { p => pp"${questionMarkIf(p.isImplicit)}${p.name}: ${p.tpe.get}" }.mkString(", ") val vps = if valueParams.isEmpty then "" else s"($valueParams)" - val bps = f.bparams.map { b => pp"{ ${b.name}: ${b.tpe.get} }" }.mkString("") + val bps = f.bparams.map { b => pp"{ ${questionMarkIf(b.isImplicit)}${b.name}: ${b.tpe.get} }" }.mkString("") val ps = if (vps.isEmpty && bps.isEmpty) "()" else s"$vps$bps" diff --git a/effekt/shared/src/main/scala/effekt/symbols/SignaturePrinter.scala b/effekt/shared/src/main/scala/effekt/symbols/SignaturePrinter.scala index f349c88c2..1a5520d1b 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/SignaturePrinter.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/SignaturePrinter.scala @@ -34,13 +34,13 @@ object SignaturePrinter extends ParenPrettyPrinter { val tpe = context.valueTypeOption(b).getOrElse { b.tpe.get } pp"val ${name.name}: ${tpe}" - case p @ ValueParam(name, t, _) => + case p @ ValueParam(name, t, isImplicit, _) => val tpe = context.valueTypeOption(p).getOrElse { t.get } - pp"${name.name}: ${tpe}" + pp"${if isImplicit then "?" else ""}${name.name}: ${tpe}" - case p @ BlockParam(name, t, _, _) => + case p @ BlockParam(name, t, _, isImplicit, _) => val tpe = context.blockTypeOption(p).getOrElse { t.get } - pp"{ ${name.name}: ${tpe} }" + pp"{ ${if isImplicit then "?" else ""}${name.name}: ${tpe} }" case b: VarBinder => val tpe = context.valueTypeOption(b).getOrElse { b.tpe.get } diff --git a/effekt/shared/src/main/scala/effekt/symbols/builtins.scala b/effekt/shared/src/main/scala/effekt/symbols/builtins.scala index 682871a9e..77c83d1a0 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/builtins.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/builtins.scala @@ -60,7 +60,7 @@ object builtins { val S: TypeParam = TypeParam(Name.local("S")) val interface: Interface = Interface(Name.local("Ref"), List(S), Nil, decl = NoSource) val get = Operation(name("get"), List(S), Nil, Nil, ValueTypeRef(S), Effects.Pure, interface, decl = NoSource) - val put = Operation(name("put"), List(S), List(ValueParam(Name.local("s"), Some(ValueTypeRef(S)), decl = NoSource)), Nil, TUnit, Effects.Pure, interface, decl = NoSource) + val put = Operation(name("put"), List(S), List(ValueParam(Name.local("s"), Some(ValueTypeRef(S)), isImplicit = false, decl = NoSource)), Nil, TUnit, Effects.Pure, interface, decl = NoSource) interface.operations = List(get, put) def apply(stateType: ValueType) = InterfaceType(interface, List(stateType)) diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala index ed5f1a041..10e5fb7ce 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala @@ -5,7 +5,7 @@ import effekt.source.{Def, DefDef, FeatureFlag, FunDef, ModuleDecl, NoSource, Re import effekt.context.Context import kiama.util.Source import effekt.context.assertions.* -import effekt.util.messages.ErrorReporter +import effekt.util.messages.{ErrorReporter, EffektMessages} /** * The symbol table contains things that can be pointed to: @@ -110,7 +110,7 @@ case class Module( sealed trait RefBinder extends BlockSymbol sealed trait Param extends TermSymbol -case class ValueParam(name: Name, tpe: Option[ValueType], decl: source.Tree) extends Param, ValueSymbol +case class ValueParam(name: Name, tpe: Option[ValueType], isImplicit: Boolean, decl: source.Tree) extends Param, ValueSymbol sealed trait TrackedParam extends Param, BlockSymbol { @@ -118,7 +118,7 @@ sealed trait TrackedParam extends Param, BlockSymbol { val capture: Capture } object TrackedParam { - case class BlockParam(name: Name, tpe: Option[BlockType], capture: Capture, decl: source.Tree) extends TrackedParam + case class BlockParam(name: Name, tpe: Option[BlockType], capture: Capture, isImplicit: Boolean, decl: source.Tree) extends TrackedParam case class ExternResource(name: Name, tpe: BlockType, capture: Capture, decl: source.Tree) extends TrackedParam } export TrackedParam.* @@ -156,6 +156,7 @@ trait Callable extends BlockSymbol { // in the future namer needs to annotate the function with the capture parameters it introduced. capt = CanonicalOrdering(effects.toList).map { tpe => CaptureParam(tpe.name) } } yield toType(ret, effects, capt) + } case class UserFunction( @@ -203,12 +204,22 @@ enum Binder extends TermSymbol { } export Binder.* +case class ImplicitContext(var values: Map[Int, source.GenerateImplicitArgs.ImplicitStencil], + var blocks: Map[Int, source.GenerateImplicitArgs.ImplicitStencil]) { + var resolved = false // will be set in namer after the values in the maps are resolved +} +object ImplicitContext { + val empty = ImplicitContext(Map.empty, Map.empty) + empty.resolved = true +} + /** * Synthetic symbol representing potentially multiple call targets * * Refined by typer. */ -case class CallTarget(symbols: List[Set[BlockSymbol]]) extends BlockSymbol { +case class CallTarget(symbols: List[Set[BlockSymbol]], + potentialImplicits: Map[BlockSymbol, ImplicitContext]) extends BlockSymbol { val name = NoName val decl = NoSource } @@ -304,7 +315,7 @@ case class Constructor(name: Name, tparams: List[TypeParam], var fields: List[Fi // TODO maybe split into Field (the symbol) and Selector (the synthetic function) case class Field(name: Name, param: ValueParam, constructor: Constructor, decl: source.Tree) extends Callable { val tparams: List[TypeParam] = constructor.tparams - val vparams = List(ValueParam(constructor.name, Some(constructor.appliedDatatype), decl = NoSource)) + val vparams = List(ValueParam(constructor.name, Some(constructor.appliedDatatype), isImplicit = false, decl = NoSource)) val bparams = List.empty[BlockParam] val returnType = param.tpe.get diff --git a/effekt/shared/src/main/scala/effekt/typer/UnboxInference.scala b/effekt/shared/src/main/scala/effekt/typer/UnboxInference.scala index 1db81ca17..d80c911fa 100644 --- a/effekt/shared/src/main/scala/effekt/typer/UnboxInference.scala +++ b/effekt/shared/src/main/scala/effekt/typer/UnboxInference.scala @@ -56,7 +56,7 @@ object UnboxInference extends Phase[NameResolved, NameResolved] { // Heuristic for a specific misunderstanding: Scala sometimes allows calling nullary fns without args, we do not sym match { case UserFunction(_, _, Nil, Nil, _, _, _, _) - | TrackedParam.BlockParam(_, Some(BlockType.FunctionType(_, _, Nil, Nil, _, _)),_, _) + | TrackedParam.BlockParam(_, Some(BlockType.FunctionType(_, _, Nil, Nil, _, _)),_, _, _) => C.info(s"Did you mean to call the function using `${sym.name.name}()`?") case ResumeParam(_) // NOTE: we don't know the type of `resume` here, so this is not _great_ advice... => C.info(s"Did you mean to resume using `resume(...)`?") @@ -106,7 +106,7 @@ object UnboxInference extends Phase[NameResolved, NameResolved] { val bargsTransformed = bargs.map(rewriteAsBlock) val hasMethods = m.definition match { - case symbols.CallTarget(syms) => syms.flatten.exists(_.isInstanceOf[symbols.Operation]) + case symbols.CallTarget(syms, _) => syms.flatten.exists(_.isInstanceOf[symbols.Operation]) case _: symbols.Operation => true case _ => false } diff --git a/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala b/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala index 5500bbf29..cf4929fc5 100644 --- a/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala +++ b/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala @@ -96,6 +96,24 @@ trait ColoredMessaging extends EffektMessaging { } mainMessage + "\n\n" + explanations.mkString("\n\n") + "\n" + case ImplicitInstantiationError(err, tpe, range) => + def formatIndex(i: Int): String = i match { + case 0 => "1st" + case 1 => "2nd" + case 2 => "3rd" + case n => s"${n + 1}th" + } + val title = bold("Cannot instantiate implicit argument.\n") + val prettyIndex = formatIndex(err.index) + val prettyTpe = TypePrinter.show(tpe) + val mainMessage = underlined(s"Cannot instantiate ${prettyIndex} ${err.kind} ${err.name} of type ${prettyTpe}.") + val info = err.explanation match { + case Some(e) => indent(e) + "\n" + case None => "" + } + val nestedErrors = indent(err.msgs.map { msg => formatContent(msg) }.mkString("\n")) + + title + mainMessage + "\n" + info + nestedErrors + "\n" } def fullname(n: Name): String = n match { diff --git a/effekt/shared/src/main/scala/effekt/util/DocumentationGenerator.scala b/effekt/shared/src/main/scala/effekt/util/DocumentationGenerator.scala index 143437611..f7f0610e6 100644 --- a/effekt/shared/src/main/scala/effekt/util/DocumentationGenerator.scala +++ b/effekt/shared/src/main/scala/effekt/util/DocumentationGenerator.scala @@ -11,6 +11,7 @@ trait DocumentationGenerator { // A recursive structure that resembles JSON enum DocValue { case DocNumber(value: Int) + case DocBool(value: Boolean) case DocString(value: String) case DocArray(value: List[DocValue]) case DocObject(value: Documentation) @@ -105,21 +106,23 @@ trait DocumentationGenerator { arr(list.map(generate)) def generateVparams(list: List[ValueParam])(using C: Context): DocValue = arr(list.map { - case ValueParam(id, tpe, span) => + case ValueParam(id, tpe, isImplicit, span) => obj(HashMap( "kind" -> str("ValueParam"), "id" -> generate(id), "tpe" -> tpe.map(generate).getOrElse(empty), + "isImplicit" -> DocBool(isImplicit), "span" -> generate(span), )) }) def generateBparams(list: List[BlockParam])(using C: Context): DocValue = arr(list.map { - case BlockParam(id, tpe, span) => + case BlockParam(id, tpe, isImplicit, span) => obj(HashMap( "kind" -> str("BlockParam"), "id" -> generate(id), "tpe" -> tpe.map(generate).getOrElse(empty), + "isImplicit" -> DocBool(isImplicit), "span" -> generate(span), )) }) @@ -322,6 +325,7 @@ case class JSONDocumentationGenerator(ast: ModuleDecl, name: String = "")(using case DocValue.DocString(str) => s"\"${str}\"" case DocValue.DocObject(obj) => toJSON(obj) case DocValue.DocArray(arr) => toJSON(arr) + case DocValue.DocBool(b) => b.toString } def toJSON(doc: Documentation): String = { diff --git a/effekt/shared/src/main/scala/effekt/util/Messages.scala b/effekt/shared/src/main/scala/effekt/util/Messages.scala index 6f716843d..6ab47737b 100644 --- a/effekt/shared/src/main/scala/effekt/util/Messages.scala +++ b/effekt/shared/src/main/scala/effekt/util/Messages.scala @@ -13,6 +13,7 @@ object messages { case class ParseError(message: String, range: Option[Range], severity: Severity) extends EffektError case class AmbiguousOverloadError(matches: List[(symbols.BlockSymbol, symbols.FunctionType)], range: Option[Range]) extends EffektError { val severity = Error } case class FailedOverloadError(failedAttempts: List[(symbols.BlockSymbol, symbols.FunctionType, EffektMessages)], range: Option[Range]) extends EffektError { val severity = Error } + case class ImplicitInstantiationError(stencil: source.GenerateImplicitArgs.Error, tpe: symbols.Type, range: Option[Range]) extends EffektError { val severity = Error } case class PlainTextError(content: String, range: Option[Range], severity: Severity) extends EffektError case class StructuredError(content: StructuredMessage, range: Option[Range], severity: Severity) extends EffektError diff --git a/effekt/shared/src/main/scala/effekt/util/package.scala b/effekt/shared/src/main/scala/effekt/util/package.scala index a9915c061..1526aaeb6 100644 --- a/effekt/shared/src/main/scala/effekt/util/package.scala +++ b/effekt/shared/src/main/scala/effekt/util/package.scala @@ -18,4 +18,10 @@ extension(ch: Char) def escape: String = ch match { case '\\' => "\\\\" case ch if ch.toInt >= 32 && ch.toInt <= 126 => String.valueOf(ch) case ch => "\\u%04x".format(ch.toInt) +} + +enum RequirementLevel { + case Required + case Optional + case Forbidden } \ No newline at end of file diff --git a/examples/neg/name-based-implicits/boxed-captures.check b/examples/neg/name-based-implicits/boxed-captures.check new file mode 100644 index 000000000..39d2b8ee5 --- /dev/null +++ b/examples/neg/name-based-implicits/boxed-captures.check @@ -0,0 +1,5 @@ +[error] Cannot instantiate implicit argument. +Cannot instantiate 1st value argument foo of type () => String at {}. + An implicit argument of a boxed type will be instantiated by boxing the block + that an implicit block argument of the same name would be instantiated to. + Not allowed {io} \ No newline at end of file diff --git a/examples/neg/name-based-implicits/boxed-captures.effekt b/examples/neg/name-based-implicits/boxed-captures.effekt new file mode 100644 index 000000000..a8da0c038 --- /dev/null +++ b/examples/neg/name-based-implicits/boxed-captures.effekt @@ -0,0 +1,11 @@ + +def foo(): String = { + println("Foo") + "Foo" +} + +def requirePureFoo(?foo: () => String at {}): Unit = println(foo()) + +def main() = { + requirePureFoo() +} \ No newline at end of file diff --git a/examples/neg/name-based-implicits/higher-order.effekt b/examples/neg/name-based-implicits/higher-order.effekt new file mode 100644 index 000000000..435fa2859 --- /dev/null +++ b/examples/neg/name-based-implicits/higher-order.effekt @@ -0,0 +1,5 @@ +def hof{ body: Int => Int }: Int = body(0) + +def main() = hof { (?x: Int) => // ERROR Implicit parameter + x + 1 +} \ No newline at end of file diff --git a/examples/neg/name-based-implicits/no-implicit.check b/examples/neg/name-based-implicits/no-implicit.check new file mode 100644 index 000000000..617e1c2f8 --- /dev/null +++ b/examples/neg/name-based-implicits/no-implicit.check @@ -0,0 +1,12 @@ +[error] Cannot typecheck call. +There are multiple overloads, which all fail to check: + +Possible overload: no-implicit::foo of type String => Unit + Cannot instantiate implicit argument. + Cannot instantiate 1st value argument barbaz of type String. + Could not resolve term barbaz + +Possible overload: no-implicit::foo of type {() => Unit} => Unit + Cannot instantiate implicit argument. + Cannot instantiate 1st block argument foobaz of type () => Unit. + Wrong number of arguments to foobaz: expected 1 value argument, but got 0 value arguments \ No newline at end of file diff --git a/examples/neg/name-based-implicits/no-implicit.effekt b/examples/neg/name-based-implicits/no-implicit.effekt new file mode 100644 index 000000000..2ff479110 --- /dev/null +++ b/examples/neg/name-based-implicits/no-implicit.effekt @@ -0,0 +1,9 @@ +def foo(?barbaz: String): Unit = () + +def foo{ ?foobaz: => Unit }: Unit = foobaz() + +def foobaz(x: String): Unit = () + +def main() = { + foo() +} \ No newline at end of file diff --git a/examples/neg/name-based-implicits/order.effekt b/examples/neg/name-based-implicits/order.effekt new file mode 100644 index 000000000..1e811b894 --- /dev/null +++ b/examples/neg/name-based-implicits/order.effekt @@ -0,0 +1,6 @@ +def foo(a: Int, ?b: Int, c: Int): Unit = { // ERROR implicit + + val x = box { (?i: Int) => () } // ERROR implicit +} + +def main() = () \ No newline at end of file diff --git a/examples/neg/name-based-implicits/type-error-call-id.check b/examples/neg/name-based-implicits/type-error-call-id.check new file mode 100644 index 000000000..23e6250e5 --- /dev/null +++ b/examples/neg/name-based-implicits/type-error-call-id.check @@ -0,0 +1,7 @@ +[error] examples/neg/name-based-implicits/type-error-call-id.effekt:4:19: Cannot instantiate implicit argument. +Cannot instantiate 1st value argument callId of type String. + Implicit callId will generate a unique Int for each call to this function in the source code. + Expected String but got Int. + + val y: String = foo() + ^^^^^ \ No newline at end of file diff --git a/examples/neg/name-based-implicits/type-error-call-id.effekt b/examples/neg/name-based-implicits/type-error-call-id.effekt new file mode 100644 index 000000000..07f2bc31d --- /dev/null +++ b/examples/neg/name-based-implicits/type-error-call-id.effekt @@ -0,0 +1,6 @@ +def foo(?callId: String): String = callId + +def main() = { + val y: String = foo() + println(y) +} \ No newline at end of file diff --git a/examples/neg/name-based-implicits/type-error-no-overload.check b/examples/neg/name-based-implicits/type-error-no-overload.check new file mode 100644 index 000000000..0e8731a43 --- /dev/null +++ b/examples/neg/name-based-implicits/type-error-no-overload.check @@ -0,0 +1,3 @@ +[error] Cannot instantiate implicit argument. +Cannot instantiate 1st block argument foo of type () => Int. + Expected Int but got Bool. \ No newline at end of file diff --git a/examples/neg/name-based-implicits/type-error-no-overload.effekt b/examples/neg/name-based-implicits/type-error-no-overload.effekt new file mode 100644 index 000000000..1b4d98111 --- /dev/null +++ b/examples/neg/name-based-implicits/type-error-no-overload.effekt @@ -0,0 +1,8 @@ +def foo(): Bool = false + +def baz{ ?foo: => Int }: String = "1" + +def main() = { + val y: String = baz() + println(y) +} \ No newline at end of file diff --git a/examples/neg/name-based-implicits/type-error-overload.check b/examples/neg/name-based-implicits/type-error-overload.check new file mode 100644 index 000000000..c47d391d7 --- /dev/null +++ b/examples/neg/name-based-implicits/type-error-overload.check @@ -0,0 +1,26 @@ +[error] Cannot typecheck call. +There are multiple overloads, which all fail to check: + +Possible overload: type-error-overload::baz of type {() => Int} => String + Cannot instantiate implicit argument. + Cannot instantiate 1st block argument foo of type () => Int. + Cannot typecheck call. + There are multiple overloads, which all fail to check: + + Possible overload: type-error-overload::foo of type () => Bool + Expected Int but got Bool. + + Possible overload: type-error-overload::foo of type () => Double + Expected Int but got Double. + +Possible overload: type-error-overload::baz of type {() => List[Bool]} => String + Cannot instantiate implicit argument. + Cannot instantiate 1st block argument foo of type () => List[Bool]. + Cannot typecheck call. + There are multiple overloads, which all fail to check: + + Possible overload: type-error-overload::foo of type () => Bool + Expected List[Bool] but got Bool. + + Possible overload: type-error-overload::foo of type () => Double + Expected List[Bool] but got Double. \ No newline at end of file diff --git a/examples/neg/name-based-implicits/type-error-overload.effekt b/examples/neg/name-based-implicits/type-error-overload.effekt new file mode 100644 index 000000000..0844d8ef2 --- /dev/null +++ b/examples/neg/name-based-implicits/type-error-overload.effekt @@ -0,0 +1,10 @@ +def foo(): Bool = false +def foo(): Double = 0.0 + +def baz{ ?foo: => Int }: String = "1" +def baz{ ?foo: => List[Bool] }: String = "2" + +def main() = { + val y: String = baz() + println(y) +} \ No newline at end of file diff --git a/examples/neg/name-based-implicits/type-error.check b/examples/neg/name-based-implicits/type-error.check new file mode 100644 index 000000000..cc4b3e51c --- /dev/null +++ b/examples/neg/name-based-implicits/type-error.check @@ -0,0 +1,12 @@ +[error] Cannot typecheck call. +There are multiple overloads, which all fail to check: + +Possible overload: type-error::baz of type {() => Int} => String + Cannot instantiate implicit argument. + Cannot instantiate 1st block argument foo of type () => Int. + Expected Int but got Bool. + +Possible overload: type-error::baz of type {() => List[Bool]} => String + Cannot instantiate implicit argument. + Cannot instantiate 1st block argument foo of type () => List[Bool]. + Expected List[Bool] but got Bool. \ No newline at end of file diff --git a/examples/neg/name-based-implicits/type-error.effekt b/examples/neg/name-based-implicits/type-error.effekt new file mode 100644 index 000000000..ce7deacae --- /dev/null +++ b/examples/neg/name-based-implicits/type-error.effekt @@ -0,0 +1,9 @@ +def foo(): Bool = false + +def baz{ ?foo: => Int }: String = "1" +def baz{ ?foo: => List[Bool] }: String = "2" + +def main() = { + val y: String = baz() + println(y) +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/boxed.check b/examples/pos/name-based-implicits/boxed.check new file mode 100644 index 000000000..502106d31 --- /dev/null +++ b/examples/pos/name-based-implicits/boxed.check @@ -0,0 +1,4 @@ +Equal() +Less() +Less() +Greater() \ No newline at end of file diff --git a/examples/pos/name-based-implicits/boxed.effekt b/examples/pos/name-based-implicits/boxed.effekt new file mode 100644 index 000000000..daf9bb6ba --- /dev/null +++ b/examples/pos/name-based-implicits/boxed.effekt @@ -0,0 +1,21 @@ +def myCompare(x: Int, y: Int): Ordering = { + compareInt(x, y) +} +def myCompare[A,B](x: (A,B), y: (A,B)){ ?myCompare: (A,A) => Ordering }{ ?myCompare: (B,B) => Ordering }: Ordering = { + val ((x1, x2), (y1, y2)) = (x, y) + myCompare(x1,y1) match { + case Equal() => myCompare(x2, y2) + case o => o + } +} + +def runBoxedCompare[A](x: A, y: A, ?myCompare: (A, A) => Ordering at {}): Unit = { + println(myCompare(x, y).show) +} + +def main() = { + runBoxedCompare((1,2), (1,2)) + runBoxedCompare((1,2), (2,3)) + runBoxedCompare(1, 2) + runBoxedCompare(((1,1),2), ((0,2),3)) +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/callid.check b/examples/pos/name-based-implicits/callid.check new file mode 100644 index 000000000..02e4a84d6 --- /dev/null +++ b/examples/pos/name-based-implicits/callid.check @@ -0,0 +1 @@ +false \ No newline at end of file diff --git a/examples/pos/name-based-implicits/callid.effekt b/examples/pos/name-based-implicits/callid.effekt new file mode 100644 index 000000000..424099379 --- /dev/null +++ b/examples/pos/name-based-implicits/callid.effekt @@ -0,0 +1,5 @@ +def here(?callId: Int): Int = callId + +def main() = { + println(here() == here()) +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/comparer.check b/examples/pos/name-based-implicits/comparer.check new file mode 100644 index 000000000..1fc1f84c2 --- /dev/null +++ b/examples/pos/name-based-implicits/comparer.check @@ -0,0 +1 @@ +Less() \ No newline at end of file diff --git a/examples/pos/name-based-implicits/comparer.effekt b/examples/pos/name-based-implicits/comparer.effekt new file mode 100644 index 000000000..0e279706b --- /dev/null +++ b/examples/pos/name-based-implicits/comparer.effekt @@ -0,0 +1,23 @@ + +def myComparer(): (Int, Int) => Ordering at {} = { + box { (x, y) => compareInt(x, y) } +} +def myComparer[A](){ ?myComparer: => ((A, A) => Ordering at {})}: (List[A], List[A]) => Ordering at {} = { + val cmp: (A,A) => Ordering at {} = myComparer() + box { + case Nil(), Nil() => Equal() + case Cons(_, _), Nil() => Greater() + case Nil(), Cons(_, _) => Less() + case Cons(lh, lt), Cons(rh, rt) => + cmp(lh, rh) match { + case Equal() => myComparer(){ => cmp }(lt, rt) + case o => o + } + } +} + +def main() = { + val cmp: (List[Int], List[Int]) => Ordering at {} = myComparer() + val x: Ordering = ((unbox cmp)([1,2,3], [2,3,4])) + println(x) +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/deep-recursive.check b/examples/pos/name-based-implicits/deep-recursive.check new file mode 100644 index 000000000..a0aba9318 --- /dev/null +++ b/examples/pos/name-based-implicits/deep-recursive.check @@ -0,0 +1 @@ +OK \ No newline at end of file diff --git a/examples/pos/name-based-implicits/deep-recursive.effekt b/examples/pos/name-based-implicits/deep-recursive.effekt new file mode 100644 index 000000000..6f07de579 --- /dev/null +++ b/examples/pos/name-based-implicits/deep-recursive.effekt @@ -0,0 +1,10 @@ + +record F[X](x: X) + +def foo(): Unit = () +def foo[X](){ ?foo: => X }: F[X] = F[X](foo()) + +def main() = { + val x: F[F[F[F[F[F[F[F[F[F[F[F[F[F[F[Unit]]]]]]]]]]]]]]] = foo() + println("OK") +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/default.check b/examples/pos/name-based-implicits/default.check new file mode 100644 index 000000000..c436174c2 --- /dev/null +++ b/examples/pos/name-based-implicits/default.check @@ -0,0 +1 @@ +Tuple2(0, Tuple2(Nil(), Tuple2(false, ))) \ No newline at end of file diff --git a/examples/pos/name-based-implicits/default.effekt b/examples/pos/name-based-implicits/default.effekt new file mode 100644 index 000000000..5c40334a9 --- /dev/null +++ b/examples/pos/name-based-implicits/default.effekt @@ -0,0 +1,10 @@ +def default(): Int = 0 +def default(): Bool = false +def default(): String = "" +def default[A](): List[A] = Nil() +def default[A,B](){ ?default: => A }{ ?default: => B }: (A, B) = (default(), default()) + +def main() = { + val x: (Int, (List[String], (Bool, String))) = default() + println(x.show) +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/implicit-interfaces.check b/examples/pos/name-based-implicits/implicit-interfaces.check new file mode 100644 index 000000000..f70d7bba4 --- /dev/null +++ b/examples/pos/name-based-implicits/implicit-interfaces.check @@ -0,0 +1 @@ +42 \ No newline at end of file diff --git a/examples/pos/name-based-implicits/implicit-interfaces.effekt b/examples/pos/name-based-implicits/implicit-interfaces.effekt new file mode 100644 index 000000000..62965f98a --- /dev/null +++ b/examples/pos/name-based-implicits/implicit-interfaces.effekt @@ -0,0 +1,12 @@ +interface Foo[A] { + def foo(): A +} + +def bar[A](){ ?instance: Foo[A] }: A = instance.foo() + +def main() = { + def instance = new Foo[Int] { + def foo() = 42 + } + println(bar().show) +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/lensy.check b/examples/pos/name-based-implicits/lensy.check new file mode 100644 index 000000000..bdf8bf298 --- /dev/null +++ b/examples/pos/name-based-implicits/lensy.check @@ -0,0 +1 @@ +Cons(2, Cons(3, Cons(4, Nil()))) \ No newline at end of file diff --git a/examples/pos/name-based-implicits/lensy.effekt b/examples/pos/name-based-implicits/lensy.effekt new file mode 100644 index 000000000..0647fdb4d --- /dev/null +++ b/examples/pos/name-based-implicits/lensy.effekt @@ -0,0 +1,9 @@ +def lens(v: Int){ fn: Int => Int }: Int = fn(v) +def lens[A](x: List[A]){ fn: A => A }{ ?lens: (A){ A => A } => A }: List[A] = + x.map { e => lens(e){fn} } +def lens[A](x: List[A]){ fn: List[A] => List[A] }: List[A] = + fn(x) + +def main() = { + println(lens([1,2,3]){ x => x + 1}.show) +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/my-show.check b/examples/pos/name-based-implicits/my-show.check new file mode 100644 index 000000000..e2cb99fa4 --- /dev/null +++ b/examples/pos/name-based-implicits/my-show.check @@ -0,0 +1,3 @@ +[[12,],][[12,],] +main +bar \ No newline at end of file diff --git a/examples/pos/name-based-implicits/my-show.effekt b/examples/pos/name-based-implicits/my-show.effekt new file mode 100644 index 000000000..1f7bf8d60 --- /dev/null +++ b/examples/pos/name-based-implicits/my-show.effekt @@ -0,0 +1,29 @@ + +def myShow(i: Int) = i.show + +def myShow[A](l: List[A]){ ?myShow: A => String }: String = { + def go(l: List[A]): String = + l match { + case Nil() => "]" + case Cons(hd, tl) => myShow(hd) ++ "," ++ go(tl) + } + "[" ++ go(l) +} + +def myShowTwo[A](a: A){ ?myShow: A => String }: String = + myShow(a) ++ myShow(a) + +def myCallee(? callee: String): Unit = { + println(callee) +} + +def main() = { + val callee = "main" + def bar() = { + val callee = "bar" + myCallee() + } + println(myShowTwo([[12]])) + myCallee() + bar() +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/operations.check b/examples/pos/name-based-implicits/operations.check new file mode 100644 index 000000000..532656090 --- /dev/null +++ b/examples/pos/name-based-implicits/operations.check @@ -0,0 +1,3 @@ +effect from main +object from main +effect from bar \ No newline at end of file diff --git a/examples/pos/name-based-implicits/operations.effekt b/examples/pos/name-based-implicits/operations.effekt new file mode 100644 index 000000000..0b2c5f115 --- /dev/null +++ b/examples/pos/name-based-implicits/operations.effekt @@ -0,0 +1,26 @@ +interface Foo { + def foo(?ctx: String): Unit +} + +def bar(): Unit / Foo = { + val ctx = "from bar" + do foo() +} + + +def main() = { + def fif = new Foo { + def foo(ctx: String) = println("object " ++ ctx) + } + val ctx = "from main" + try { + do foo() + fif.foo() + bar() + } with Foo { + def foo(ctx: String) = { + println("effect " ++ ctx) + resume(()) + } + } +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/parametrized-type-tags.check b/examples/pos/name-based-implicits/parametrized-type-tags.check new file mode 100644 index 000000000..d3fd265f8 --- /dev/null +++ b/examples/pos/name-based-implicits/parametrized-type-tags.check @@ -0,0 +1 @@ +List[List[Int]] \ No newline at end of file diff --git a/examples/pos/name-based-implicits/parametrized-type-tags.effekt b/examples/pos/name-based-implicits/parametrized-type-tags.effekt new file mode 100644 index 000000000..69ad98188 --- /dev/null +++ b/examples/pos/name-based-implicits/parametrized-type-tags.effekt @@ -0,0 +1,20 @@ +type TypeTag[A] { + Int() + String() + List[E](elements: TypeTag[E]) +} + +def typeTag(): TypeTag[Int] = Int() +def typeTag(): TypeTag[String] = String() +def typeTag[X](){ ?typeTag: => TypeTag[X] }: TypeTag[List[X]] = List[List[X], X](typeTag()) + +def showType[X](t: TypeTag[X]): String = t match { + case Int() => "Int" + case String() => "String" + case List(els) => s"List[${showType(els)}]" +} + +def main() = { + val t: TypeTag[List[List[Int]]] = typeTag() + println(showType(t)) +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/source-positions.check b/examples/pos/name-based-implicits/source-positions.check new file mode 100644 index 000000000..01aff2b26 --- /dev/null +++ b/examples/pos/name-based-implicits/source-positions.check @@ -0,0 +1 @@ +examples/pos/name-based-implicits/source-positions.effekt:10:3-10:8 \ No newline at end of file diff --git a/examples/pos/name-based-implicits/source-positions.effekt b/examples/pos/name-based-implicits/source-positions.effekt new file mode 100644 index 000000000..14fe2970f --- /dev/null +++ b/examples/pos/name-based-implicits/source-positions.effekt @@ -0,0 +1,11 @@ +def SourcePosition(file: String, sl: Int, sc: Int, el: Int, ec: Int): String = { + s"${file}:${sl.show}:${sc.show}-${el.show}:${ec.show}" +} + +def foo(?sourcePosition: String): Unit = { + println(sourcePosition.show) +} + +def main() = { + foo() +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/type-tags.check b/examples/pos/name-based-implicits/type-tags.check new file mode 100644 index 000000000..1858f774d --- /dev/null +++ b/examples/pos/name-based-implicits/type-tags.check @@ -0,0 +1 @@ +List(List(Bool())) \ No newline at end of file diff --git a/examples/pos/name-based-implicits/type-tags.effekt b/examples/pos/name-based-implicits/type-tags.effekt new file mode 100644 index 000000000..0ab3781cb --- /dev/null +++ b/examples/pos/name-based-implicits/type-tags.effekt @@ -0,0 +1,21 @@ +type TypeTag { + Int() + Bool() + String() + List(of: TypeTag) + Bottom() +} + +def typeTag(x: Int): TypeTag = Int() +def typeTag(b: Bool): TypeTag = Bool() +def typeTag(s: String): TypeTag = String() +def typeTag[A](v: List[A]){ ?typeTag: A => TypeTag }: TypeTag = + v match { + case Cons(hd, _) => List(typeTag(hd)) + case _ => List(Bottom()) + } + +def main() = { + val x = typeTag([[true]]) + println(x.show) +} \ No newline at end of file diff --git a/examples/pos/name-based-implicits/typelevel.check b/examples/pos/name-based-implicits/typelevel.check new file mode 100644 index 000000000..e440e5c84 --- /dev/null +++ b/examples/pos/name-based-implicits/typelevel.check @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/examples/pos/name-based-implicits/typelevel.effekt b/examples/pos/name-based-implicits/typelevel.effekt new file mode 100644 index 000000000..b07465d24 --- /dev/null +++ b/examples/pos/name-based-implicits/typelevel.effekt @@ -0,0 +1,15 @@ +record Zero() +record Succ[A](a: A) + +def toInt(a: Zero): Int = 0 +def toInt[A](a: Succ[A]){?toInt: A => Int}: Int = toInt(a.a) + 1 + +def add[A](x: A, y: Zero): A = x +def add[A](y: Zero, x: Succ[A]): Succ[A] = x +def add[A,B,C](x: Succ[A], y: Succ[B]){?add: (A,B) => C}: Succ[Succ[C]] = + Succ(Succ(add(x.a, y.a))) + +def main() = { + val x = add(Succ(Succ(Zero())), Succ(Zero())) + println(toInt(x).show) +} \ No newline at end of file diff --git a/examples/tour/name-based-implicits.effekt.md b/examples/tour/name-based-implicits.effekt.md new file mode 100644 index 000000000..abc3f1c05 --- /dev/null +++ b/examples/tour/name-based-implicits.effekt.md @@ -0,0 +1,138 @@ +--- +title: Name-based implicits +permalink: tour/name-based-implicits +--- + +# Name-Based Implicit Parameters + +Some functions need additional parameters that can be easily inferred based on +their name and type. For example, a sort function might want to get +a order as its argument: + +```effekt +def mySort1[A](x: List[A]){ compare: (A, A) => Ordering }: List[A] = + // we use the standard library sort here, which gets a less than or equal + x.sortBy{ (x, y) => + compare(x,y) match { + case Greater() => false + case _ => true + } + } + +def compare(x: Int, y: Int): Ordering = compareInt(x, y) +``` +```effekt:repl +mySort1([3,1,2]){ (x, y) => compare(x, y) } +``` + +However, often there will be a function `compare` in scope, and we could +simplify the call if this was used automatically. In Effekt, we can +instruct the compiler to generate the block parameter like above by +putting a `?` in front of the parameter: +``` +def mySort2[A](x: List[A]){ ?compare: (A, A) => Ordering }: List[A] = + // we use the standard library sort here, which gets a less than or equal + x.sortBy{ (x, y) => + compare(x,y) match { + case Greater() => false + case _ => true + } + } +``` + +Then we can just call it as: +```effekt:repl +mySort2([3,1,2]) +``` + +This generates something like the following, which we are still allowed to write explicitly, too: +```effekt:repl +mySort2([3,1,2]){ (x, y) => compare(x, y) } +``` + +Name-based implicit parameters also will be passed +to the functions that get called implicitly like this, so if we define +```effekt +def compare[A](x: Option[A], y: Option[A]){ ?compare: (A,A) => Ordering }: Ordering = + (x, y) match { + case (None(), None()) => Equal() + case (Some(_), None()) => Greater() + case (None(), Some(_)) => Less() + case (Some(x), Some(y)) => compare(x, y) + } +``` + +we can now also call `mySort2` with lists of options (or options of options, ...): +```effekt:repl +mySort2([Some(12), None(), Some(2)]) +``` + +This will expand to code equivalent to: +```effekt:repl +mySort2([Some(12), None(), Some(2)]){ (x,y) => compare(x, y){ (u, v) => compare(u, v) } } +``` + +## Values + +We can also pass value parameters implicitly when they are marked with a `?`: + +```effekt +def foo(?context: String): Unit = + println(s"Called from ${context}") + +def example1() = { + val context = "example1" + foo() +} + +def example2(context: String) = { + example1() + foo() +} +``` +```effekt:repl +example2("ex2") +``` + +That is, implicit value parameters use any value binding of the correct name +at the call site. +For some special cases, they work differently, though. We will discuss those now. + +### Boxing + +If the value parameter has a boxed type (something like `A => B at {}`), +this is expanded to `box {...}` and expanded like block parameters, so +the following works: +``` +def mySort3[A](x: List[A], ?compare: (A, A) => Ordering at {}): List[A] = + mySort2(x){ (x,y) => (unbox compare)(x, y) } +``` +```effekt:repl +mySort3([12,3,42]) +``` + +### Source Positions + +When a function takes an implicit parameter with the name `sourcePosition`, +it will get the result of calling `SourcePosition` with the filename, +start line, start column, end line, and end column of the source position of the call: +``` +def printSourcePos(?sourcePosition: SourcePosition): Unit = + println(sourcePosition.show) +``` +```effekt:repl +printSourcePos() +``` + +### Call IDs + +Sometimes we would use source positions just to get a unique id for each +call in the source code. Instead, we can take a parameter `callId: Int` +which will get a unique integer ID directly: + +``` +def here(?callId: Int): Int = callId +``` +```effekt:repl +here() == here() +``` \ No newline at end of file diff --git a/libraries/common/effekt.effekt b/libraries/common/effekt.effekt index 5871f9d9c..a09c16515 100644 --- a/libraries/common/effekt.effekt +++ b/libraries/common/effekt.effekt @@ -842,4 +842,10 @@ effect write(s: String): Unit effect splice[A](x: A): Unit +// Source positions +// ================ +// +// When adding an implicit value argument ?sourcePosition of this type, will have the information +// of the call site unless passed explicitly. +record SourcePosition(file: String, startLine: Int, startColumn: Int, endLine: Int, endColumn: Int) \ No newline at end of file