From a0d927fb49c5e914db0c8873e1fa229e6804373b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 15:57:21 +0200 Subject: [PATCH 01/46] Initial draft --- .../shared/src/main/scala/effekt/Lexer.scala | 2 + .../shared/src/main/scala/effekt/Namer.scala | 75 +++++++++-- .../shared/src/main/scala/effekt/Parser.scala | 27 ++-- .../shared/src/main/scala/effekt/Typer.scala | 117 +++++++++++------- .../scala/effekt/context/Annotations.scala | 11 ++ .../main/scala/effekt/context/Context.scala | 27 ++++ .../effekt/source/ExplicitCapabilities.scala | 8 +- .../src/main/scala/effekt/source/Tree.scala | 11 +- .../src/main/scala/effekt/symbols/Scope.scala | 51 ++++++++ .../effekt/symbols/SignaturePrinter.scala | 8 +- .../main/scala/effekt/symbols/builtins.scala | 2 +- .../main/scala/effekt/symbols/symbols.scala | 19 ++- .../scala/effekt/typer/UnboxInference.scala | 4 +- .../effekt/util/DocumentationGenerator.scala | 9 +- .../src/main/scala/effekt/util/Messages.scala | 2 +- 15 files changed, 289 insertions(+), 84 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Lexer.scala b/effekt/shared/src/main/scala/effekt/Lexer.scala index 73ba5828e1..11848f1cff 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 fbdbc3bca7..d04abe6985 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -11,6 +11,7 @@ import effekt.source.{Def, Id, IdDef, IdRef, Many, MatchGuard, ModuleDecl, Term, import effekt.symbols.* import effekt.util.messages.ErrorMessageReifier import effekt.symbols.scopes.* +import effekt.context.Try import scala.annotation.tailrec import scala.collection.mutable @@ -445,6 +446,40 @@ object Namer extends Phase[Parsed, NameResolved] { resolve(a.value) } + 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. + val tVimpls = vimpls.flatMap { case k -> v => + val r = Try { + resolve(v) + }; + if (r.isRight) { + Some(k -> v) + } else { + None + } + } + val tBimpls = bimpls.flatMap { case k -> b => + val r = Try { + resolve(b) + } + if (r.isRight) { + Some(k -> b) + } else { + None + } + } + c.values = tVimpls + c.blocks = tBimpls + case _ => () + } + case _ => () + } + } + def resolve(t: source.Term)(using Context): Unit = Context.focusing(t) { case source.Literal(value, tpe, _) => () @@ -491,7 +526,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) @@ -551,19 +586,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 +703,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 +756,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, _) => @@ -781,6 +818,8 @@ object Namer extends Phase[Parsed, NameResolved] { // TODO reconsider reusing the same set for terms and types... case source.BoxedType(tpe, capt, _) => BoxedType(resolveBlockType(tpe), resolve(capt)) + case source.ReifiedType(tpe: ValueType) => + tpe case other => Context.error(pretty"Expected value type, but got ${describeType(other)}.") other match { @@ -807,6 +846,7 @@ object Namer extends Phase[Parsed, NameResolved] { case t: source.FunctionType => resolve(t) case t: source.BlockTypeTree => t.eff case t: source.TypeRef => resolveBlockRef(t) + case source.ReifiedType(tpe: BlockType) => tpe case other => Context.error(pretty"Expected block type, but got ${describeType(other)}.") other match @@ -926,9 +966,10 @@ object Namer extends Phase[Parsed, NameResolved] { case _: source.FunctionType => s"a second-class function type ${t.sourceOf}" case _: source.Effectful => s"a type-and-effect annotation ${t.sourceOf}" - // THESE TWO SHOULD NEVER BE USER-VISIBLE! + // THESE THREE SHOULD NEVER BE USER-VISIBLE! case source.ValueTypeTree(tpe, _) => s"a value type tree ${tpe}" case source.BlockTypeTree(eff, _) => s"a block type tree ${eff}" + case source.ReifiedType(tpe) => "a type in generated code" } private def prettySourceEffectSet(effects: Set[source.TypeRef])(using Context) = @@ -1081,7 +1122,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, scope.lookupPotentialImplicits(syms3))) + true + } else { false } } private[namer] def resolveOverloadedFunction(id: IdRef): Boolean = at(id) { @@ -1092,7 +1137,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, scope.lookupPotentialImplicits(syms4))) + true + } else { false } } /** @@ -1116,7 +1165,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, scope.lookupPotentialImplicits(blocks))) } } @@ -1170,7 +1219,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, scope.lookupPotentialImplicits(bsyms))) } /** @@ -1184,7 +1234,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, scope.lookupPotentialImplicits(bsyms))) } /** diff --git a/effekt/shared/src/main/scala/effekt/Parser.scala b/effekt/shared/src/main/scala/effekt/Parser.scala index 72bda07493..1bf2f42add 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 f9c345af25..059f9f2926 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, Map.empty, Map.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,11 +1207,29 @@ 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) + val (cvimpls, cbimpls) = findImplicitArgs(op, impls.getOrElse(op, ImplicitContext.empty)) + checkCallTo(call, op.name.name, op.vparams.map { p => p.name.name }, funTpe, synthTargs, vargs, bargs, expected, cvimpls, cbimpls) } resolveOverload(id, List(successes), errors) } + def findImplicitArgs(r: BlockSymbol, + impls: ImplicitContext): (Map[Int, source.ValueArg], Map[Int, source.Term]) = { + r match { + case c: Callable => + (c.vparams.zipWithIndex.flatMap { + case (v, i) if v.isImplicit => + impls.values.get(v).map(i -> _) + case _ => None + }.toMap, c.bparams.zipWithIndex.flatMap { + case (b, i) if b.isImplicit => + impls.blocks.get(b).map(i -> _) + case _ => None + }.toMap) + case _ => (Map.empty, Map.empty) + } + } + /** * Attempts to check a potentially overladed call, not reporting any errors but returning them instead. * @@ -1228,11 +1246,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 +1278,8 @@ 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 (cvimpls, cbimpls) = findImplicitArgs(receiver, impls.getOrElse(receiver, ImplicitContext.empty)) + val Result(tpe, effs) = checkCallTo(call, receiver.name.name, vpnames, funTpe, targs, vargs, bargs, expected, cvimpls, cbimpls) // This is different, compared to method calls: usingCapture(capture) Result(tpe, effs) @@ -1293,7 +1312,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 +1342,15 @@ 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(by: PartialFunction[(B, Int), A]): Aligned[A,B] = { + val (impl, remaining) = self.missing.zipWithIndex.partition(by.isDefinedAt) + Aligned(self.matched ++ impl.map{ (a, i) => (by(a, i), a) }, self.extra, remaining.map{ (a, _) => a }) + } 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] = @@ -1451,12 +1476,30 @@ object Typer extends Phase[NameResolved, Typechecked] { targs: List[ValueType], vargs: List[source.ValueArg], bargs: List[source.Term], - expected: Option[ValueType] + expected: Option[ValueType], + potentialValueImplicits: Map[Int, source.ValueArg], + potentialBlockImplicits: Map[Int, Term] )(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 implicitVargs: mutable.ListBuffer[source.ValueArg] = mutable.ListBuffer.empty + val implicitBargs: mutable.ListBuffer[source.Term] = mutable.ListBuffer.empty + val avargs = Aligned(vargs, funTpe.vparams).fillImplicit { + case (e, i) if potentialValueImplicits.contains(i) => + val v = potentialValueImplicits(i) + implicitVargs.append(v) + v + } + val abargs = Aligned(bargs, funTpe.bparams).fillImplicit { + case (bb, i) if potentialBlockImplicits.contains(i) => + val b = (new Tree.Rewrite{}).rewrite(potentialBlockImplicits(i)) // deep copy the tree // TODO prevent infinite recursion due to this // TODO rename params in block lits + implicitBargs.append(b) + b + } + Context.annotateImplicits(call, implicitVargs.toList, implicitBargs.toList) + assertArgsParamsAlign(name = Some(name), atargs, avargs, abargs) // (1) Instantiate blocktype // e.g. `[A, B] (A, A) => B` becomes `(?A, ?A) => ?B` @@ -1486,7 +1529,7 @@ object Typer extends Phase[NameResolved, Typechecked] { case _ => () } - (vps zip vargs) foreach { case (tpe, expr) => + (vps zip (vargs ++ implicitVargs.toList)) foreach { case (tpe, expr) => val Result(t, eff) = checkExpr(expr.value, Some(tpe)) effs = effs ++ eff.toEffects } @@ -1494,7 +1537,7 @@ object Typer extends Phase[NameResolved, Typechecked] { // To improve inference, we first type check block arguments that DO NOT subtract effects, // since those need to be fully known. - val (withoutEffects, withEffects) = (bps zip (bargs zip captArgs)).partitionMap { + val (withoutEffects, withEffects) = (bps zip ((bargs ++ implicitBargs.toList) zip captArgs)).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)) @@ -1522,7 +1565,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 +1591,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 +1837,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 +1916,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 +1952,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 +2002,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 1087279758..2d477f968d 100644 --- a/effekt/shared/src/main/scala/effekt/context/Annotations.scala +++ b/effekt/shared/src/main/scala/effekt/context/Annotations.scala @@ -139,6 +139,17 @@ object Annotations { "the inferred or annotated type arguments of" ) + /** + */ + val ImplicitValueArguments = TreeAnnotation[source.CallLike, List[source.ValueArg]]( + "ImplicitValueArguments", + "the inferred implicit value arguments" + ) + 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 47b8c84c3b..7d8707bbd1 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 b7318ae8f4..42705de2c6 100644 --- a/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala +++ b/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala @@ -81,8 +81,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 +156,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/Tree.scala b/effekt/shared/src/main/scala/effekt/source/Tree.scala index 597036b2ff..d54f078fb6 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,13 @@ case class FunctionType(tparams: Many[Id], vparams: Many[ValueType], bparams: Ma */ case class Effectful(tpe: ValueType, eff: Effects, span: Span) extends Type +/** + * For generating code with already resolved types + */ +case class ReifiedType(tpe: symbols.ValueType | symbols.BlockType) extends Type { + val span = Span.missing +} + // These are just type aliases for documentation purposes. type BlockType = Type type ValueType = Type diff --git a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala index 102ceef848..b765ddc1a4 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala @@ -1,6 +1,7 @@ package effekt package symbols +import scala.collection.mutable import effekt.source.IdRef import effekt.util.messages.ErrorReporter @@ -278,6 +279,56 @@ object scopes { namespace.terms.getOrElse(id.name, Set.empty).collect { case op: Operation if filter(op) => op } } + // for caching (to prevent infinite recursion here) + val foundImplicits: mutable.HashMap[(Scope, BlockSymbol), ImplicitContext] = mutable.HashMap.empty + + def lookupPotentialImplicits(forCandidates: List[Set[BlockSymbol]])(using ErrorReporter): 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.collect { case p if p.isImplicit => + p -> source.ValueArg(Some(p.name.name), source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing), source.Span.missing) + }.toMap, + c.bparams.collect { case p if p.isImplicit => + p.tpe.get match { + case BlockType.FunctionType(tparams, cparams, vparams, bparams, result, effects) => + val gtparams = tparams.map { p => source.IdDef(p.name.name, source.Span.missing) } + val gvparams: List[source.ValueParam] = + vparams.map { p => source.ValueParam(source.IdDef("arg0", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } + val gbparams: List[source.BlockParam] = + bparams.map { p => source.BlockParam(source.IdDef("block_arg0", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } + p -> source.BlockLiteral(Nil, gvparams, gbparams, + source.Return(source.Call(source.IdTarget(source.IdRef(Nil, p.name.name, source.Span.missing)), Nil, + gvparams.map { x => source.ValueArg(None, source.Var(source.IdRef(Nil, x.id.name, source.Span.missing), source.Span.missing), source.Span.missing) }, + gbparams.map { x => source.Var(source.IdRef(Nil, x.id.name, source.Span.missing), source.Span.missing) }, + source.Span.missing), + source.Span.missing), source.Span.missing) + case BlockType.InterfaceType(typeConstructor, args) => + // TODO eta-exapnd here, too ? + p -> source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing) + } + }.toMap) + foundImplicits.put((scope, b), r) + Some(b -> r) + case _ => None + } + } + } + }.toMap + } + // the last element in the path can also be the type of the name. def lookupOperation(path: List[String], name: String)(using ErrorReporter): List[Set[Operation]] = all(path, scope) { namespace => diff --git a/effekt/shared/src/main/scala/effekt/symbols/SignaturePrinter.scala b/effekt/shared/src/main/scala/effekt/symbols/SignaturePrinter.scala index f349c88c2d..1a5520d1bc 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 682871a9ea..77c83d1a06 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 ed5f1a041a..c9f7f85b8e 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala @@ -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[ValueParam, source.ValueArg], + var blocks: Map[BlockParam, source.Term]) { + 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 1db81ca171..d80c911fa5 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/DocumentationGenerator.scala b/effekt/shared/src/main/scala/effekt/util/DocumentationGenerator.scala index 143437611e..b057074d90 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) @@ -51,6 +52,7 @@ trait DocumentationGenerator { // block params case BlockTypeTree(eff, _) => ??? + case ReifiedType(tpe) => ??? case FunctionType(tparams, vparams, bparams, result, effects, span) => obj(HashMap( "kind" -> str("FunctionType"), "tparams" -> generateTparams(tparams.unspan), @@ -105,21 +107,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 +326,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 6f716843df..14f3ba19b9 100644 --- a/effekt/shared/src/main/scala/effekt/util/Messages.scala +++ b/effekt/shared/src/main/scala/effekt/util/Messages.scala @@ -169,4 +169,4 @@ object messages { } } } -} +} \ No newline at end of file From 37139528082dd90b959dff9ebaf55acc38ddb35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 18:11:11 +0200 Subject: [PATCH 02/46] Instantiate implicit parameters late --- .../shared/src/main/scala/effekt/Typer.scala | 105 +++++++++++++++++- .../src/main/scala/effekt/symbols/Scope.scala | 2 +- 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 059f9f2926..78063fb33f 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1468,6 +1468,74 @@ object Typer extends Phase[NameResolved, Typechecked] { } } + def instantiateImplicitBlock(b: source.Term, tpe: symbols.BlockType)(using Context): source.Term = { + if(!Context.messaging.hasErrors) { + (b, tpe) match { + // TODO move the instantiation down to when we have the types? + case (a, symbols.BlockType.FunctionType(tps, cps, vps, bps, res, effs)) => + // TODO prevent infinite recursion due to this + a match { + case source.BlockLiteral(tparams, vparams, bparams, source.Return(source.Call(fn, targs, vargs, bargs, _), _), _) => + // 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 = tparams.map { x => symbols.TypeParam(Name.local(x.name)) } + val ftparams = (tparams zip ftpsyms).map { (x, sym) => + val r = source.IdDef(x.name, source.Span.missing) + Context.annotate(Annotations.Symbol, r, sym) + r + } + val ftargs = ftpsyms.map { x => + val r = source.TypeRef(source.IdRef(Nil, x.name.name, source.Span.missing), Many(Nil, source.Span.missing), source.Span.missing) + Context.annotate(Annotations.Symbol, r, x) + r + } + val fvpsyms = (vparams zip vps).map { (x, t) => symbols.ValueParam(Name.local(x.id.name), Some(t), false, NoSource) } + val fvparams = (vparams zip fvpsyms).map { (x, sym) => + val r: source.ValueParam = source.ValueParam(source.IdDef(x.id.name, source.Span.missing), Some(source.ReifiedType(sym.tpe.get)), false, source.Span.missing) + Context.annotate(Annotations.Symbol, r, sym) + Context.annotate(Annotations.Symbol, r.id, sym) + r + } + val fvargs = fvpsyms.map { x => + val r = source.Var(source.IdRef(Nil, x.name.name, source.Span.missing), source.Span.missing) + Context.annotate(Annotations.Symbol, r, x) + Context.annotate(Annotations.Symbol, r.id, x) + source.ValueArg(None, r, source.Span.missing) + } + val fbpsyms = (bparams zip bps).map { (x, t) => symbols.BlockParam(Name.local(x.id.name), Some(t), x.symbol.capture, false, NoSource) } + val fbparams = (bparams zip fbpsyms).map { (x, sym) => + val r: source.BlockParam = source.BlockParam(source.IdDef(x.id.name, source.Span.missing), Some(source.ReifiedType(sym.tpe.get)), false, source.Span.missing) + Context.annotate(Annotations.Symbol, r, sym) + Context.annotate(Annotations.Symbol, r.id, sym) + r + } + val fbargs = fbpsyms.map { x => + val r = source.Var(source.IdRef(Nil, x.name.name, source.Span.missing), source.Span.missing) + Context.annotate(Annotations.Symbol, r, x) + r + } + source.BlockLiteral(ftparams, fvparams, fbparams, + source.Return(source.Call(fn, ftargs, fvargs, fbargs, + source.Span.missing), source.Span.missing), source.Span.missing) + case _ => Context.panic("Unexpected implicit value for implicit block parameter") + } + case (a, symbols.BlockType.InterfaceType(tCons, tArgs)) => + // TODO prevent infinite recursion due to this + // TODO rename everything to use fresh names (preserving Namer results) + // TODO instantiate / substitute types here + a + } + } else { + Context.abort("Not instantiating implicit block argument since there are errors.") + } + } + def instantiateImplicitValue(b: source.ValueArg, tpe: symbols.ValueType)(using Context): source.ValueArg = { + // TODO rename everything to use fresh names (preserving Namer results) + // TODO instantiate / substitute types here + b + } + def checkCallTo( call: source.CallLike, name: String, @@ -1493,12 +1561,11 @@ object Typer extends Phase[NameResolved, Typechecked] { v } val abargs = Aligned(bargs, funTpe.bparams).fillImplicit { - case (bb, i) if potentialBlockImplicits.contains(i) => - val b = (new Tree.Rewrite{}).rewrite(potentialBlockImplicits(i)) // deep copy the tree // TODO prevent infinite recursion due to this // TODO rename params in block lits + case (_, i) if potentialBlockImplicits.contains(i) => + val b = potentialBlockImplicits(i) implicitBargs.append(b) b } - Context.annotateImplicits(call, implicitVargs.toList, implicitBargs.toList) assertArgsParamsAlign(name = Some(name), atargs, avargs, abargs) // (1) Instantiate blocktype @@ -1529,15 +1596,29 @@ object Typer extends Phase[NameResolved, Typechecked] { case _ => () } - (vps zip (vargs ++ implicitVargs.toList)) 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 } + (implicitVps zip implicitVargs) foreach { case (tpe, expr) => + // TODO NOW refresh and instantiate them + val inst = instantiateImplicitValue(expr, tpe) + instImplicitVargs.append(inst) + val Result(t, eff) = 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 ++ implicitBargs.toList) 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)) @@ -1552,6 +1633,20 @@ object Typer extends Phase[NameResolved, Typechecked] { } } + (implicitBps zip (implicitBargs zip implicitCaptArgs)) foreach { case (tpe, (expr, capt)) => + // TODO NOW, refresh and instantiate before checking + flowsInto(capt, callsite) + // capture of block <: ?C + flowingInto(capt) { + val inst = instantiateImplicitBlock(expr, tpe) + instImplicitBargs.append(inst) + val Result(t, eff) = checkExprAsBlock(inst, Some(tpe)) + effs = effs ++ eff.toEffects + } + } + + 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) diff --git a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala index b765ddc1a4..26cd1a8e8e 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala @@ -309,7 +309,7 @@ object scopes { vparams.map { p => source.ValueParam(source.IdDef("arg0", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } val gbparams: List[source.BlockParam] = bparams.map { p => source.BlockParam(source.IdDef("block_arg0", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } - p -> source.BlockLiteral(Nil, gvparams, gbparams, + p -> source.BlockLiteral(gtparams, gvparams, gbparams, source.Return(source.Call(source.IdTarget(source.IdRef(Nil, p.name.name, source.Span.missing)), Nil, gvparams.map { x => source.ValueArg(None, source.Var(source.IdRef(Nil, x.id.name, source.Span.missing), source.Span.missing), source.Span.missing) }, gbparams.map { x => source.Var(source.IdRef(Nil, x.id.name, source.Span.missing), source.Span.missing) }, From 6132a0fa2629575bdf2df347caa7d6860859bafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 18:16:15 +0200 Subject: [PATCH 03/46] Remove TODOs --- effekt/shared/src/main/scala/effekt/Typer.scala | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 78063fb33f..acd727e53d 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1471,9 +1471,7 @@ object Typer extends Phase[NameResolved, Typechecked] { def instantiateImplicitBlock(b: source.Term, tpe: symbols.BlockType)(using Context): source.Term = { if(!Context.messaging.hasErrors) { (b, tpe) match { - // TODO move the instantiation down to when we have the types? case (a, symbols.BlockType.FunctionType(tps, cps, vps, bps, res, effs)) => - // TODO prevent infinite recursion due to this a match { case source.BlockLiteral(tparams, vparams, bparams, source.Return(source.Call(fn, targs, vargs, bargs, _), _), _) => // We need to refresh the whole binding structure, so we don't have duplicate stuff in the tree. @@ -1521,19 +1519,16 @@ object Typer extends Phase[NameResolved, Typechecked] { case _ => Context.panic("Unexpected implicit value for implicit block parameter") } case (a, symbols.BlockType.InterfaceType(tCons, tArgs)) => - // TODO prevent infinite recursion due to this - // TODO rename everything to use fresh names (preserving Namer results) - // TODO instantiate / substitute types here + // There is nothing to do here a } } else { Context.abort("Not instantiating implicit block argument since there are errors.") } } - def instantiateImplicitValue(b: source.ValueArg, tpe: symbols.ValueType)(using Context): source.ValueArg = { - // TODO rename everything to use fresh names (preserving Namer results) - // TODO instantiate / substitute types here - b + def instantiateImplicitValue(v: source.ValueArg, tpe: symbols.ValueType)(using Context): source.ValueArg = { + // There is nothing to do here + v } def checkCallTo( From e1849235b4e78f10f89d4ea6ebad54bce37aa8cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 18:40:15 +0200 Subject: [PATCH 04/46] Fix recursive usage of implicits --- effekt/shared/src/main/scala/effekt/Typer.scala | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index acd727e53d..780a11bee0 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1513,8 +1513,19 @@ object Typer extends Phase[NameResolved, Typechecked] { Context.annotate(Annotations.Symbol, r, x) r } + 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 + }) + 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(fn, ftargs, fvargs, fbargs, + source.Return(source.Call(ffn, ftargs, fvargs, fbargs, source.Span.missing), source.Span.missing), source.Span.missing) case _ => Context.panic("Unexpected implicit value for implicit block parameter") } From 5b301e20e28559b0e3e23bbb7e2a2b9f8fb268e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 19:50:59 +0200 Subject: [PATCH 05/46] Fix case where there are implicit and explicit arguments --- effekt/shared/src/main/scala/effekt/Typer.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 780a11bee0..83323d5930 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1348,8 +1348,9 @@ object Typer extends Phase[NameResolved, Typechecked] { object Aligned { extension[A,B](self: Aligned[A, B]) def fillImplicit(by: PartialFunction[(B, Int), A]): Aligned[A,B] = { - val (impl, remaining) = self.missing.zipWithIndex.partition(by.isDefinedAt) - Aligned(self.matched ++ impl.map{ (a, i) => (by(a, i), a) }, self.extra, remaining.map{ (a, _) => a }) + val pre = self.matched.length + val (impl, remaining) = self.missing.zipWithIndex.partition{ (a,i) => by.isDefinedAt(a, i + pre) } + Aligned(self.matched ++ impl.map{ (a, i) => (by(a, i + pre), a) }, self.extra, remaining.map{ (a, _) => a }) } def apply[A, B](got: List[A], expected: List[B]): Aligned[A, B] = { @scala.annotation.tailrec From 6dcaf3074fb9ffeb404417f0575faa64ff281afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 19:55:33 +0200 Subject: [PATCH 06/46] Add missing annotation for block argument --- effekt/shared/src/main/scala/effekt/Typer.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 83323d5930..750ba8bd38 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1512,6 +1512,7 @@ object Typer extends Phase[NameResolved, Typechecked] { val fbargs = fbpsyms.map { x => val r = source.Var(source.IdRef(Nil, x.name.name, source.Span.missing), source.Span.missing) Context.annotate(Annotations.Symbol, r, x) + Context.annotate(Annotations.Symbol, r.id, x) r } val ffn = fn match { From 01ac986e23b99b16d3bec08c13d5905a95cf3f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 20:02:04 +0200 Subject: [PATCH 07/46] Split out generation of implicits into separate methods --- .../src/main/scala/effekt/symbols/Scope.scala | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala index 26cd1a8e8e..90fa3ebe15 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala @@ -4,6 +4,7 @@ package symbols import scala.collection.mutable import effekt.source.IdRef import effekt.util.messages.ErrorReporter +import effekt.context.Context /** * An immutable container of bindings. @@ -282,7 +283,29 @@ object scopes { // for caching (to prevent infinite recursion here) val foundImplicits: mutable.HashMap[(Scope, BlockSymbol), ImplicitContext] = mutable.HashMap.empty - def lookupPotentialImplicits(forCandidates: List[Set[BlockSymbol]])(using ErrorReporter): Map[BlockSymbol, ImplicitContext] = { + def generateImplicitValueArg(p: symbols.ValueParam)(using Context): source.ValueArg = { + source.ValueArg(Some(p.name.name), source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing), source.Span.missing) + } + def generateImplicitBlockArg(p: symbols.BlockParam)(using Context): source.Term = + p.tpe.get match { + case BlockType.FunctionType(tparams, cparams, vparams, bparams, result, effects) => + val gtparams = tparams.map { p => source.IdDef(p.name.name, source.Span.missing) } + val gvparams: List[source.ValueParam] = + vparams.map { p => source.ValueParam(source.IdDef("arg0", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } + val gbparams: List[source.BlockParam] = + bparams.map { p => source.BlockParam(source.IdDef("block_arg0", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } + source.BlockLiteral(gtparams, gvparams, gbparams, + source.Return(source.Call(source.IdTarget(source.IdRef(Nil, p.name.name, source.Span.missing)), Nil, + gvparams.map { x => source.ValueArg(None, source.Var(source.IdRef(Nil, x.id.name, source.Span.missing), source.Span.missing), source.Span.missing) }, + gbparams.map { x => source.Var(source.IdRef(Nil, x.id.name, source.Span.missing), source.Span.missing) }, + source.Span.missing), + source.Span.missing), source.Span.missing) + case BlockType.InterfaceType(typeConstructor, args) => + // TODO eta-exapnd here, too ? + source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing) + } + + def lookupPotentialImplicits(forCandidates: List[Set[BlockSymbol]])(using Context): Map[BlockSymbol, ImplicitContext] = { forCandidates.flatMap { level => level.flatMap { b => def findCached(b: BlockSymbol, scope: Scope): Option[ImplicitContext] = { @@ -298,28 +321,9 @@ object scopes { b match { // walks up scopes, because block parameters should be eta-expanded below case c: Callable => - val r =ImplicitContext(c.vparams.collect { case p if p.isImplicit => - p -> source.ValueArg(Some(p.name.name), source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing), source.Span.missing) - }.toMap, - c.bparams.collect { case p if p.isImplicit => - p.tpe.get match { - case BlockType.FunctionType(tparams, cparams, vparams, bparams, result, effects) => - val gtparams = tparams.map { p => source.IdDef(p.name.name, source.Span.missing) } - val gvparams: List[source.ValueParam] = - vparams.map { p => source.ValueParam(source.IdDef("arg0", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } - val gbparams: List[source.BlockParam] = - bparams.map { p => source.BlockParam(source.IdDef("block_arg0", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } - p -> source.BlockLiteral(gtparams, gvparams, gbparams, - source.Return(source.Call(source.IdTarget(source.IdRef(Nil, p.name.name, source.Span.missing)), Nil, - gvparams.map { x => source.ValueArg(None, source.Var(source.IdRef(Nil, x.id.name, source.Span.missing), source.Span.missing), source.Span.missing) }, - gbparams.map { x => source.Var(source.IdRef(Nil, x.id.name, source.Span.missing), source.Span.missing) }, - source.Span.missing), - source.Span.missing), source.Span.missing) - case BlockType.InterfaceType(typeConstructor, args) => - // TODO eta-exapnd here, too ? - p -> source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing) - } - }.toMap) + val r = ImplicitContext( + c.vparams.collect { case p if p.isImplicit => p -> generateImplicitValueArg(p) }.toMap, + c.bparams.collect { case p if p.isImplicit => p -> generateImplicitBlockArg(p) }.toMap) foundImplicits.put((scope, b), r) Some(b -> r) case _ => None From 99e07aad64aba8532419f9a3ed4bbcb486b97583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 20:22:02 +0200 Subject: [PATCH 08/46] Add sourcePosition implicit arguments --- .../src/main/scala/effekt/symbols/Scope.scala | 15 ++++++++++++++- libraries/common/effekt.effekt | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala index 90fa3ebe15..787b71cd63 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala @@ -284,7 +284,20 @@ object scopes { val foundImplicits: mutable.HashMap[(Scope, BlockSymbol), ImplicitContext] = mutable.HashMap.empty def generateImplicitValueArg(p: symbols.ValueParam)(using Context): source.ValueArg = { - source.ValueArg(Some(p.name.name), source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing), source.Span.missing) + source.ValueArg(Some(p.name.name), p.name.name match { + case "sourcePosition" => + val pos = Context.focus.span + val from = pos.source.offsetToPosition(pos.from) + val to = pos.source.offsetToPosition(pos.to) + source.Call(source.IdTarget(source.IdRef(Nil, "SourcePosition", source.Span.missing)), Nil, List( + source.ValueArg(None, source.Literal(pos.source.name, builtins.TString, source.Span.missing), source.Span.missing), + source.ValueArg(None, source.Literal(from.line, builtins.TInt, source.Span.missing), source.Span.missing), + source.ValueArg(None, source.Literal(from.column, builtins.TInt, source.Span.missing), source.Span.missing), + source.ValueArg(None, source.Literal(to.line, builtins.TInt, source.Span.missing), source.Span.missing), + source.ValueArg(None, source.Literal(to.column, builtins.TInt, source.Span.missing), source.Span.missing), + ), Nil, source.Span.missing) + case _ => source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing) + }, source.Span.missing) } def generateImplicitBlockArg(p: symbols.BlockParam)(using Context): source.Term = p.tpe.get match { diff --git a/libraries/common/effekt.effekt b/libraries/common/effekt.effekt index 5871f9d9ca..a09c16515b 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 From d187654eb2a1c3a2aff52158ac61173b40ae7826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 21:02:26 +0200 Subject: [PATCH 09/46] Add basic check for order between implicits and non-implicits --- .../shared/src/main/scala/effekt/Namer.scala | 19 +++++++++++++++++++ .../src/main/scala/effekt/util/package.scala | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index d04abe6985..e6d08361b1 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -16,6 +16,7 @@ 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 @@ -338,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) } @@ -348,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) } @@ -365,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) { @@ -446,6 +450,19 @@ object Namer extends Phase[Parsed, NameResolved] { resolve(a.value) } + @tailrec + def checkImplicitParams(l: List[source.Param], implicitsAllowed: RequirementLevel = RequirementLevel.Optional)(using Context): Unit = (l, implicitsAllowed) match { + case ((source.ValueParam(_, _, true, _) | source.BlockParam(_, _, true, _)) :: tl, RequirementLevel.Forbidden) => + Context.error(pretty"Implicit parameter ${l.head.span.text.getOrElse(l.head.id.name)} can never be passed implicitly to here.") + case ((source.ValueParam(_, _, false, _) | source.BlockParam(_, _, false, _)) :: tl, RequirementLevel.Required) => + 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, _) => () + } + def resolveImplicits(id: source.Id)(using Context): Unit = { Context.symbolOf(id) match { case symbols.CallTarget(syms, cs) => @@ -534,6 +551,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 diff --git a/effekt/shared/src/main/scala/effekt/util/package.scala b/effekt/shared/src/main/scala/effekt/util/package.scala index a9915c0616..1526aaeb6f 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 From 1c82dd67f4e09e67607231d1f441135a6af1ad93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 21:16:06 +0200 Subject: [PATCH 10/46] Add some tests for name-based implicits --- .../neg/name-based-implicits/order.effekt | 6 ++++ examples/pos/name-based-implicits/lensy.check | 1 + .../pos/name-based-implicits/lensy.effekt | 9 ++++++ .../pos/name-based-implicits/my-show.check | 3 ++ .../pos/name-based-implicits/my-show.effekt | 29 +++++++++++++++++++ .../source-positions.check | 1 + .../source-positions.effekt | 11 +++++++ .../pos/name-based-implicits/type-tags.check | 1 + .../pos/name-based-implicits/type-tags.effekt | 21 ++++++++++++++ 9 files changed, 82 insertions(+) create mode 100644 examples/neg/name-based-implicits/order.effekt create mode 100644 examples/pos/name-based-implicits/lensy.check create mode 100644 examples/pos/name-based-implicits/lensy.effekt create mode 100644 examples/pos/name-based-implicits/my-show.check create mode 100644 examples/pos/name-based-implicits/my-show.effekt create mode 100644 examples/pos/name-based-implicits/source-positions.check create mode 100644 examples/pos/name-based-implicits/source-positions.effekt create mode 100644 examples/pos/name-based-implicits/type-tags.check create mode 100644 examples/pos/name-based-implicits/type-tags.effekt diff --git a/examples/neg/name-based-implicits/order.effekt b/examples/neg/name-based-implicits/order.effekt new file mode 100644 index 0000000000..1e811b8943 --- /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/pos/name-based-implicits/lensy.check b/examples/pos/name-based-implicits/lensy.check new file mode 100644 index 0000000000..bdf8bf2984 --- /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 0000000000..0647fdb4d3 --- /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 0000000000..e2cb99fa40 --- /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 0000000000..1f7bf8d608 --- /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/source-positions.check b/examples/pos/name-based-implicits/source-positions.check new file mode 100644 index 0000000000..77b9f33381 --- /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:6 \ 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 0000000000..14fe2970fd --- /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 0000000000..1858f774d0 --- /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 0000000000..0ab3781cbe --- /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 From a7d36761d7b30362a54fb6899c390f2cba07cb6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 21:38:30 +0200 Subject: [PATCH 11/46] Fix name-based implicits on operations (on objects/effects) --- .../effekt/source/ExplicitCapabilities.scala | 14 +++++++--- .../pos/name-based-implicits/operations.check | 3 +++ .../name-based-implicits/operations.effekt | 26 +++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 examples/pos/name-based-implicits/operations.check create mode 100644 examples/pos/name-based-implicits/operations.effekt diff --git a/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala b/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala index 42705de2c6..f774fb1382 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) diff --git a/examples/pos/name-based-implicits/operations.check b/examples/pos/name-based-implicits/operations.check new file mode 100644 index 0000000000..532656090e --- /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 0000000000..0b2c5f1153 --- /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 From da1119ad0e3f69d63e95e92cda35b04e11ecb6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 22:12:06 +0200 Subject: [PATCH 12/46] Fix generated names, Report errors during namechecking of impicit arguments if overloading fails --- .../shared/src/main/scala/effekt/Namer.scala | 28 +++++-------- .../shared/src/main/scala/effekt/Typer.scala | 39 +++++++++++++------ .../src/main/scala/effekt/symbols/Scope.scala | 20 +++++----- .../main/scala/effekt/symbols/symbols.scala | 6 +-- 4 files changed, 50 insertions(+), 43 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index e6d08361b1..293e5f9610 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -469,25 +469,17 @@ object Namer extends Phase[Parsed, NameResolved] { 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. - val tVimpls = vimpls.flatMap { case k -> v => - val r = Try { - resolve(v) - }; - if (r.isRight) { - Some(k -> v) - } else { - None - } + val tVimpls = vimpls.map { case k -> mv => + k -> (for { + v <- mv + r <- Try { resolve(v) } + } yield v) } - val tBimpls = bimpls.flatMap { case k -> b => - val r = Try { - resolve(b) - } - if (r.isRight) { - Some(k -> b) - } else { - None - } + val tBimpls = bimpls.map { case k -> mb => + k -> (for { + b <- mb + r <- Try { resolve(b) } + } yield b) } c.values = tVimpls c.blocks = tBimpls diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 750ba8bd38..cbdda1760d 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1214,7 +1214,8 @@ object Typer extends Phase[NameResolved, Typechecked] { } def findImplicitArgs(r: BlockSymbol, - impls: ImplicitContext): (Map[Int, source.ValueArg], Map[Int, source.Term]) = { + impls: ImplicitContext): + (Map[Int, Either[EffektMessages, source.ValueArg]], Map[Int, Either[EffektMessages, source.Term]]) = { r match { case c: Callable => (c.vparams.zipWithIndex.flatMap { @@ -1347,10 +1348,10 @@ object Typer extends Phase[NameResolved, Typechecked] { object Aligned { extension[A,B](self: Aligned[A, B]) - def fillImplicit(by: PartialFunction[(B, Int), A]): Aligned[A,B] = { + def fillImplicit(by: (B, Int) => Option[A]): Aligned[A,B] = { val pre = self.matched.length - val (impl, remaining) = self.missing.zipWithIndex.partition{ (a,i) => by.isDefinedAt(a, i + pre) } - Aligned(self.matched ++ impl.map{ (a, i) => (by(a, i + pre), a) }, self.extra, remaining.map{ (a, _) => a }) + val (remaining, impl) = self.missing.zipWithIndex.partitionMap{ (a,i) => by(a, i + pre).map((_, a)).toRight(a) } + Aligned(self.matched ++ impl, self.extra, remaining) } def apply[A, B](got: List[A], expected: List[B]): Aligned[A, B] = { @scala.annotation.tailrec @@ -1553,8 +1554,8 @@ object Typer extends Phase[NameResolved, Typechecked] { vargs: List[source.ValueArg], bargs: List[source.Term], expected: Option[ValueType], - potentialValueImplicits: Map[Int, source.ValueArg], - potentialBlockImplicits: Map[Int, Term] + potentialValueImplicits: Map[Int, Either[EffektMessages, source.ValueArg]], + potentialBlockImplicits: Map[Int, Either[EffektMessages, Term]] )(using Context, Captures): Result[ValueType] = { val callsite = currentCapture @@ -1564,15 +1565,29 @@ object Typer extends Phase[NameResolved, Typechecked] { val implicitBargs: mutable.ListBuffer[source.Term] = mutable.ListBuffer.empty val avargs = Aligned(vargs, funTpe.vparams).fillImplicit { case (e, i) if potentialValueImplicits.contains(i) => - val v = potentialValueImplicits(i) - implicitVargs.append(v) - v + potentialValueImplicits(i) match { + case Right(v) => + implicitVargs.append(v) + Some(v) + case Left(msgs) => + Context.error(pretty"""Could not resolve implicit value parameter ${i}:""") + msgs.foreach(Context.report) + None + } + case _ => None } val abargs = Aligned(bargs, funTpe.bparams).fillImplicit { case (_, i) if potentialBlockImplicits.contains(i) => - val b = potentialBlockImplicits(i) - implicitBargs.append(b) - b + potentialBlockImplicits(i) match { + case Right(b) => + implicitBargs.append(b) + Some(b) + case Left(msgs) => + Context.error(pretty"""Could not resolve implicit block parameter ${i}:""") + msgs.foreach(Context.report) + None + } + case _ => None } assertArgsParamsAlign(name = Some(name), atargs, avargs, abargs) diff --git a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala index 787b71cd63..a3a5c2820a 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala @@ -3,7 +3,7 @@ package symbols import scala.collection.mutable import effekt.source.IdRef -import effekt.util.messages.ErrorReporter +import effekt.util.messages.{ErrorReporter, EffektMessages} import effekt.context.Context /** @@ -283,8 +283,8 @@ object scopes { // for caching (to prevent infinite recursion here) val foundImplicits: mutable.HashMap[(Scope, BlockSymbol), ImplicitContext] = mutable.HashMap.empty - def generateImplicitValueArg(p: symbols.ValueParam)(using Context): source.ValueArg = { - source.ValueArg(Some(p.name.name), p.name.name match { + def generateImplicitValueArg(p: symbols.ValueParam)(using Context): Either[EffektMessages, source.ValueArg] = { + Right(source.ValueArg(Some(p.name.name), p.name.name match { case "sourcePosition" => val pos = Context.focus.span val from = pos.source.offsetToPosition(pos.from) @@ -297,25 +297,25 @@ object scopes { source.ValueArg(None, source.Literal(to.column, builtins.TInt, source.Span.missing), source.Span.missing), ), Nil, source.Span.missing) case _ => source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing) - }, source.Span.missing) + }, source.Span.missing)) } - def generateImplicitBlockArg(p: symbols.BlockParam)(using Context): source.Term = + def generateImplicitBlockArg(p: symbols.BlockParam)(using Context): Either[EffektMessages, source.Term] = p.tpe.get match { case BlockType.FunctionType(tparams, cparams, vparams, bparams, result, effects) => val gtparams = tparams.map { p => source.IdDef(p.name.name, source.Span.missing) } val gvparams: List[source.ValueParam] = - vparams.map { p => source.ValueParam(source.IdDef("arg0", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } + vparams.zipWithIndex.map { (p, i) => source.ValueParam(source.IdDef(s"arg${i}", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } val gbparams: List[source.BlockParam] = - bparams.map { p => source.BlockParam(source.IdDef("block_arg0", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } - source.BlockLiteral(gtparams, gvparams, gbparams, + bparams.zipWithIndex.map { (p, i) => source.BlockParam(source.IdDef(s"block_arg${i}", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } + Right(source.BlockLiteral(gtparams, gvparams, gbparams, source.Return(source.Call(source.IdTarget(source.IdRef(Nil, p.name.name, source.Span.missing)), Nil, gvparams.map { x => source.ValueArg(None, source.Var(source.IdRef(Nil, x.id.name, source.Span.missing), source.Span.missing), source.Span.missing) }, gbparams.map { x => source.Var(source.IdRef(Nil, x.id.name, source.Span.missing), source.Span.missing) }, source.Span.missing), - source.Span.missing), source.Span.missing) + source.Span.missing), source.Span.missing)) case BlockType.InterfaceType(typeConstructor, args) => // TODO eta-exapnd here, too ? - source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing) + Right(source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing)) } def lookupPotentialImplicits(forCandidates: List[Set[BlockSymbol]])(using Context): Map[BlockSymbol, ImplicitContext] = { diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala index c9f7f85b8e..5a5bb8a4b9 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: @@ -204,8 +204,8 @@ enum Binder extends TermSymbol { } export Binder.* -case class ImplicitContext(var values: Map[ValueParam, source.ValueArg], - var blocks: Map[BlockParam, source.Term]) { +case class ImplicitContext(var values: Map[ValueParam, Either[EffektMessages, source.ValueArg]], + var blocks: Map[BlockParam, Either[EffektMessages, source.Term]]) { var resolved = false // will be set in namer after the values in the maps are resolved } object ImplicitContext { From 111b691e1f33995a28f7a99a14340683e52eb51e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 22:27:36 +0200 Subject: [PATCH 13/46] Add another example (implementing type-level plus and toInt) --- examples/pos/name-based-implicits/typelevel.check | 1 + examples/pos/name-based-implicits/typelevel.effekt | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 examples/pos/name-based-implicits/typelevel.check create mode 100644 examples/pos/name-based-implicits/typelevel.effekt diff --git a/examples/pos/name-based-implicits/typelevel.check b/examples/pos/name-based-implicits/typelevel.check new file mode 100644 index 0000000000..e440e5c842 --- /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 0000000000..4e1af8af50 --- /dev/null +++ b/examples/pos/name-based-implicits/typelevel.effekt @@ -0,0 +1,12 @@ +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: A): 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() = println(toInt(add(Succ(Succ(Zero())), Succ(Zero()))).show) \ No newline at end of file From 5695bfccad13ff9779230c330c5008039fd8d42a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 22:32:45 +0200 Subject: [PATCH 14/46] Make ints long to make the llvm backend happy --- effekt/shared/src/main/scala/effekt/symbols/Scope.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala index a3a5c2820a..c6c754d8d6 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala @@ -291,10 +291,10 @@ object scopes { val to = pos.source.offsetToPosition(pos.to) source.Call(source.IdTarget(source.IdRef(Nil, "SourcePosition", source.Span.missing)), Nil, List( source.ValueArg(None, source.Literal(pos.source.name, builtins.TString, source.Span.missing), source.Span.missing), - source.ValueArg(None, source.Literal(from.line, builtins.TInt, source.Span.missing), source.Span.missing), - source.ValueArg(None, source.Literal(from.column, builtins.TInt, source.Span.missing), source.Span.missing), - source.ValueArg(None, source.Literal(to.line, builtins.TInt, source.Span.missing), source.Span.missing), - source.ValueArg(None, source.Literal(to.column, builtins.TInt, source.Span.missing), source.Span.missing), + source.ValueArg(None, source.Literal(from.line.toLong, builtins.TInt, source.Span.missing), source.Span.missing), + source.ValueArg(None, source.Literal(from.column.toLong, builtins.TInt, source.Span.missing), source.Span.missing), + source.ValueArg(None, source.Literal(to.line.toLong, builtins.TInt, source.Span.missing), source.Span.missing), + source.ValueArg(None, source.Literal(to.column.toLong, builtins.TInt, source.Span.missing), source.Span.missing), ), Nil, source.Span.missing) case _ => source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing) }, source.Span.missing)) From 8725374cee1615d61a5768df06592ef22719242c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 22:35:54 +0200 Subject: [PATCH 15/46] Bind intermediate value in example --- examples/pos/name-based-implicits/typelevel.effekt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/pos/name-based-implicits/typelevel.effekt b/examples/pos/name-based-implicits/typelevel.effekt index 4e1af8af50..0fd6e285ed 100644 --- a/examples/pos/name-based-implicits/typelevel.effekt +++ b/examples/pos/name-based-implicits/typelevel.effekt @@ -9,4 +9,7 @@ def add[A](y: Zero, x: A): 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() = println(toInt(add(Succ(Succ(Zero())), Succ(Zero()))).show) \ No newline at end of file +def main() = { + val x = add(Succ(Succ(Zero())), Succ(Zero())) + println(toInt(x).show) +} \ No newline at end of file From f8930cd5ae2ad0905635cba3ea563886f2e157bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 22:38:02 +0200 Subject: [PATCH 16/46] Fix typelevel example for other inputs --- examples/pos/name-based-implicits/typelevel.effekt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pos/name-based-implicits/typelevel.effekt b/examples/pos/name-based-implicits/typelevel.effekt index 0fd6e285ed..b07465d24a 100644 --- a/examples/pos/name-based-implicits/typelevel.effekt +++ b/examples/pos/name-based-implicits/typelevel.effekt @@ -5,7 +5,7 @@ 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: A): 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))) From 87b14aca68b259d35300784139b98d1d7dfd7e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 22:41:43 +0200 Subject: [PATCH 17/46] Print implicit arguments correctly in DeclPrinter --- .../shared/src/main/scala/effekt/symbols/DeclPrinter.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/symbols/DeclPrinter.scala b/effekt/shared/src/main/scala/effekt/symbols/DeclPrinter.scala index 7ca6228b23..e5e86db773 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" From bd7bacc137f90731dc538d6c49db3718cd4aa960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 23:11:30 +0200 Subject: [PATCH 18/46] Move initial generation of implicit arguments to separate object --- .../shared/src/main/scala/effekt/Namer.scala | 16 ++-- .../shared/src/main/scala/effekt/Typer.scala | 37 ++------- .../scala/effekt/context/Annotations.scala | 6 ++ .../effekt/source/GenerateImplicitArgs.scala | 78 +++++++++++++++++++ .../src/main/scala/effekt/symbols/Scope.scala | 70 +---------------- .../main/scala/effekt/symbols/symbols.scala | 4 +- 6 files changed, 105 insertions(+), 106 deletions(-) create mode 100644 effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index 293e5f9610..323af9ea7d 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -7,7 +7,7 @@ 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.* @@ -450,6 +450,10 @@ 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 ((source.ValueParam(_, _, true, _) | source.BlockParam(_, _, true, _)) :: tl, RequirementLevel.Forbidden) => @@ -1135,7 +1139,7 @@ trait NamerOps extends ContextOps { Context: Context => if (syms2.nonEmpty) { val syms3 = syms2.asInstanceOf[List[Set[BlockSymbol]]] - assignSymbol(id, CallTarget(syms3, scope.lookupPotentialImplicits(syms3))) + assignSymbol(id, CallTarget(syms3, GenerateImplicitArgs.lookupPotentialImplicits(syms3, scope.scope))) true } else { false } } @@ -1150,7 +1154,7 @@ trait NamerOps extends ContextOps { Context: Context => if (syms3.nonEmpty) { val syms4 = syms3.asInstanceOf[List[Set[BlockSymbol]]] - assignSymbol(id, CallTarget(syms4, scope.lookupPotentialImplicits(syms4))) + assignSymbol(id, CallTarget(syms4, GenerateImplicitArgs.lookupPotentialImplicits(syms4, scope.scope))) true } else { false } } @@ -1176,7 +1180,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, scope.lookupPotentialImplicits(blocks))) + assignSymbol(id, CallTarget(blocks, GenerateImplicitArgs.lookupPotentialImplicits(blocks, scope.scope))) } } @@ -1231,7 +1235,7 @@ trait NamerOps extends ContextOps { Context: Context => } val bsyms = syms.asInstanceOf[List[Set[BlockSymbol]]] - assignSymbol(id, CallTarget(bsyms, scope.lookupPotentialImplicits(bsyms))) + assignSymbol(id, CallTarget(bsyms, GenerateImplicitArgs.lookupPotentialImplicits(bsyms, scope.scope))) } /** @@ -1246,7 +1250,7 @@ trait NamerOps extends ContextOps { Context: Context => } val bsyms = syms.asInstanceOf[List[Set[BlockSymbol]]] - assignSymbol(id, CallTarget(bsyms, scope.lookupPotentialImplicits(bsyms))) + assignSymbol(id, CallTarget(bsyms, GenerateImplicitArgs.lookupPotentialImplicits(bsyms, scope.scope))) } /** diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index cbdda1760d..602aea4514 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -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, Map.empty, Map.empty) + 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`. @@ -1207,30 +1207,11 @@ 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 - val (cvimpls, cbimpls) = findImplicitArgs(op, impls.getOrElse(op, ImplicitContext.empty)) - checkCallTo(call, op.name.name, op.vparams.map { p => p.name.name }, funTpe, synthTargs, vargs, bargs, expected, cvimpls, cbimpls) + 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) } - def findImplicitArgs(r: BlockSymbol, - impls: ImplicitContext): - (Map[Int, Either[EffektMessages, source.ValueArg]], Map[Int, Either[EffektMessages, source.Term]]) = { - r match { - case c: Callable => - (c.vparams.zipWithIndex.flatMap { - case (v, i) if v.isImplicit => - impls.values.get(v).map(i -> _) - case _ => None - }.toMap, c.bparams.zipWithIndex.flatMap { - case (b, i) if b.isImplicit => - impls.blocks.get(b).map(i -> _) - case _ => None - }.toMap) - case _ => (Map.empty, Map.empty) - } - } - /** * Attempts to check a potentially overladed call, not reporting any errors but returning them instead. * @@ -1279,8 +1260,7 @@ object Typer extends Phase[NameResolved, Typechecked] { case c: Callable => c.vparams.map(_.name.name) case _ => Nil } - val (cvimpls, cbimpls) = findImplicitArgs(receiver, impls.getOrElse(receiver, ImplicitContext.empty)) - val Result(tpe, effs) = checkCallTo(call, receiver.name.name, vpnames, funTpe, targs, vargs, bargs, expected, cvimpls, cbimpls) + 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) @@ -1554,8 +1534,7 @@ object Typer extends Phase[NameResolved, Typechecked] { vargs: List[source.ValueArg], bargs: List[source.Term], expected: Option[ValueType], - potentialValueImplicits: Map[Int, Either[EffektMessages, source.ValueArg]], - potentialBlockImplicits: Map[Int, Either[EffektMessages, Term]] + potentialImplicits: ImplicitContext )(using Context, Captures): Result[ValueType] = { val callsite = currentCapture @@ -1564,8 +1543,8 @@ object Typer extends Phase[NameResolved, Typechecked] { val implicitVargs: mutable.ListBuffer[source.ValueArg] = mutable.ListBuffer.empty val implicitBargs: mutable.ListBuffer[source.Term] = mutable.ListBuffer.empty val avargs = Aligned(vargs, funTpe.vparams).fillImplicit { - case (e, i) if potentialValueImplicits.contains(i) => - potentialValueImplicits(i) match { + case (e, i) if potentialImplicits.values.contains(i) => + potentialImplicits.values(i) match { case Right(v) => implicitVargs.append(v) Some(v) @@ -1577,8 +1556,8 @@ object Typer extends Phase[NameResolved, Typechecked] { case _ => None } val abargs = Aligned(bargs, funTpe.bparams).fillImplicit { - case (_, i) if potentialBlockImplicits.contains(i) => - potentialBlockImplicits(i) match { + case (_, i) if potentialImplicits.blocks.contains(i) => + potentialImplicits.blocks(i) match { case Right(b) => implicitBargs.append(b) Some(b) diff --git a/effekt/shared/src/main/scala/effekt/context/Annotations.scala b/effekt/shared/src/main/scala/effekt/context/Annotations.scala index 2d477f968d..ef2640f879 100644 --- a/effekt/shared/src/main/scala/effekt/context/Annotations.scala +++ b/effekt/shared/src/main/scala/effekt/context/Annotations.scala @@ -140,11 +140,17 @@ object Annotations { ) /** + * 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" 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 0000000000..dda3b36f84 --- /dev/null +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -0,0 +1,78 @@ +package effekt.source +import scala.collection.mutable +import effekt.util.messages.{ErrorReporter, EffektMessages} +import effekt.context.Context +import effekt.symbols +import effekt.symbols.scopes.Scope +import effekt.symbols.{BlockSymbol, BlockType, Callable, ImplicitContext, builtins} + +object GenerateImplicitArgs { + + // for caching (to prevent infinite recursion here) + val foundImplicits: mutable.HashMap[(Scope, BlockSymbol), ImplicitContext] = mutable.HashMap.empty + + def generateImplicitValueArg(p: symbols.ValueParam)(using Context): Either[EffektMessages, ValueArg] = { + Right(ValueArg(Some(p.name.name), p.name.name match { + case "sourcePosition" => + val pos = Context.focus.span + val from = pos.source.offsetToPosition(pos.from) + val to = pos.source.offsetToPosition(pos.to) + Call(IdTarget(IdRef(Nil, "SourcePosition", Span.missing)), Nil, List( + ValueArg(None, Literal(pos.source.name, builtins.TString, Span.missing), Span.missing), + ValueArg(None, Literal(from.line.toLong, builtins.TInt, Span.missing), Span.missing), + ValueArg(None, Literal(from.column.toLong, builtins.TInt, Span.missing), Span.missing), + ValueArg(None, Literal(to.line.toLong, builtins.TInt, Span.missing), Span.missing), + ValueArg(None, Literal(to.column.toLong, builtins.TInt, Span.missing), Span.missing), + ), Nil, Span.missing) + case _ => Var(IdRef(Nil, p.name.name, Span.missing), Span.missing) + }, Span.missing)) + } + + def generateImplicitBlockArg(p: symbols.BlockParam)(using Context): Either[EffektMessages, Term] = + 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(ReifiedType(p)), false, Span.missing) } + val gbparams: List[BlockParam] = + bparams.zipWithIndex.map { (p, i) => BlockParam(IdDef(s"block_arg${i}", Span.missing), Some(ReifiedType(p)), false, Span.missing) } + Right(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 ? + Right(Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) + } + + 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 + } +} diff --git a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala index c6c754d8d6..102ceef848 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala @@ -1,10 +1,8 @@ package effekt package symbols -import scala.collection.mutable import effekt.source.IdRef -import effekt.util.messages.{ErrorReporter, EffektMessages} -import effekt.context.Context +import effekt.util.messages.ErrorReporter /** * An immutable container of bindings. @@ -280,72 +278,6 @@ object scopes { namespace.terms.getOrElse(id.name, Set.empty).collect { case op: Operation if filter(op) => op } } - // for caching (to prevent infinite recursion here) - val foundImplicits: mutable.HashMap[(Scope, BlockSymbol), ImplicitContext] = mutable.HashMap.empty - - def generateImplicitValueArg(p: symbols.ValueParam)(using Context): Either[EffektMessages, source.ValueArg] = { - Right(source.ValueArg(Some(p.name.name), p.name.name match { - case "sourcePosition" => - val pos = Context.focus.span - val from = pos.source.offsetToPosition(pos.from) - val to = pos.source.offsetToPosition(pos.to) - source.Call(source.IdTarget(source.IdRef(Nil, "SourcePosition", source.Span.missing)), Nil, List( - source.ValueArg(None, source.Literal(pos.source.name, builtins.TString, source.Span.missing), source.Span.missing), - source.ValueArg(None, source.Literal(from.line.toLong, builtins.TInt, source.Span.missing), source.Span.missing), - source.ValueArg(None, source.Literal(from.column.toLong, builtins.TInt, source.Span.missing), source.Span.missing), - source.ValueArg(None, source.Literal(to.line.toLong, builtins.TInt, source.Span.missing), source.Span.missing), - source.ValueArg(None, source.Literal(to.column.toLong, builtins.TInt, source.Span.missing), source.Span.missing), - ), Nil, source.Span.missing) - case _ => source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing) - }, source.Span.missing)) - } - def generateImplicitBlockArg(p: symbols.BlockParam)(using Context): Either[EffektMessages, source.Term] = - p.tpe.get match { - case BlockType.FunctionType(tparams, cparams, vparams, bparams, result, effects) => - val gtparams = tparams.map { p => source.IdDef(p.name.name, source.Span.missing) } - val gvparams: List[source.ValueParam] = - vparams.zipWithIndex.map { (p, i) => source.ValueParam(source.IdDef(s"arg${i}", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } - val gbparams: List[source.BlockParam] = - bparams.zipWithIndex.map { (p, i) => source.BlockParam(source.IdDef(s"block_arg${i}", source.Span.missing), Some(source.ReifiedType(p)), false, source.Span.missing) } - Right(source.BlockLiteral(gtparams, gvparams, gbparams, - source.Return(source.Call(source.IdTarget(source.IdRef(Nil, p.name.name, source.Span.missing)), Nil, - gvparams.map { x => source.ValueArg(None, source.Var(source.IdRef(Nil, x.id.name, source.Span.missing), source.Span.missing), source.Span.missing) }, - gbparams.map { x => source.Var(source.IdRef(Nil, x.id.name, source.Span.missing), source.Span.missing) }, - source.Span.missing), - source.Span.missing), source.Span.missing)) - case BlockType.InterfaceType(typeConstructor, args) => - // TODO eta-exapnd here, too ? - Right(source.Var(IdRef(Nil, p.name.name, source.Span.missing), source.Span.missing)) - } - - def lookupPotentialImplicits(forCandidates: List[Set[BlockSymbol]])(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.collect { case p if p.isImplicit => p -> generateImplicitValueArg(p) }.toMap, - c.bparams.collect { case p if p.isImplicit => p -> generateImplicitBlockArg(p) }.toMap) - foundImplicits.put((scope, b), r) - Some(b -> r) - case _ => None - } - } - } - }.toMap - } - // the last element in the path can also be the type of the name. def lookupOperation(path: List[String], name: String)(using ErrorReporter): List[Set[Operation]] = all(path, scope) { namespace => diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala index 5a5bb8a4b9..08624b265e 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala @@ -204,8 +204,8 @@ enum Binder extends TermSymbol { } export Binder.* -case class ImplicitContext(var values: Map[ValueParam, Either[EffektMessages, source.ValueArg]], - var blocks: Map[BlockParam, Either[EffektMessages, source.Term]]) { +case class ImplicitContext(var values: Map[Int, Either[EffektMessages, source.ValueArg]], + var blocks: Map[Int, Either[EffektMessages, source.Term]]) { var resolved = false // will be set in namer after the values in the maps are resolved } object ImplicitContext { From 8c944c1d93d6e6af22c1d71e26c18c120cd9e93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 23:23:04 +0200 Subject: [PATCH 19/46] Move implicit-related helpers from typer too the object, too --- .../shared/src/main/scala/effekt/Typer.scala | 86 ++----------------- .../effekt/source/GenerateImplicitArgs.scala | 84 +++++++++++++++++- 2 files changed, 89 insertions(+), 81 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 602aea4514..3979abeb99 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1450,81 +1450,6 @@ object Typer extends Phase[NameResolved, Typechecked] { } } - def instantiateImplicitBlock(b: source.Term, tpe: symbols.BlockType)(using Context): source.Term = { - if(!Context.messaging.hasErrors) { - (b, tpe) match { - case (a, symbols.BlockType.FunctionType(tps, cps, vps, bps, res, effs)) => - a match { - case source.BlockLiteral(tparams, vparams, bparams, source.Return(source.Call(fn, targs, vargs, bargs, _), _), _) => - // 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 = tparams.map { x => symbols.TypeParam(Name.local(x.name)) } - val ftparams = (tparams zip ftpsyms).map { (x, sym) => - val r = source.IdDef(x.name, source.Span.missing) - Context.annotate(Annotations.Symbol, r, sym) - r - } - val ftargs = ftpsyms.map { x => - val r = source.TypeRef(source.IdRef(Nil, x.name.name, source.Span.missing), Many(Nil, source.Span.missing), source.Span.missing) - Context.annotate(Annotations.Symbol, r, x) - r - } - val fvpsyms = (vparams zip vps).map { (x, t) => symbols.ValueParam(Name.local(x.id.name), Some(t), false, NoSource) } - val fvparams = (vparams zip fvpsyms).map { (x, sym) => - val r: source.ValueParam = source.ValueParam(source.IdDef(x.id.name, source.Span.missing), Some(source.ReifiedType(sym.tpe.get)), false, source.Span.missing) - Context.annotate(Annotations.Symbol, r, sym) - Context.annotate(Annotations.Symbol, r.id, sym) - r - } - val fvargs = fvpsyms.map { x => - val r = source.Var(source.IdRef(Nil, x.name.name, source.Span.missing), source.Span.missing) - Context.annotate(Annotations.Symbol, r, x) - Context.annotate(Annotations.Symbol, r.id, x) - source.ValueArg(None, r, source.Span.missing) - } - val fbpsyms = (bparams zip bps).map { (x, t) => symbols.BlockParam(Name.local(x.id.name), Some(t), x.symbol.capture, false, NoSource) } - val fbparams = (bparams zip fbpsyms).map { (x, sym) => - val r: source.BlockParam = source.BlockParam(source.IdDef(x.id.name, source.Span.missing), Some(source.ReifiedType(sym.tpe.get)), false, source.Span.missing) - Context.annotate(Annotations.Symbol, r, sym) - Context.annotate(Annotations.Symbol, r.id, sym) - r - } - val fbargs = fbpsyms.map { x => - val r = source.Var(source.IdRef(Nil, x.name.name, source.Span.missing), source.Span.missing) - Context.annotate(Annotations.Symbol, r, x) - Context.annotate(Annotations.Symbol, r.id, x) - r - } - 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 - }) - 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 _ => Context.panic("Unexpected implicit value for implicit block parameter") - } - case (a, symbols.BlockType.InterfaceType(tCons, tArgs)) => - // There is nothing to do here - a - } - } else { - Context.abort("Not instantiating implicit block argument since there are errors.") - } - } - def instantiateImplicitValue(v: source.ValueArg, tpe: symbols.ValueType)(using Context): source.ValueArg = { - // There is nothing to do here - v - } - def checkCallTo( call: source.CallLike, name: String, @@ -1607,9 +1532,10 @@ object Typer extends Phase[NameResolved, Typechecked] { 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) foreach { case (tpe, expr) => - // TODO NOW refresh and instantiate them - val inst = instantiateImplicitValue(expr, tpe) + val inst = source.GenerateImplicitArgs.instantiateImplicitValue(expr, tpe) instImplicitVargs.append(inst) val Result(t, eff) = checkExpr(inst.value, Some(tpe)) effs = effs ++ eff.toEffects @@ -1635,18 +1561,20 @@ 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)) foreach { case (tpe, (expr, capt)) => - // TODO NOW, refresh and instantiate before checking flowsInto(capt, callsite) // capture of block <: ?C flowingInto(capt) { - val inst = instantiateImplicitBlock(expr, tpe) + val inst = source.GenerateImplicitArgs.instantiateImplicitBlock(expr, tpe) instImplicitBargs.append(inst) val Result(t, eff) = 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 diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index dda3b36f84..fae08aff23 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -1,10 +1,13 @@ -package effekt.source +package effekt +package source + import scala.collection.mutable import effekt.util.messages.{ErrorReporter, EffektMessages} import effekt.context.Context import effekt.symbols import effekt.symbols.scopes.Scope -import effekt.symbols.{BlockSymbol, BlockType, Callable, ImplicitContext, builtins} +import effekt.symbols.{BlockSymbol, BlockType, Callable, ImplicitContext, builtins, Name} +import effekt.context.Annotations object GenerateImplicitArgs { @@ -75,4 +78,81 @@ object GenerateImplicitArgs { } }.toMap } + + + def instantiateImplicitBlock(b: source.Term, tpe: symbols.BlockType)(using Context): source.Term = { + if(!Context.messaging.hasErrors) { + (b, tpe) match { + case (a, symbols.BlockType.FunctionType(tps, cps, vps, bps, res, effs)) => + a match { + case source.BlockLiteral(tparams, vparams, bparams, source.Return(source.Call(fn, targs, vargs, bargs, _), _), _) => + // 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 = tparams.map { x => symbols.TypeParam(Name.local(x.name)) } + val ftparams = (tparams zip ftpsyms).map { (x, sym) => + val r = source.IdDef(x.name, source.Span.missing) + Context.annotate(Annotations.Symbol, r, sym) + r + } + val ftargs = ftpsyms.map { x => + val r = source.TypeRef(source.IdRef(Nil, x.name.name, source.Span.missing), Many(Nil, source.Span.missing), source.Span.missing) + Context.annotate(Annotations.Symbol, r, x) + r + } + val fvpsyms = (vparams zip vps).map { (x, t) => symbols.ValueParam(Name.local(x.id.name), Some(t), false, NoSource) } + val fvparams = (vparams zip fvpsyms).map { (x, sym) => + val r: source.ValueParam = source.ValueParam(source.IdDef(x.id.name, source.Span.missing), Some(source.ReifiedType(sym.tpe.get)), false, source.Span.missing) + Context.annotate(Annotations.Symbol, r, sym) + Context.annotate(Annotations.Symbol, r.id, sym) + r + } + val fvargs = fvpsyms.map { x => + val r = source.Var(source.IdRef(Nil, x.name.name, source.Span.missing), source.Span.missing) + Context.annotate(Annotations.Symbol, r, x) + Context.annotate(Annotations.Symbol, r.id, x) + source.ValueArg(None, r, source.Span.missing) + } + val fbpsyms = (bparams zip bps).map { (x, t) => symbols.BlockParam(Name.local(x.id.name), Some(t), x.symbol.capture, false, NoSource) } + val fbparams = (bparams zip fbpsyms).map { (x, sym) => + val r: source.BlockParam = source.BlockParam(source.IdDef(x.id.name, source.Span.missing), Some(source.ReifiedType(sym.tpe.get)), false, source.Span.missing) + Context.annotate(Annotations.Symbol, r, sym) + Context.annotate(Annotations.Symbol, r.id, sym) + r + } + val fbargs = fbpsyms.map { x => + val r = source.Var(source.IdRef(Nil, x.name.name, source.Span.missing), source.Span.missing) + Context.annotate(Annotations.Symbol, r, x) + Context.annotate(Annotations.Symbol, r.id, x) + r + } + 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 + }) + 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 _ => Context.panic("Unexpected implicit value for implicit block parameter") + } + case (a, symbols.BlockType.InterfaceType(tCons, tArgs)) => + // There is nothing to do here + a + } + } else { + Context.abort("Not instantiating implicit block argument since there are errors.") + } + } + def instantiateImplicitValue(v: source.ValueArg, tpe: symbols.ValueType)(using Context): source.ValueArg = { + // There is nothing to do here + v + } + } From b4dcaf3d115120f7a7f856c4b36da8c428c309cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Wed, 15 Apr 2026 23:38:43 +0200 Subject: [PATCH 20/46] Add some documentation --- .../shared/src/main/scala/effekt/Namer.scala | 6 +++ .../effekt/source/GenerateImplicitArgs.scala | 40 ++++++++++++++++--- .../src/main/scala/effekt/util/Messages.scala | 2 +- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index 323af9ea7d..76f6be5bb1 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -467,6 +467,12 @@ object Namer extends Phase[Parsed, NameResolved] { 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) => diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index fae08aff23..dc76cf4a80 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -11,9 +11,18 @@ import effekt.context.Annotations object GenerateImplicitArgs { - // for caching (to prevent infinite recursion here) + /** + * 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): Either[EffektMessages, ValueArg] = { Right(ValueArg(Some(p.name.name), p.name.name match { case "sourcePosition" => @@ -31,6 +40,11 @@ object GenerateImplicitArgs { }, 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): Either[EffektMessages, Term] = p.tpe.get match { case BlockType.FunctionType(tparams, cparams, vparams, bparams, result, effects) => @@ -50,6 +64,12 @@ object GenerateImplicitArgs { Right(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 => @@ -79,7 +99,11 @@ object GenerateImplicitArgs { }.toMap } - + /** + * 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: source.Term, tpe: symbols.BlockType)(using Context): source.Term = { if(!Context.messaging.hasErrors) { (b, tpe) match { @@ -143,16 +167,20 @@ object GenerateImplicitArgs { case _ => Context.panic("Unexpected implicit value for implicit block parameter") } case (a, symbols.BlockType.InterfaceType(tCons, tArgs)) => - // There is nothing to do here - a + a // TODO Is it a problem if this is used more than once? } } 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: source.ValueArg, tpe: symbols.ValueType)(using Context): source.ValueArg = { - // There is nothing to do here - v + v // TODO Is it a problem if this is used more than once? } } diff --git a/effekt/shared/src/main/scala/effekt/util/Messages.scala b/effekt/shared/src/main/scala/effekt/util/Messages.scala index 14f3ba19b9..6f716843df 100644 --- a/effekt/shared/src/main/scala/effekt/util/Messages.scala +++ b/effekt/shared/src/main/scala/effekt/util/Messages.scala @@ -169,4 +169,4 @@ object messages { } } } -} \ No newline at end of file +} From da15c205241e023350dcd4796bb5fe6a0c4bc929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Thu, 16 Apr 2026 11:15:03 +0200 Subject: [PATCH 21/46] Add example with return-type overloading --- examples/pos/name-based-implicits/default.check | 1 + examples/pos/name-based-implicits/default.effekt | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 examples/pos/name-based-implicits/default.check create mode 100644 examples/pos/name-based-implicits/default.effekt diff --git a/examples/pos/name-based-implicits/default.check b/examples/pos/name-based-implicits/default.check new file mode 100644 index 0000000000..c436174c29 --- /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 0000000000..5c40334a92 --- /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 From 83ab24a8aa5027fdae5183dc29d0bd3ab1aace1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Thu, 16 Apr 2026 11:22:40 +0200 Subject: [PATCH 22/46] Add type-tag example variant with return type overloading --- .../parametrized-type-tags.check | 1 + .../parametrized-type-tags.effekt | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 examples/pos/name-based-implicits/parametrized-type-tags.check create mode 100644 examples/pos/name-based-implicits/parametrized-type-tags.effekt 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 0000000000..d3fd265f8d --- /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 0000000000..69ad98188e --- /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 From 8b85f185c4639c06f4208b6920dab3302ca74cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Thu, 16 Apr 2026 11:26:23 +0200 Subject: [PATCH 23/46] Add example using implicit interface arguments --- .../name-based-implicits/implicit-interfaces.check | 1 + .../name-based-implicits/implicit-interfaces.effekt | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 examples/pos/name-based-implicits/implicit-interfaces.check create mode 100644 examples/pos/name-based-implicits/implicit-interfaces.effekt 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 0000000000..f70d7bba4a --- /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 0000000000..62965f98a5 --- /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 From 2cf8a169ef54ae72ce3e5eddf9cd0f4a5a48a4db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Thu, 16 Apr 2026 11:44:57 +0200 Subject: [PATCH 24/46] Slightly improve error messages for implicits --- effekt/shared/src/main/scala/effekt/Namer.scala | 2 +- effekt/shared/src/main/scala/effekt/Typer.scala | 4 ++-- .../scala/effekt/source/GenerateImplicitArgs.scala | 4 +++- .../shared/src/main/scala/effekt/symbols/symbols.scala | 6 ++++-- examples/neg/name-based-implicits/no-implicit.check | 10 ++++++++++ examples/neg/name-based-implicits/no-implicit.effekt | 9 +++++++++ 6 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 examples/neg/name-based-implicits/no-implicit.check create mode 100644 examples/neg/name-based-implicits/no-implicit.effekt diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index 76f6be5bb1..ff71083eb6 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -477,7 +477,7 @@ object Namer extends Phase[Parsed, NameResolved] { Context.symbolOf(id) match { case symbols.CallTarget(syms, cs) => cs.foreach { - case (b, c@ImplicitContext(vimpls, bimpls)) if !c.resolved => + case (b, c@ImplicitContext(vimpls, _, bimpls, _)) if !c.resolved => c.resolved = true // set as resolved first, so recursive calls do not continue doing so. val tVimpls = vimpls.map { case k -> mv => k -> (for { diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 3979abeb99..dfada3d691 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1474,7 +1474,7 @@ object Typer extends Phase[NameResolved, Typechecked] { implicitVargs.append(v) Some(v) case Left(msgs) => - Context.error(pretty"""Could not resolve implicit value parameter ${i}:""") + Context.error(pretty"""Could not resolve implicit value parameter ${potentialImplicits.valueNames(i)} (index ${i}).""") msgs.foreach(Context.report) None } @@ -1487,7 +1487,7 @@ object Typer extends Phase[NameResolved, Typechecked] { implicitBargs.append(b) Some(b) case Left(msgs) => - Context.error(pretty"""Could not resolve implicit block parameter ${i}:""") + Context.error(pretty"""Could not resolve implicit block parameter ${potentialImplicits.blockNames(i)} (index ${i}).""") msgs.foreach(Context.report) None } diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index dc76cf4a80..5ec3352538 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -89,7 +89,9 @@ object GenerateImplicitArgs { 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) + c.vparams.zipWithIndex.collect { case (p, i) if p.isImplicit => i -> p.name.name }.toMap, + c.bparams.zipWithIndex.collect { case (p, i) if p.isImplicit => i -> generateImplicitBlockArg(p) }.toMap, + c.bparams.zipWithIndex.collect { case (p, i) if p.isImplicit => i -> p.name.name }.toMap) foundImplicits.put((scope, b), r) Some(b -> r) case _ => None diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala index 08624b265e..4f330d17a9 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala @@ -205,11 +205,13 @@ enum Binder extends TermSymbol { export Binder.* case class ImplicitContext(var values: Map[Int, Either[EffektMessages, source.ValueArg]], - var blocks: Map[Int, Either[EffektMessages, source.Term]]) { + valueNames: Map[Int, String], + var blocks: Map[Int, Either[EffektMessages, source.Term]], + blockNames: Map[Int, String]) { 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) + val empty = ImplicitContext(Map.empty, Map.empty, Map.empty, Map.empty) empty.resolved = true } 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 0000000000..71aad5f029 --- /dev/null +++ b/examples/neg/name-based-implicits/no-implicit.check @@ -0,0 +1,10 @@ +[error] Cannot typecheck call. +There are multiple overloads, which all fail to check: + +Possible overload: no-implicit::foo of type String => Unit + Could not resolve implicit value parameter barbaz (index 0). + Could not resolve term barbaz + Wrong number of arguments to foo: expected 1 value argument, but got 0 value arguments + +Possible overload: no-implicit::foo of type {() => Unit} => Unit + Wrong number of arguments to foobaz: expected 1 value argument, but got 0 value arguments 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 0000000000..2ff4791103 --- /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 From 0f9a95736910caf03ffcd139c1c2acb9f3973c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Thu, 16 Apr 2026 13:39:33 +0200 Subject: [PATCH 25/46] Better error message location for implicits in block literals (disallowed) --- effekt/shared/src/main/scala/effekt/Namer.scala | 12 ++++++++---- .../neg/name-based-implicits/higher-order.effekt | 5 +++++ 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 examples/neg/name-based-implicits/higher-order.effekt diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index ff71083eb6..b5427c4d9a 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -456,10 +456,14 @@ object Namer extends Phase[Parsed, NameResolved] { */ @tailrec def checkImplicitParams(l: List[source.Param], implicitsAllowed: RequirementLevel = RequirementLevel.Optional)(using Context): Unit = (l, implicitsAllowed) match { - case ((source.ValueParam(_, _, true, _) | source.BlockParam(_, _, true, _)) :: tl, RequirementLevel.Forbidden) => - Context.error(pretty"Implicit parameter ${l.head.span.text.getOrElse(l.head.id.name)} can never be passed implicitly to here.") - case ((source.ValueParam(_, _, false, _) | source.BlockParam(_, _, false, _)) :: tl, RequirementLevel.Required) => - 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 ((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, _) => 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 0000000000..435fa2859f --- /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 From 1e59302a789bfb9b9cbfe42858e57852a067164b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Thu, 16 Apr 2026 16:13:33 +0200 Subject: [PATCH 26/46] Stop recursion if the types aren't getting smaller for multiple levels --- .../shared/src/main/scala/effekt/Typer.scala | 4 +- .../effekt/source/GenerateImplicitArgs.scala | 48 ++++++++++++++++++- .../pos/name-based-implicits/comparer.check | 1 + .../pos/name-based-implicits/comparer.effekt | 23 +++++++++ .../name-based-implicits/deep-recursive.check | 1 + .../deep-recursive.effekt | 10 ++++ 6 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 examples/pos/name-based-implicits/comparer.check create mode 100644 examples/pos/name-based-implicits/comparer.effekt create mode 100644 examples/pos/name-based-implicits/deep-recursive.check create mode 100644 examples/pos/name-based-implicits/deep-recursive.effekt diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index dfada3d691..9ccd5a3671 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1569,7 +1569,9 @@ object Typer extends Phase[NameResolved, Typechecked] { flowingInto(capt) { val inst = source.GenerateImplicitArgs.instantiateImplicitBlock(expr, tpe) instImplicitBargs.append(inst) - val Result(t, eff) = checkExprAsBlock(inst, Some(tpe)) + val Result(t, eff) = source.GenerateImplicitArgs.recursionGuard(inst, tpe){ () => + checkExprAsBlock(inst, Some(tpe)) + } effs = effs ++ eff.toEffects } } diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index 5ec3352538..2bc4da2a4c 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -2,15 +2,61 @@ package effekt package source import scala.collection.mutable +import scala.util.DynamicVariable + import effekt.util.messages.{ErrorReporter, EffektMessages} import effekt.context.Context -import effekt.symbols import effekt.symbols.scopes.Scope import effekt.symbols.{BlockSymbol, BlockType, Callable, ImplicitContext, builtins, Name} import effekt.context.Annotations object GenerateImplicitArgs { + import effekt.symbols.ValueType + + 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](inst: source.Term, tpe: symbols.BlockType)(body: () => R)(using Context): R = { + val instBlockTpe = Context.unification(tpe) + val tpeSize = typeSize(instBlockTpe) + 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) { + body() + } + } + /** * Map for caching the result of [[lookupPotentialImplicits]] (to prevent infinite recursion in Namer) */ diff --git a/examples/pos/name-based-implicits/comparer.check b/examples/pos/name-based-implicits/comparer.check new file mode 100644 index 0000000000..1fc1f84c2e --- /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 0000000000..7686027a18 --- /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 = 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 0000000000..a0aba9318a --- /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 0000000000..6f07de5799 --- /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 From 9e756b534a09da35619399eccef252183da365b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Thu, 16 Apr 2026 16:26:05 +0200 Subject: [PATCH 27/46] Add type annotation to make the example work --- examples/pos/name-based-implicits/comparer.effekt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pos/name-based-implicits/comparer.effekt b/examples/pos/name-based-implicits/comparer.effekt index 7686027a18..0e279706b2 100644 --- a/examples/pos/name-based-implicits/comparer.effekt +++ b/examples/pos/name-based-implicits/comparer.effekt @@ -17,7 +17,7 @@ def myComparer[A](){ ?myComparer: => ((A, A) => Ordering at {})}: (List[A], List } def main() = { - val cmp = myComparer() + 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 From 8d968bbfcb6738f758429e18811c4d7468c7045a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Thu, 16 Apr 2026 16:52:30 +0200 Subject: [PATCH 28/46] Make it more obvious we are only interested in side effects and errors from resolve --- effekt/shared/src/main/scala/effekt/Namer.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index b5427c4d9a..a327a3e134 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -486,13 +486,13 @@ object Namer extends Phase[Parsed, NameResolved] { val tVimpls = vimpls.map { case k -> mv => k -> (for { v <- mv - r <- Try { resolve(v) } + () <- Try { resolve(v) } } yield v) } val tBimpls = bimpls.map { case k -> mb => k -> (for { b <- mb - r <- Try { resolve(b) } + () <- Try { resolve(b) } } yield b) } c.values = tVimpls From ba63f67884e3e72abfa25fff942f960157e1c082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Thu, 16 Apr 2026 16:56:24 +0200 Subject: [PATCH 29/46] Can't use () so use _ --- effekt/shared/src/main/scala/effekt/Namer.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index a327a3e134..ee888233f8 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -486,13 +486,13 @@ object Namer extends Phase[Parsed, NameResolved] { val tVimpls = vimpls.map { case k -> mv => k -> (for { v <- mv - () <- Try { resolve(v) } + _ <- Try { resolve(v) } } yield v) } val tBimpls = bimpls.map { case k -> mb => k -> (for { b <- mb - () <- Try { resolve(b) } + _ <- Try { resolve(b) } } yield b) } c.values = tVimpls From ba3ea3e5e0f3a814c358e589720f4b2647506d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Fri, 17 Apr 2026 11:12:37 +0200 Subject: [PATCH 30/46] Refactor: Use type for incomplete stencils in ImplicitContext, simplify --- .../shared/src/main/scala/effekt/Namer.scala | 18 +-- .../shared/src/main/scala/effekt/Typer.scala | 40 ++---- .../effekt/source/GenerateImplicitArgs.scala | 119 +++++++++++++----- .../main/scala/effekt/symbols/symbols.scala | 24 +++- .../name-based-implicits/no-implicit.check | 2 +- .../pos/name-based-implicits/callid.check | 1 + .../pos/name-based-implicits/callid.effekt | 5 + .../source-positions.check | 2 +- 8 files changed, 126 insertions(+), 85 deletions(-) create mode 100644 examples/pos/name-based-implicits/callid.check create mode 100644 examples/pos/name-based-implicits/callid.effekt diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index ee888233f8..bb4d44e9cb 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -481,22 +481,10 @@ object Namer extends Phase[Parsed, NameResolved] { Context.symbolOf(id) match { case symbols.CallTarget(syms, cs) => cs.foreach { - case (b, c@ImplicitContext(vimpls, _, bimpls, _)) if !c.resolved => + case (b, c@ImplicitContext(vimpls, bimpls)) if !c.resolved => c.resolved = true // set as resolved first, so recursive calls do not continue doing so. - val tVimpls = vimpls.map { case k -> mv => - k -> (for { - v <- mv - _ <- Try { resolve(v) } - } yield v) - } - val tBimpls = bimpls.map { case k -> mb => - k -> (for { - b <- mb - _ <- Try { resolve(b) } - } yield b) - } - c.values = tVimpls - c.blocks = tBimpls + 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 _ => () diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 9ccd5a3671..4106812942 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1328,10 +1328,10 @@ object Typer extends Phase[NameResolved, Typechecked] { object Aligned { extension[A,B](self: Aligned[A, B]) - def fillImplicit(by: (B, Int) => Option[A]): 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) + (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 @@ -1357,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 | symbols.ImplicitContext.ImplicitStencil[_], TypeParam], + values: Aligned[source.ValueParam | source.ValueArg | symbols.ImplicitContext.ImplicitStencil[_], ValueType], + blocks: Aligned[source.BlockParam | source.Term | symbols.ImplicitContext.ImplicitStencil[_], BlockType] )(using Context): Unit = { // Type args are OK iff nothing provided or perfectly aligned @@ -1465,34 +1465,8 @@ object Typer extends Phase[NameResolved, Typechecked] { // (0) Check that arg & param counts align val atargs = Aligned(targs, funTpe.tparams) - val implicitVargs: mutable.ListBuffer[source.ValueArg] = mutable.ListBuffer.empty - val implicitBargs: mutable.ListBuffer[source.Term] = mutable.ListBuffer.empty - val avargs = Aligned(vargs, funTpe.vparams).fillImplicit { - case (e, i) if potentialImplicits.values.contains(i) => - potentialImplicits.values(i) match { - case Right(v) => - implicitVargs.append(v) - Some(v) - case Left(msgs) => - Context.error(pretty"""Could not resolve implicit value parameter ${potentialImplicits.valueNames(i)} (index ${i}).""") - msgs.foreach(Context.report) - None - } - case _ => None - } - val abargs = Aligned(bargs, funTpe.bparams).fillImplicit { - case (_, i) if potentialImplicits.blocks.contains(i) => - potentialImplicits.blocks(i) match { - case Right(b) => - implicitBargs.append(b) - Some(b) - case Left(msgs) => - Context.error(pretty"""Could not resolve implicit block parameter ${potentialImplicits.blockNames(i)} (index ${i}).""") - msgs.foreach(Context.report) - None - } - case _ => None - } + 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 diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index 2bc4da2a4c..5395ff0326 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -62,6 +62,8 @@ object GenerateImplicitArgs { */ val foundImplicits: mutable.HashMap[(Scope, BlockSymbol), ImplicitContext] = mutable.HashMap.empty + private var nextCallId: Long = 0 + /** * Generate source for the given implicit value parameter, matching on the name. * @@ -69,21 +71,21 @@ object GenerateImplicitArgs { * 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): Either[EffektMessages, ValueArg] = { - Right(ValueArg(Some(p.name.name), p.name.name match { + def generateImplicitValueArg(p: symbols.ValueParam)(using Context): ImplicitContext.ImplicitStencil[Term] = { + p.name.name match { case "sourcePosition" => - val pos = Context.focus.span - val from = pos.source.offsetToPosition(pos.from) - val to = pos.source.offsetToPosition(pos.to) - Call(IdTarget(IdRef(Nil, "SourcePosition", Span.missing)), Nil, List( - ValueArg(None, Literal(pos.source.name, builtins.TString, Span.missing), Span.missing), - ValueArg(None, Literal(from.line.toLong, builtins.TInt, Span.missing), Span.missing), - ValueArg(None, Literal(from.column.toLong, builtins.TInt, Span.missing), Span.missing), - ValueArg(None, Literal(to.line.toLong, builtins.TInt, Span.missing), Span.missing), - ValueArg(None, Literal(to.column.toLong, builtins.TInt, Span.missing), Span.missing), - ), Nil, Span.missing) - case _ => Var(IdRef(Nil, p.name.name, Span.missing), Span.missing) - }, Span.missing)) + // This generates a dummy source to be name-resolved (the actual arguments will be generated later) + ImplicitContext.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" => + ImplicitContext.CallId() + case _ => ImplicitContext.ImplicitVar(p.name.name, Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) + } } /** @@ -91,7 +93,7 @@ object GenerateImplicitArgs { * * 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): Either[EffektMessages, Term] = + def generateImplicitBlockArg(p: symbols.BlockParam)(using Context): ImplicitContext.ImplicitStencil[Term] = p.tpe.get match { case BlockType.FunctionType(tparams, cparams, vparams, bparams, result, effects) => val gtparams = tparams.map { p => IdDef(p.name.name, Span.missing) } @@ -99,7 +101,7 @@ object GenerateImplicitArgs { vparams.zipWithIndex.map { (p, i) => ValueParam(IdDef(s"arg${i}", Span.missing), Some(ReifiedType(p)), false, Span.missing) } val gbparams: List[BlockParam] = bparams.zipWithIndex.map { (p, i) => BlockParam(IdDef(s"block_arg${i}", Span.missing), Some(ReifiedType(p)), false, Span.missing) } - Right(BlockLiteral(gtparams, gvparams, gbparams, + ImplicitContext.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) }, @@ -107,7 +109,7 @@ object GenerateImplicitArgs { Span.missing), Span.missing)) case BlockType.InterfaceType(typeConstructor, args) => // TODO eta-exapnd here, too ? - Right(Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) + ImplicitContext.ImplicitVar(p.name.name, Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) } /** @@ -135,9 +137,7 @@ object GenerateImplicitArgs { case c: Callable => val r = ImplicitContext( c.vparams.zipWithIndex.collect { case (p, i) if p.isImplicit => i -> generateImplicitValueArg(p) }.toMap, - c.vparams.zipWithIndex.collect { case (p, i) if p.isImplicit => i -> p.name.name }.toMap, - c.bparams.zipWithIndex.collect { case (p, i) if p.isImplicit => i -> generateImplicitBlockArg(p) }.toMap, - c.bparams.zipWithIndex.collect { case (p, i) if p.isImplicit => i -> p.name.name }.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 @@ -152,12 +152,16 @@ object GenerateImplicitArgs { * * Also annotates all symbols for the returned code correctly where necessary. */ - def instantiateImplicitBlock(b: source.Term, tpe: symbols.BlockType)(using Context): source.Term = { + def instantiateImplicitBlock(b: ImplicitContext.ImplicitStencil[Term], tpe: symbols.BlockType)(using Context): source.Term = { if(!Context.messaging.hasErrors) { (b, tpe) match { - case (a, symbols.BlockType.FunctionType(tps, cps, vps, bps, res, effs)) => - a match { - case source.BlockLiteral(tparams, vparams, bparams, source.Return(source.Call(fn, targs, vargs, bargs, _), _), _) => + case (ImplicitContext.Error(_, pretty, msgs), _) => + Context.error(s"""Could not resolve implicit block parameter ${pretty}.""") + msgs.foreach(Context.report) + Context.abort("Aborting due to previous errors.") + + case (ImplicitContext.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. @@ -212,10 +216,12 @@ object GenerateImplicitArgs { source.BlockLiteral(ftparams, fvparams, fbparams, source.Return(source.Call(ffn, ftargs, fvargs, fbargs, source.Span.missing), source.Span.missing), source.Span.missing) - case _ => Context.panic("Unexpected implicit value for implicit block parameter") - } - case (a, symbols.BlockType.InterfaceType(tCons, tArgs)) => - a // TODO Is it a problem if this is used more than once? + + case (ImplicitContext.ImplicitVar(name, b), _) => + b // TODO Is it a problem if this is used more than once? + + case _ => + Context.panic("Unexpected type for implicit stencil.") } } else { Context.abort("Not instantiating implicit block argument since there are errors.") @@ -227,8 +233,61 @@ object GenerateImplicitArgs { * * Also annotates all symbols for the returned code correctly where necessary. */ - def instantiateImplicitValue(v: source.ValueArg, tpe: symbols.ValueType)(using Context): source.ValueArg = { - v // TODO Is it a problem if this is used more than once? + def instantiateImplicitValue(v: ImplicitContext.ImplicitStencil[Term], tpe: symbols.ValueType)(using Context): source.ValueArg = { + v match { + case ImplicitContext.Error(_, pretty, msgs) => + Context.error(s"""Could not resolve implicit value parameter ${pretty}.""") + msgs.foreach(Context.report) + Context.abort("Aborting due to previous errors.") + + case ImplicitContext.ImplicitVar(name, content) => + source.ValueArg(Some(name), content, Span.missing) // TODO Is it a problem if this is used more than once? + + case ImplicitContext.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), + ValueArg(None, Literal(from.line.toLong, builtins.TInt, Span.missing), Span.missing), + ValueArg(None, Literal(from.column.toLong, builtins.TInt, Span.missing), Span.missing), + ValueArg(None, Literal(to.line.toLong, builtins.TInt, Span.missing), Span.missing), + ValueArg(None, Literal(to.column.toLong, builtins.TInt, Span.missing), Span.missing), + ), Nil, Span.missing) + + // copying over the annotations generated by Namer. + val (x: IdTarget, y: IdTarget) = (content.target, code.target): @unchecked + Context.copyAnnotations(x, y) + Context.copyAnnotations(x.id, y.id) + + // and returns the result + source.ValueArg(Some(v.name), code, Span.missing) + + case ImplicitContext.CallId() => + val id = nextCallId + nextCallId = nextCallId + 1 + source.ValueArg(Some(v.name), Literal(id, builtins.TInt, Span.missing), Span.missing) + + case ImplicitContext.ImplicitBlockLiteral(_, _) => Context.panic("Cannot instantiate block literal as an implicit value argument.") + } + } + + /** + * Run body on each of the stencil code parts, generating an Error context if errors occur. + */ + def runPhaseOn[A](i: Int, s: ImplicitContext.ImplicitStencil[A])(body: A => Either[EffektMessages, Unit]): ImplicitContext.ImplicitStencil[A] = { + s match { + case ImplicitContext.ImplicitBlockLiteral(name, content) => body(content) + case ImplicitContext.ImplicitVar(name, content) => body(content) + case ImplicitContext.SourcePosition(content) => body(content) + case ImplicitContext.CallId() => Right(()) + case ImplicitContext.Error(name, pretty, msgs) => Right(()) + } match { + case Left(msgs) => + ImplicitContext.Error(s.name, s"${s.pretty} (index ${i})", msgs) + case Right(()) => s + } } } diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala index 4f330d17a9..4efa62d5fa 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala @@ -204,15 +204,29 @@ enum Binder extends TermSymbol { } export Binder.* -case class ImplicitContext(var values: Map[Int, Either[EffektMessages, source.ValueArg]], - valueNames: Map[Int, String], - var blocks: Map[Int, Either[EffektMessages, source.Term]], - blockNames: Map[Int, String]) { +case class ImplicitContext(var values: Map[Int, ImplicitContext.ImplicitStencil[source.Term]], + var blocks: Map[Int, ImplicitContext.ImplicitStencil[source.Term]]) { 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, Map.empty, Map.empty) + val empty = ImplicitContext(Map.empty, Map.empty) empty.resolved = true + + sealed trait ImplicitStencil[+A] { + def name: String + def pretty: String = s"${name}" + } + case class ImplicitBlockLiteral(name: String, content: source.BlockLiteral) extends ImplicitStencil[source.BlockLiteral] + case class ImplicitVar(name: String, content: source.Var) extends ImplicitStencil[source.Var] + case class SourcePosition(content: source.Call) extends ImplicitStencil[source.Call] { + def name = "sourcePosition" + override def pretty = s"${name} as a call to SourcePosition" + } + case class CallId() extends ImplicitStencil[source.Literal] { + def name = "callId" + override def pretty = s"${name} as a fresh literal int id" + } + case class Error(name: String, override val pretty: String, msgs: EffektMessages) extends ImplicitStencil[Nothing] } /** diff --git a/examples/neg/name-based-implicits/no-implicit.check b/examples/neg/name-based-implicits/no-implicit.check index 71aad5f029..fd125773d8 100644 --- a/examples/neg/name-based-implicits/no-implicit.check +++ b/examples/neg/name-based-implicits/no-implicit.check @@ -4,7 +4,7 @@ There are multiple overloads, which all fail to check: Possible overload: no-implicit::foo of type String => Unit Could not resolve implicit value parameter barbaz (index 0). Could not resolve term barbaz - Wrong number of arguments to foo: expected 1 value argument, but got 0 value arguments + Aborting due to previous errors. Possible overload: no-implicit::foo of type {() => Unit} => Unit Wrong number of arguments to foobaz: expected 1 value argument, but got 0 value arguments diff --git a/examples/pos/name-based-implicits/callid.check b/examples/pos/name-based-implicits/callid.check new file mode 100644 index 0000000000..02e4a84d62 --- /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 0000000000..4240993792 --- /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/source-positions.check b/examples/pos/name-based-implicits/source-positions.check index 77b9f33381..01aff2b264 100644 --- a/examples/pos/name-based-implicits/source-positions.check +++ b/examples/pos/name-based-implicits/source-positions.check @@ -1 +1 @@ -examples/pos/name-based-implicits/source-positions.effekt:10:3-10:6 \ No newline at end of file +examples/pos/name-based-implicits/source-positions.effekt:10:3-10:8 \ No newline at end of file From 31e1ef583ab400bc8cf40d31a5acc35410da4984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Fri, 17 Apr 2026 12:04:55 +0200 Subject: [PATCH 31/46] Generate nicer (nested) errors for implicit instantiation --- .../effekt/source/GenerateImplicitArgs.scala | 20 +++++++++++-------- .../scala/effekt/util/ColoredMessaging.scala | 5 +++++ .../src/main/scala/effekt/util/Messages.scala | 1 + .../name-based-implicits/no-implicit.check | 8 ++++---- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index 5395ff0326..0c7f2b0aac 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -155,10 +155,9 @@ object GenerateImplicitArgs { def instantiateImplicitBlock(b: ImplicitContext.ImplicitStencil[Term], tpe: symbols.BlockType)(using Context): source.Term = { if(!Context.messaging.hasErrors) { (b, tpe) match { - case (ImplicitContext.Error(_, pretty, msgs), _) => - Context.error(s"""Could not resolve implicit block parameter ${pretty}.""") - msgs.foreach(Context.report) - Context.abort("Aborting due to previous errors.") + case (e @ ImplicitContext.Error(_, pretty, msgs), _) => + Context.abort(util.messages.ImplicitInstantiationError( + "block argument", pretty, msgs, Context.rangeOf(Context.focus))) case (ImplicitContext.ImplicitBlockLiteral(name, source.BlockLiteral(tparams, vparams, bparams, source.Return(source.Call(fn, targs, vargs, bargs, _), _), _)), symbols.BlockType.FunctionType(tps, cps, vps, bps, res, effs)) => @@ -236,9 +235,8 @@ object GenerateImplicitArgs { def instantiateImplicitValue(v: ImplicitContext.ImplicitStencil[Term], tpe: symbols.ValueType)(using Context): source.ValueArg = { v match { case ImplicitContext.Error(_, pretty, msgs) => - Context.error(s"""Could not resolve implicit value parameter ${pretty}.""") - msgs.foreach(Context.report) - Context.abort("Aborting due to previous errors.") + Context.abort(util.messages.ImplicitInstantiationError( + "value argument", pretty, msgs, Context.rangeOf(Context.focus))) case ImplicitContext.ImplicitVar(name, content) => source.ValueArg(Some(name), content, Span.missing) // TODO Is it a problem if this is used more than once? @@ -285,7 +283,13 @@ object GenerateImplicitArgs { case ImplicitContext.Error(name, pretty, msgs) => Right(()) } match { case Left(msgs) => - ImplicitContext.Error(s.name, s"${s.pretty} (index ${i})", msgs) + def formatIndex(i: Int): String = i match { + case 0 => "1st" + case 1 => "2nd" + case 2 => "3rd" + case n => s"${n + 1}th" + } + ImplicitContext.Error(s.name, s"${s.pretty} (${formatIndex(i)} argument)", msgs) case Right(()) => s } } diff --git a/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala b/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala index 5500bbf295..cd7f9b1a14 100644 --- a/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala +++ b/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala @@ -96,6 +96,11 @@ trait ColoredMessaging extends EffektMessaging { } mainMessage + "\n\n" + explanations.mkString("\n\n") + "\n" + case ImplicitInstantiationError(kind, prettyName, msgs, range) => + val mainMessage = underlined(s"Cannot instantiate implicit ${kind} ${prettyName}.") + val nestedErrors = indent(msgs.map { msg => formatContent(msg) }.mkString("\n")) + + "\n" + mainMessage + "\n" + nestedErrors + "\n" } def fullname(n: Name): String = n match { diff --git a/effekt/shared/src/main/scala/effekt/util/Messages.scala b/effekt/shared/src/main/scala/effekt/util/Messages.scala index 6f716843df..f0d07051f7 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(kind: String, prettyName: String, msgs: EffektMessages, 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/examples/neg/name-based-implicits/no-implicit.check b/examples/neg/name-based-implicits/no-implicit.check index fd125773d8..9285105327 100644 --- a/examples/neg/name-based-implicits/no-implicit.check +++ b/examples/neg/name-based-implicits/no-implicit.check @@ -2,9 +2,9 @@ There are multiple overloads, which all fail to check: Possible overload: no-implicit::foo of type String => Unit - Could not resolve implicit value parameter barbaz (index 0). - Could not resolve term barbaz - Aborting due to previous errors. + + Cannot instantiate implicit value argument barbaz (1st argument). + Could not resolve term barbaz Possible overload: no-implicit::foo of type {() => Unit} => Unit - Wrong number of arguments to foobaz: expected 1 value argument, but got 0 value arguments + Wrong number of arguments to foobaz: expected 1 value argument, but got 0 value arguments \ No newline at end of file From db261c2f0e40d72fb368cbd076dc3312f94fb881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Fri, 17 Apr 2026 13:20:05 +0200 Subject: [PATCH 32/46] Further improve error message (include type) --- .../effekt/source/GenerateImplicitArgs.scala | 18 ++++++------------ .../main/scala/effekt/symbols/symbols.scala | 2 +- .../scala/effekt/util/ColoredMessaging.scala | 12 ++++++++++-- .../src/main/scala/effekt/util/Messages.scala | 2 +- .../neg/name-based-implicits/no-implicit.check | 2 +- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index 0c7f2b0aac..a40bd71ed5 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -155,9 +155,9 @@ object GenerateImplicitArgs { def instantiateImplicitBlock(b: ImplicitContext.ImplicitStencil[Term], tpe: symbols.BlockType)(using Context): source.Term = { if(!Context.messaging.hasErrors) { (b, tpe) match { - case (e @ ImplicitContext.Error(_, pretty, msgs), _) => + case (e @ ImplicitContext.Error(_, pretty, i, msgs), _) => Context.abort(util.messages.ImplicitInstantiationError( - "block argument", pretty, msgs, Context.rangeOf(Context.focus))) + "block argument", pretty, i, tpe, msgs, Context.rangeOf(Context.focus))) case (ImplicitContext.ImplicitBlockLiteral(name, source.BlockLiteral(tparams, vparams, bparams, source.Return(source.Call(fn, targs, vargs, bargs, _), _), _)), symbols.BlockType.FunctionType(tps, cps, vps, bps, res, effs)) => @@ -234,9 +234,9 @@ object GenerateImplicitArgs { */ def instantiateImplicitValue(v: ImplicitContext.ImplicitStencil[Term], tpe: symbols.ValueType)(using Context): source.ValueArg = { v match { - case ImplicitContext.Error(_, pretty, msgs) => + case ImplicitContext.Error(_, pretty, i, msgs) => Context.abort(util.messages.ImplicitInstantiationError( - "value argument", pretty, msgs, Context.rangeOf(Context.focus))) + "value argument", pretty, i, tpe, msgs, Context.rangeOf(Context.focus))) case ImplicitContext.ImplicitVar(name, content) => source.ValueArg(Some(name), content, Span.missing) // TODO Is it a problem if this is used more than once? @@ -280,16 +280,10 @@ object GenerateImplicitArgs { case ImplicitContext.ImplicitVar(name, content) => body(content) case ImplicitContext.SourcePosition(content) => body(content) case ImplicitContext.CallId() => Right(()) - case ImplicitContext.Error(name, pretty, msgs) => Right(()) + case ImplicitContext.Error(name, pretty, i, msgs) => Right(()) } match { case Left(msgs) => - def formatIndex(i: Int): String = i match { - case 0 => "1st" - case 1 => "2nd" - case 2 => "3rd" - case n => s"${n + 1}th" - } - ImplicitContext.Error(s.name, s"${s.pretty} (${formatIndex(i)} argument)", msgs) + ImplicitContext.Error(s.name, s"${s.pretty}", Some(i), msgs) case Right(()) => s } } diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala index 4efa62d5fa..4558e00443 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala @@ -226,7 +226,7 @@ object ImplicitContext { def name = "callId" override def pretty = s"${name} as a fresh literal int id" } - case class Error(name: String, override val pretty: String, msgs: EffektMessages) extends ImplicitStencil[Nothing] + case class Error(name: String, override val pretty: String, index: Option[Int], msgs: EffektMessages) extends ImplicitStencil[Nothing] } /** diff --git a/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala b/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala index cd7f9b1a14..83c37af4bc 100644 --- a/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala +++ b/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala @@ -96,8 +96,16 @@ trait ColoredMessaging extends EffektMessaging { } mainMessage + "\n\n" + explanations.mkString("\n\n") + "\n" - case ImplicitInstantiationError(kind, prettyName, msgs, range) => - val mainMessage = underlined(s"Cannot instantiate implicit ${kind} ${prettyName}.") + case ImplicitInstantiationError(kind, prettyName, index, tpe, msgs, range) => + def formatIndex(i: Int): String = i match { + case 0 => "1st" + case 1 => "2nd" + case 2 => "3rd" + case n => s"${n + 1}th" + } + val prettyIndex = index.map(formatIndex).getOrElse("the") + val prettyTpe = TypePrinter.show(tpe) + val mainMessage = underlined(s"Cannot implicitly instantiate ${prettyIndex} ${kind} ${prettyName} of type ${prettyTpe}.") val nestedErrors = indent(msgs.map { msg => formatContent(msg) }.mkString("\n")) "\n" + mainMessage + "\n" + nestedErrors + "\n" diff --git a/effekt/shared/src/main/scala/effekt/util/Messages.scala b/effekt/shared/src/main/scala/effekt/util/Messages.scala index f0d07051f7..a8126ecc5a 100644 --- a/effekt/shared/src/main/scala/effekt/util/Messages.scala +++ b/effekt/shared/src/main/scala/effekt/util/Messages.scala @@ -13,7 +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(kind: String, prettyName: String, msgs: EffektMessages, range: Option[Range]) extends EffektError { val severity = Error } + case class ImplicitInstantiationError(kind: String, prettyName: String, index: Option[Int], tpe: symbols.Type, msgs: EffektMessages, 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/examples/neg/name-based-implicits/no-implicit.check b/examples/neg/name-based-implicits/no-implicit.check index 9285105327..c68da7a3d9 100644 --- a/examples/neg/name-based-implicits/no-implicit.check +++ b/examples/neg/name-based-implicits/no-implicit.check @@ -3,7 +3,7 @@ There are multiple overloads, which all fail to check: Possible overload: no-implicit::foo of type String => Unit - Cannot instantiate implicit value argument barbaz (1st argument). + Cannot implicitly instantiate 1st value argument barbaz of type String. Could not resolve term barbaz Possible overload: no-implicit::foo of type {() => Unit} => Unit From 233aed44186e5282cd63d4c45bc96bb57c2f1fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Fri, 17 Apr 2026 14:05:29 +0200 Subject: [PATCH 33/46] Even better error messages, now with explanations for special cases --- .../shared/src/main/scala/effekt/Typer.scala | 10 ++-- .../effekt/source/GenerateImplicitArgs.scala | 46 +++++++++++++------ .../main/scala/effekt/symbols/symbols.scala | 26 ++++++++--- .../scala/effekt/util/ColoredMessaging.scala | 15 ++++-- .../src/main/scala/effekt/util/Messages.scala | 2 +- .../type-error-call-id.check | 7 +++ .../type-error-call-id.effekt | 6 +++ .../type-error-no-overload.check | 3 ++ .../type-error-no-overload.effekt | 8 ++++ .../neg/name-based-implicits/type-error.check | 12 +++++ .../name-based-implicits/type-error.effekt | 9 ++++ 11 files changed, 113 insertions(+), 31 deletions(-) create mode 100644 examples/neg/name-based-implicits/type-error-call-id.check create mode 100644 examples/neg/name-based-implicits/type-error-call-id.effekt create mode 100644 examples/neg/name-based-implicits/type-error-no-overload.check create mode 100644 examples/neg/name-based-implicits/type-error-no-overload.effekt create mode 100644 examples/neg/name-based-implicits/type-error.check create mode 100644 examples/neg/name-based-implicits/type-error.effekt diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 4106812942..f0f4c86481 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1508,10 +1508,12 @@ object Typer extends Phase[NameResolved, Typechecked] { // implicit arguments work like normal ones, except that we first have to instantiate them, // and later annotate them to be inserted - (implicitVps zip implicitVargs) foreach { case (tpe, expr) => + (implicitVps zip implicitVargs).zipWithIndex foreach { case ((tpe, expr), ii) => val inst = source.GenerateImplicitArgs.instantiateImplicitValue(expr, tpe) instImplicitVargs.append(inst) - val Result(t, eff) = checkExpr(inst.value, Some(tpe)) + val Result(t, eff) = source.GenerateImplicitArgs.recursionGuard(expr, "value argument", vargs.length + ii, inst, tpe) { + checkExpr(inst.value, Some(tpe)) + } effs = effs ++ eff.toEffects } @@ -1537,13 +1539,13 @@ 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)) foreach { case (tpe, (expr, capt)) => + (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(inst, tpe){ () => + val Result(t, eff) = source.GenerateImplicitArgs.recursionGuard(expr, "block argument", bargs.length + ii, inst, tpe){ checkExprAsBlock(inst, Some(tpe)) } effs = effs ++ eff.toEffects diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index a40bd71ed5..2f6f4218f2 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -9,11 +9,16 @@ import effekt.context.Context import effekt.symbols.scopes.Scope import effekt.symbols.{BlockSymbol, BlockType, Callable, ImplicitContext, builtins, Name} import effekt.context.Annotations +import effekt.context.Try object GenerateImplicitArgs { import effekt.symbols.ValueType + 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) @@ -40,9 +45,12 @@ object GenerateImplicitArgs { * Wrapper for recursive type-checking of generated implicits. * Should fail for infinite recursion. */ - def recursionGuard[R](inst: source.Term, tpe: symbols.BlockType)(body: () => R)(using Context): R = { - val instBlockTpe = Context.unification(tpe) - val tpeSize = typeSize(instBlockTpe) + def recursionGuard[R](stencil: ImplicitContext.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)) @@ -53,7 +61,15 @@ object GenerateImplicitArgs { case _ => recursionStack.value } recursionStack.withValue(newValue) { - body() + Try { + body + } match { + case Left(msgs) => + // Note: Recursion + Context.abort(util.messages.ImplicitInstantiationError( + ImplicitContext.Error(stencil, None, msgs), tpe, Context.rangeOf(Context.focus))) + case Right(r) => r + } } } @@ -84,7 +100,7 @@ object GenerateImplicitArgs { ), Nil, Span.missing)) case "callId" => ImplicitContext.CallId() - case _ => ImplicitContext.ImplicitVar(p.name.name, Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) + case _ => ImplicitContext.ImplicitVar("value argument", p.name.name, Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) } } @@ -109,7 +125,7 @@ object GenerateImplicitArgs { Span.missing), Span.missing)) case BlockType.InterfaceType(typeConstructor, args) => // TODO eta-exapnd here, too ? - ImplicitContext.ImplicitVar(p.name.name, Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) + ImplicitContext.ImplicitVar("block argument", p.name.name, Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) } /** @@ -155,9 +171,9 @@ object GenerateImplicitArgs { def instantiateImplicitBlock(b: ImplicitContext.ImplicitStencil[Term], tpe: symbols.BlockType)(using Context): source.Term = { if(!Context.messaging.hasErrors) { (b, tpe) match { - case (e @ ImplicitContext.Error(_, pretty, i, msgs), _) => + case (e @ ImplicitContext.Error(s, i, msgs), _) => Context.abort(util.messages.ImplicitInstantiationError( - "block argument", pretty, i, tpe, msgs, Context.rangeOf(Context.focus))) + e, tpe, Context.rangeOf(Context.focus))) case (ImplicitContext.ImplicitBlockLiteral(name, source.BlockLiteral(tparams, vparams, bparams, source.Return(source.Call(fn, targs, vargs, bargs, _), _), _)), symbols.BlockType.FunctionType(tps, cps, vps, bps, res, effs)) => @@ -216,7 +232,7 @@ object GenerateImplicitArgs { source.Return(source.Call(ffn, ftargs, fvargs, fbargs, source.Span.missing), source.Span.missing), source.Span.missing) - case (ImplicitContext.ImplicitVar(name, b), _) => + case (ImplicitContext.ImplicitVar(kind, name, b), _) => b // TODO Is it a problem if this is used more than once? case _ => @@ -234,11 +250,11 @@ object GenerateImplicitArgs { */ def instantiateImplicitValue(v: ImplicitContext.ImplicitStencil[Term], tpe: symbols.ValueType)(using Context): source.ValueArg = { v match { - case ImplicitContext.Error(_, pretty, i, msgs) => + case e @ ImplicitContext.Error(s, i, msgs) => Context.abort(util.messages.ImplicitInstantiationError( - "value argument", pretty, i, tpe, msgs, Context.rangeOf(Context.focus))) + e, tpe, Context.rangeOf(Context.focus))) - case ImplicitContext.ImplicitVar(name, content) => + case ImplicitContext.ImplicitVar(kind, name, content) => source.ValueArg(Some(name), content, Span.missing) // TODO Is it a problem if this is used more than once? case ImplicitContext.SourcePosition(content) => @@ -277,13 +293,13 @@ object GenerateImplicitArgs { def runPhaseOn[A](i: Int, s: ImplicitContext.ImplicitStencil[A])(body: A => Either[EffektMessages, Unit]): ImplicitContext.ImplicitStencil[A] = { s match { case ImplicitContext.ImplicitBlockLiteral(name, content) => body(content) - case ImplicitContext.ImplicitVar(name, content) => body(content) + case ImplicitContext.ImplicitVar(kind, name, content) => body(content) case ImplicitContext.SourcePosition(content) => body(content) case ImplicitContext.CallId() => Right(()) - case ImplicitContext.Error(name, pretty, i, msgs) => Right(()) + case ImplicitContext.Error(_, _, _) => Right(()) } match { case Left(msgs) => - ImplicitContext.Error(s.name, s"${s.pretty}", Some(i), msgs) + ImplicitContext.Error(s, Some(i), msgs) case Right(()) => s } } diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala index 4558e00443..f9da9cd1a3 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala @@ -214,19 +214,33 @@ object ImplicitContext { sealed trait ImplicitStencil[+A] { def name: String - def pretty: String = s"${name}" + def kind: String + def explanation: Option[String] = None } - case class ImplicitBlockLiteral(name: String, content: source.BlockLiteral) extends ImplicitStencil[source.BlockLiteral] - case class ImplicitVar(name: String, content: source.Var) extends ImplicitStencil[source.Var] + case class ImplicitBlockLiteral(name: String, content: source.BlockLiteral) extends ImplicitStencil[source.BlockLiteral] { + def kind = "block argument" + } + case class ImplicitVar(kind: String, name: String, content: source.Var) extends ImplicitStencil[source.Var] case class SourcePosition(content: source.Call) extends ImplicitStencil[source.Call] { def name = "sourcePosition" - override def pretty = s"${name} as a call to 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 respecitve values for the source position of the call. + |""".stripMargin) } case class CallId() extends ImplicitStencil[source.Literal] { def name = "callId" - override def pretty = s"${name} as a fresh literal int id" + 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: Option[Int], msgs: EffektMessages) extends ImplicitStencil[Nothing] { + export underlying.{name, kind} + override def explanation: Option[String] = underlying.explanation } - case class Error(name: String, override val pretty: String, index: Option[Int], msgs: EffektMessages) extends ImplicitStencil[Nothing] } /** diff --git a/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala b/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala index 83c37af4bc..53b270a21c 100644 --- a/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala +++ b/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala @@ -96,19 +96,24 @@ trait ColoredMessaging extends EffektMessaging { } mainMessage + "\n\n" + explanations.mkString("\n\n") + "\n" - case ImplicitInstantiationError(kind, prettyName, index, tpe, msgs, range) => + 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 prettyIndex = index.map(formatIndex).getOrElse("the") + val title = bold("Cannot instantiate implicit argument.\n") + val prettyIndex = err.index.map(formatIndex).getOrElse("a") val prettyTpe = TypePrinter.show(tpe) - val mainMessage = underlined(s"Cannot implicitly instantiate ${prettyIndex} ${kind} ${prettyName} of type ${prettyTpe}.") - val nestedErrors = indent(msgs.map { msg => formatContent(msg) }.mkString("\n")) + 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")) - "\n" + mainMessage + "\n" + nestedErrors + "\n" + title + mainMessage + "\n" + info + nestedErrors + "\n" } def fullname(n: Name): String = n match { diff --git a/effekt/shared/src/main/scala/effekt/util/Messages.scala b/effekt/shared/src/main/scala/effekt/util/Messages.scala index a8126ecc5a..cdca3f23ab 100644 --- a/effekt/shared/src/main/scala/effekt/util/Messages.scala +++ b/effekt/shared/src/main/scala/effekt/util/Messages.scala @@ -13,7 +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(kind: String, prettyName: String, index: Option[Int], tpe: symbols.Type, msgs: EffektMessages, range: Option[Range]) extends EffektError { val severity = Error } + case class ImplicitInstantiationError(stencil: symbols.ImplicitContext.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/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 0000000000..8e282e0b79 --- /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 a 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() + ^^^^^ 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 0000000000..07f2bc31d3 --- /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 0000000000..c2b7140fcd --- /dev/null +++ b/examples/neg/name-based-implicits/type-error-no-overload.check @@ -0,0 +1,3 @@ +[error] Cannot instantiate implicit argument. +Cannot instantiate a 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 0000000000..1b4d981114 --- /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.check b/examples/neg/name-based-implicits/type-error.check new file mode 100644 index 0000000000..b173aa8b04 --- /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 a 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 a 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 0000000000..ce7deacae6 --- /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 From 7d78a4a3cfd09d106dd98bb4a6513f37dee5bf02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Fri, 17 Apr 2026 14:14:47 +0200 Subject: [PATCH 34/46] Always have a parameter index --- .../effekt/source/GenerateImplicitArgs.scala | 4 +-- .../main/scala/effekt/symbols/symbols.scala | 2 +- .../scala/effekt/util/ColoredMessaging.scala | 2 +- .../name-based-implicits/no-implicit.check | 8 +++--- .../type-error-call-id.check | 4 +-- .../type-error-no-overload.check | 2 +- .../type-error-overload.check | 26 +++++++++++++++++++ .../type-error-overload.effekt | 10 +++++++ .../neg/name-based-implicits/type-error.check | 4 +-- 9 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 examples/neg/name-based-implicits/type-error-overload.check create mode 100644 examples/neg/name-based-implicits/type-error-overload.effekt diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index 2f6f4218f2..6a10f7236e 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -67,7 +67,7 @@ object GenerateImplicitArgs { case Left(msgs) => // Note: Recursion Context.abort(util.messages.ImplicitInstantiationError( - ImplicitContext.Error(stencil, None, msgs), tpe, Context.rangeOf(Context.focus))) + ImplicitContext.Error(stencil, index, msgs), tpe, Context.rangeOf(Context.focus))) case Right(r) => r } } @@ -299,7 +299,7 @@ object GenerateImplicitArgs { case ImplicitContext.Error(_, _, _) => Right(()) } match { case Left(msgs) => - ImplicitContext.Error(s, Some(i), msgs) + ImplicitContext.Error(s, i, msgs) case Right(()) => s } } diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala index f9da9cd1a3..ef9a8923c5 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala @@ -237,7 +237,7 @@ object ImplicitContext { 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: Option[Int], msgs: EffektMessages) extends ImplicitStencil[Nothing] { + index: Int, msgs: EffektMessages) extends ImplicitStencil[Nothing] { export underlying.{name, kind} override def explanation: Option[String] = underlying.explanation } diff --git a/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala b/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala index 53b270a21c..cf4929fc5d 100644 --- a/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala +++ b/effekt/shared/src/main/scala/effekt/util/ColoredMessaging.scala @@ -104,7 +104,7 @@ trait ColoredMessaging extends EffektMessaging { case n => s"${n + 1}th" } val title = bold("Cannot instantiate implicit argument.\n") - val prettyIndex = err.index.map(formatIndex).getOrElse("a") + 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 { diff --git a/examples/neg/name-based-implicits/no-implicit.check b/examples/neg/name-based-implicits/no-implicit.check index c68da7a3d9..617e1c2f83 100644 --- a/examples/neg/name-based-implicits/no-implicit.check +++ b/examples/neg/name-based-implicits/no-implicit.check @@ -2,9 +2,11 @@ There are multiple overloads, which all fail to check: Possible overload: no-implicit::foo of type String => Unit - - Cannot implicitly instantiate 1st value argument barbaz of type String. + 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 - Wrong number of arguments to foobaz: expected 1 value argument, but got 0 value arguments \ No newline at end of file + 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/type-error-call-id.check b/examples/neg/name-based-implicits/type-error-call-id.check index 8e282e0b79..23e6250e5a 100644 --- a/examples/neg/name-based-implicits/type-error-call-id.check +++ b/examples/neg/name-based-implicits/type-error-call-id.check @@ -1,7 +1,7 @@ [error] examples/neg/name-based-implicits/type-error-call-id.effekt:4:19: Cannot instantiate implicit argument. -Cannot instantiate a value argument callId of type String. +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-no-overload.check b/examples/neg/name-based-implicits/type-error-no-overload.check index c2b7140fcd..0e8731a43d 100644 --- a/examples/neg/name-based-implicits/type-error-no-overload.check +++ b/examples/neg/name-based-implicits/type-error-no-overload.check @@ -1,3 +1,3 @@ [error] Cannot instantiate implicit argument. -Cannot instantiate a block argument foo of type () => Int. +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-overload.check b/examples/neg/name-based-implicits/type-error-overload.check new file mode 100644 index 0000000000..c47d391d7f --- /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 0000000000..0844d8ef20 --- /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 index b173aa8b04..cc4b3e51ca 100644 --- a/examples/neg/name-based-implicits/type-error.check +++ b/examples/neg/name-based-implicits/type-error.check @@ -3,10 +3,10 @@ There are multiple overloads, which all fail to check: Possible overload: type-error::baz of type {() => Int} => String Cannot instantiate implicit argument. - Cannot instantiate a block argument foo of type () => Int. + 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 a block argument foo of type () => List[Bool]. + Cannot instantiate 1st block argument foo of type () => List[Bool]. Expected List[Bool] but got Bool. \ No newline at end of file From 7ba66555b8f1656f216f2996a67b03516835fecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Fri, 17 Apr 2026 14:18:19 +0200 Subject: [PATCH 35/46] typo --- effekt/shared/src/main/scala/effekt/symbols/symbols.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala index ef9a8923c5..fd3e4e8145 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala @@ -228,7 +228,7 @@ object ImplicitContext { Some( """Implicit sourcePosition will call | SourcePosition(file, start_line, start_col, end_line, end_col) - | with the respecitve values for the source position of the call. + | with the respective values for the source position of the call. |""".stripMargin) } case class CallId() extends ImplicitStencil[source.Literal] { From c30787f3dddebf0e171bb0a5f1166dd537c4baaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Fri, 17 Apr 2026 14:43:35 +0200 Subject: [PATCH 36/46] Refactor: Remove now-obsolete type parameter --- .../shared/src/main/scala/effekt/Typer.scala | 6 +++--- .../effekt/source/GenerateImplicitArgs.scala | 18 ++++++++---------- .../main/scala/effekt/symbols/symbols.scala | 18 +++++++++--------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index f0f4c86481..090ce4f1ec 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1357,9 +1357,9 @@ object Typer extends Phase[NameResolved, Typechecked] { */ private def assertArgsParamsAlign( name: Option[String], - types: Aligned[source.Id | ValueType | symbols.ImplicitContext.ImplicitStencil[_], TypeParam], - values: Aligned[source.ValueParam | source.ValueArg | symbols.ImplicitContext.ImplicitStencil[_], ValueType], - blocks: Aligned[source.BlockParam | source.Term | symbols.ImplicitContext.ImplicitStencil[_], BlockType] + types: Aligned[source.Id | ValueType | symbols.ImplicitContext.ImplicitStencil, TypeParam], + values: Aligned[source.ValueParam | source.ValueArg | symbols.ImplicitContext.ImplicitStencil, ValueType], + blocks: Aligned[source.BlockParam | source.Term | symbols.ImplicitContext.ImplicitStencil, BlockType] )(using Context): Unit = { // Type args are OK iff nothing provided or perfectly aligned diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index 6a10f7236e..06cede7386 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -4,17 +4,15 @@ package source import scala.collection.mutable import scala.util.DynamicVariable -import effekt.util.messages.{ErrorReporter, EffektMessages} +import effekt.util.messages.EffektMessages import effekt.context.Context import effekt.symbols.scopes.Scope -import effekt.symbols.{BlockSymbol, BlockType, Callable, ImplicitContext, builtins, Name} +import effekt.symbols.{BlockSymbol, BlockType, ValueType, Callable, ImplicitContext, builtins, Name} import effekt.context.Annotations import effekt.context.Try object GenerateImplicitArgs { - import effekt.symbols.ValueType - def typeSize(tpe: symbols.Type): Int = tpe match { case tpe: symbols.BlockType => typeSize(tpe) case tpe: symbols.ValueType => typeSize(tpe) @@ -45,7 +43,7 @@ object GenerateImplicitArgs { * Wrapper for recursive type-checking of generated implicits. * Should fail for infinite recursion. */ - def recursionGuard[R](stencil: ImplicitContext.ImplicitStencil[_], kind: String, index: Int, inst: source.Tree, tpe: symbols.Type)(body: => R)(using Context): R = { + def recursionGuard[R](stencil: ImplicitContext.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) @@ -87,7 +85,7 @@ object GenerateImplicitArgs { * 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): ImplicitContext.ImplicitStencil[Term] = { + def generateImplicitValueArg(p: symbols.ValueParam)(using Context): ImplicitContext.ImplicitStencil = { p.name.name match { case "sourcePosition" => // This generates a dummy source to be name-resolved (the actual arguments will be generated later) @@ -109,7 +107,7 @@ object GenerateImplicitArgs { * * 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): ImplicitContext.ImplicitStencil[Term] = + def generateImplicitBlockArg(p: symbols.BlockParam)(using Context): ImplicitContext.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) } @@ -168,7 +166,7 @@ object GenerateImplicitArgs { * * Also annotates all symbols for the returned code correctly where necessary. */ - def instantiateImplicitBlock(b: ImplicitContext.ImplicitStencil[Term], tpe: symbols.BlockType)(using Context): source.Term = { + def instantiateImplicitBlock(b: ImplicitContext.ImplicitStencil, tpe: symbols.BlockType)(using Context): source.Term = { if(!Context.messaging.hasErrors) { (b, tpe) match { case (e @ ImplicitContext.Error(s, i, msgs), _) => @@ -248,7 +246,7 @@ object GenerateImplicitArgs { * * Also annotates all symbols for the returned code correctly where necessary. */ - def instantiateImplicitValue(v: ImplicitContext.ImplicitStencil[Term], tpe: symbols.ValueType)(using Context): source.ValueArg = { + def instantiateImplicitValue(v: ImplicitContext.ImplicitStencil, tpe: symbols.ValueType)(using Context): source.ValueArg = { v match { case e @ ImplicitContext.Error(s, i, msgs) => Context.abort(util.messages.ImplicitInstantiationError( @@ -290,7 +288,7 @@ object GenerateImplicitArgs { /** * Run body on each of the stencil code parts, generating an Error context if errors occur. */ - def runPhaseOn[A](i: Int, s: ImplicitContext.ImplicitStencil[A])(body: A => Either[EffektMessages, Unit]): ImplicitContext.ImplicitStencil[A] = { + def runPhaseOn[A](i: Int, s: ImplicitContext.ImplicitStencil)(body: source.Term => Either[EffektMessages, Unit]): ImplicitContext.ImplicitStencil = { s match { case ImplicitContext.ImplicitBlockLiteral(name, content) => body(content) case ImplicitContext.ImplicitVar(kind, name, content) => body(content) diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala index fd3e4e8145..560e3e781e 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala @@ -204,24 +204,24 @@ enum Binder extends TermSymbol { } export Binder.* -case class ImplicitContext(var values: Map[Int, ImplicitContext.ImplicitStencil[source.Term]], - var blocks: Map[Int, ImplicitContext.ImplicitStencil[source.Term]]) { +case class ImplicitContext(var values: Map[Int, ImplicitContext.ImplicitStencil], + var blocks: Map[Int, ImplicitContext.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 - sealed trait ImplicitStencil[+A] { + sealed trait ImplicitStencil { def name: String def kind: String def explanation: Option[String] = None } - case class ImplicitBlockLiteral(name: String, content: source.BlockLiteral) extends ImplicitStencil[source.BlockLiteral] { + case class ImplicitBlockLiteral(name: String, content: source.BlockLiteral) extends ImplicitStencil { def kind = "block argument" } - case class ImplicitVar(kind: String, name: String, content: source.Var) extends ImplicitStencil[source.Var] - case class SourcePosition(content: source.Call) extends ImplicitStencil[source.Call] { + 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 = @@ -231,13 +231,13 @@ object ImplicitContext { | with the respective values for the source position of the call. |""".stripMargin) } - case class CallId() extends ImplicitStencil[source.Literal] { + 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[Nothing] { + case class Error(underlying: ImplicitStencil, + index: Int, msgs: EffektMessages) extends ImplicitStencil { export underlying.{name, kind} override def explanation: Option[String] = underlying.explanation } From 55ab9605f9c90b085463f812900a3d69314944ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Fri, 17 Apr 2026 14:54:00 +0200 Subject: [PATCH 37/46] Refactor: Move stencils into GenerateImplicitArgs, add some structur --- .../shared/src/main/scala/effekt/Typer.scala | 6 +- .../effekt/source/GenerateImplicitArgs.scala | 107 +++++++++++++----- .../main/scala/effekt/symbols/symbols.scala | 34 +----- .../src/main/scala/effekt/util/Messages.scala | 2 +- 4 files changed, 85 insertions(+), 64 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 090ce4f1ec..8433affdb6 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -1357,9 +1357,9 @@ object Typer extends Phase[NameResolved, Typechecked] { */ private def assertArgsParamsAlign( name: Option[String], - types: Aligned[source.Id | ValueType | symbols.ImplicitContext.ImplicitStencil, TypeParam], - values: Aligned[source.ValueParam | source.ValueArg | symbols.ImplicitContext.ImplicitStencil, ValueType], - blocks: Aligned[source.BlockParam | source.Term | symbols.ImplicitContext.ImplicitStencil, 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 diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index 06cede7386..15004ecad0 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -13,6 +13,43 @@ 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 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) @@ -43,7 +80,7 @@ object GenerateImplicitArgs { * Wrapper for recursive type-checking of generated implicits. * Should fail for infinite recursion. */ - def recursionGuard[R](stencil: ImplicitContext.ImplicitStencil, kind: String, index: Int, inst: source.Tree, tpe: symbols.Type)(body: => R)(using Context): R = { + 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) @@ -65,18 +102,20 @@ object GenerateImplicitArgs { case Left(msgs) => // Note: Recursion Context.abort(util.messages.ImplicitInstantiationError( - ImplicitContext.Error(stencil, index, msgs), tpe, Context.rangeOf(Context.focus))) + 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 - private var nextCallId: Long = 0 /** * Generate source for the given implicit value parameter, matching on the name. @@ -85,11 +124,11 @@ object GenerateImplicitArgs { * 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): ImplicitContext.ImplicitStencil = { + def generateImplicitValueArg(p: symbols.ValueParam)(using Context): ImplicitStencil = { p.name.name match { case "sourcePosition" => // This generates a dummy source to be name-resolved (the actual arguments will be generated later) - ImplicitContext.SourcePosition(Call(IdTarget(IdRef(Nil, "SourcePosition", Span.missing)), Nil, List( + 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), @@ -97,8 +136,8 @@ object GenerateImplicitArgs { ValueArg(None, Literal(-1L, builtins.TInt, Span.missing), Span.missing), ), Nil, Span.missing)) case "callId" => - ImplicitContext.CallId() - case _ => ImplicitContext.ImplicitVar("value argument", p.name.name, Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) + CallId() + case _ => ImplicitVar("value argument", p.name.name, Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) } } @@ -107,7 +146,7 @@ object GenerateImplicitArgs { * * 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): ImplicitContext.ImplicitStencil = + 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) } @@ -115,7 +154,7 @@ object GenerateImplicitArgs { vparams.zipWithIndex.map { (p, i) => ValueParam(IdDef(s"arg${i}", Span.missing), Some(ReifiedType(p)), false, Span.missing) } val gbparams: List[BlockParam] = bparams.zipWithIndex.map { (p, i) => BlockParam(IdDef(s"block_arg${i}", Span.missing), Some(ReifiedType(p)), false, Span.missing) } - ImplicitContext.ImplicitBlockLiteral(p.name.name, BlockLiteral(gtparams, gvparams, gbparams, + 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) }, @@ -123,7 +162,7 @@ object GenerateImplicitArgs { Span.missing), Span.missing)) case BlockType.InterfaceType(typeConstructor, args) => // TODO eta-exapnd here, too ? - ImplicitContext.ImplicitVar("block argument", p.name.name, Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) + ImplicitVar("block argument", p.name.name, Var(IdRef(Nil, p.name.name, Span.missing), Span.missing)) } /** @@ -161,19 +200,27 @@ object GenerateImplicitArgs { }.toMap } + // Instantiation (during typer) + // ============================ + + /** + * Counter for integer ids for calls in the source code, to be passed as callId. + */ + private var nextCallId: Long = 0 + /** * 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: ImplicitContext.ImplicitStencil, tpe: symbols.BlockType)(using Context): source.Term = { + def instantiateImplicitBlock(b: ImplicitStencil, tpe: symbols.BlockType)(using Context): source.Term = { if(!Context.messaging.hasErrors) { (b, tpe) match { - case (e @ ImplicitContext.Error(s, i, msgs), _) => + case (e @ Error(s, i, msgs), _) => Context.abort(util.messages.ImplicitInstantiationError( e, tpe, Context.rangeOf(Context.focus))) - case (ImplicitContext.ImplicitBlockLiteral(name, source.BlockLiteral(tparams, vparams, bparams, source.Return(source.Call(fn, targs, vargs, bargs, _), _), _)), + 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. @@ -230,7 +277,7 @@ object GenerateImplicitArgs { source.Return(source.Call(ffn, ftargs, fvargs, fbargs, source.Span.missing), source.Span.missing), source.Span.missing) - case (ImplicitContext.ImplicitVar(kind, name, b), _) => + case (ImplicitVar(kind, name, b), _) => b // TODO Is it a problem if this is used more than once? case _ => @@ -246,16 +293,16 @@ object GenerateImplicitArgs { * * Also annotates all symbols for the returned code correctly where necessary. */ - def instantiateImplicitValue(v: ImplicitContext.ImplicitStencil, tpe: symbols.ValueType)(using Context): source.ValueArg = { + def instantiateImplicitValue(v: ImplicitStencil, tpe: symbols.ValueType)(using Context): source.ValueArg = { v match { - case e @ ImplicitContext.Error(s, i, msgs) => + case e @ Error(s, i, msgs) => Context.abort(util.messages.ImplicitInstantiationError( e, tpe, Context.rangeOf(Context.focus))) - case ImplicitContext.ImplicitVar(kind, name, content) => + 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 ImplicitContext.SourcePosition(content) => + case SourcePosition(content) => // this generates the version with the correct current positions, val pos = Context.focus.span val from = pos.source.offsetToPosition(pos.from) @@ -276,28 +323,32 @@ object GenerateImplicitArgs { // and returns the result source.ValueArg(Some(v.name), code, Span.missing) - case ImplicitContext.CallId() => + case CallId() => val id = nextCallId nextCallId = nextCallId + 1 source.ValueArg(Some(v.name), Literal(id, builtins.TInt, Span.missing), Span.missing) - case ImplicitContext.ImplicitBlockLiteral(_, _) => Context.panic("Cannot instantiate block literal as an implicit value argument.") + 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: ImplicitContext.ImplicitStencil)(body: source.Term => Either[EffektMessages, Unit]): ImplicitContext.ImplicitStencil = { + def runPhaseOn[A](i: Int, s: ImplicitStencil)(body: source.Term => Either[EffektMessages, Unit]): ImplicitStencil = { s match { - case ImplicitContext.ImplicitBlockLiteral(name, content) => body(content) - case ImplicitContext.ImplicitVar(kind, name, content) => body(content) - case ImplicitContext.SourcePosition(content) => body(content) - case ImplicitContext.CallId() => Right(()) - case ImplicitContext.Error(_, _, _) => Right(()) + case ImplicitBlockLiteral(name, content) => body(content) + case ImplicitVar(kind, name, content) => body(content) + case SourcePosition(content) => body(content) + case CallId() => Right(()) + case Error(_, _, _) => Right(()) } match { - case Left(msgs) => - ImplicitContext.Error(s, i, msgs) + case Left(msgs) => Error(s, i, msgs) case Right(()) => s } } diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala index 560e3e781e..10e5fb7ceb 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala @@ -204,43 +204,13 @@ enum Binder extends TermSymbol { } export Binder.* -case class ImplicitContext(var values: Map[Int, ImplicitContext.ImplicitStencil], - var blocks: Map[Int, ImplicitContext.ImplicitStencil]) { +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 - - 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 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 - } } /** diff --git a/effekt/shared/src/main/scala/effekt/util/Messages.scala b/effekt/shared/src/main/scala/effekt/util/Messages.scala index cdca3f23ab..6ab47737be 100644 --- a/effekt/shared/src/main/scala/effekt/util/Messages.scala +++ b/effekt/shared/src/main/scala/effekt/util/Messages.scala @@ -13,7 +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: symbols.ImplicitContext.Error, tpe: symbols.Type, 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 From b84341dc53fa95481a57864ec2e0cd826f60f1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Fri, 17 Apr 2026 15:37:20 +0200 Subject: [PATCH 38/46] Expand boxes in implicit value arguments --- .../effekt/source/GenerateImplicitArgs.scala | 46 ++++++++++++++----- examples/pos/name-based-implicits/boxed.check | 4 ++ .../pos/name-based-implicits/boxed.effekt | 21 +++++++++ 3 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 examples/pos/name-based-implicits/boxed.check create mode 100644 examples/pos/name-based-implicits/boxed.effekt diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index 15004ecad0..52e1e98780 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -24,6 +24,16 @@ object GenerateImplicitArgs { 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 => "." + })) + } case class ImplicitVar(kind: String, name: String, content: source.Var) extends ImplicitStencil case class SourcePosition(content: source.Call) extends ImplicitStencil { def name = "sourcePosition" @@ -125,8 +135,8 @@ object GenerateImplicitArgs { * - 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 match { - case "sourcePosition" => + (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), @@ -135,8 +145,10 @@ object GenerateImplicitArgs { 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 ("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)) } } @@ -281,7 +293,7 @@ object GenerateImplicitArgs { b // TODO Is it a problem if this is used more than once? case _ => - Context.panic("Unexpected type for implicit stencil.") + Context.panic("Unexpected type for implicit stencil for a block argument.") } } else { Context.abort("Not instantiating implicit block argument since there are errors.") @@ -328,6 +340,13 @@ object GenerateImplicitArgs { nextCallId = nextCallId + 1 source.ValueArg(Some(v.name), Literal(id, builtins.TInt, Span.missing), Span.missing) + 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.") } } @@ -341,13 +360,16 @@ object GenerateImplicitArgs { * 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 = { - s match { - case ImplicitBlockLiteral(name, content) => body(content) - case ImplicitVar(kind, name, content) => body(content) - case SourcePosition(content) => body(content) - case CallId() => Right(()) - case Error(_, _, _) => Right(()) - } match { + 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/examples/pos/name-based-implicits/boxed.check b/examples/pos/name-based-implicits/boxed.check new file mode 100644 index 0000000000..502106d31a --- /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 0000000000..daf9bb6baf --- /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 From 5b2b0d9bf97e0aa78f0de1968e8014fc01bab83b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Fri, 17 Apr 2026 15:51:53 +0200 Subject: [PATCH 39/46] Fix error message for boxed types --- .../scala/effekt/source/GenerateImplicitArgs.scala | 6 +++--- .../neg/name-based-implicits/boxed-captures.check | 5 +++++ .../neg/name-based-implicits/boxed-captures.effekt | 11 +++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 examples/neg/name-based-implicits/boxed-captures.check create mode 100644 examples/neg/name-based-implicits/boxed-captures.effekt diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index 52e1e98780..0dd5a3e21a 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -28,10 +28,10 @@ object GenerateImplicitArgs { 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 { + | that an implicit block argument of the same name would be instantiated to""".stripMargin + + (block.explanation match { case Some(e) => ":\n" + e - case None => "." + case None => ".\n" })) } case class ImplicitVar(kind: String, name: String, content: source.Var) extends ImplicitStencil 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 0000000000..39d2b8ee5e --- /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 0000000000..a8da0c0381 --- /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 From b26c6a807af937dfb01c878c762f805e0e19a7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Sat, 18 Apr 2026 00:45:09 +0200 Subject: [PATCH 40/46] Remove ReifiedType, there is *TypeTree already... --- effekt/shared/src/main/scala/effekt/Namer.scala | 4 ---- .../scala/effekt/source/GenerateImplicitArgs.scala | 14 ++++++++++---- .../shared/src/main/scala/effekt/source/Tree.scala | 6 ------ .../scala/effekt/util/DocumentationGenerator.scala | 1 - 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index bb4d44e9cb..bbabbc12f8 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -831,8 +831,6 @@ object Namer extends Phase[Parsed, NameResolved] { // TODO reconsider reusing the same set for terms and types... case source.BoxedType(tpe, capt, _) => BoxedType(resolveBlockType(tpe), resolve(capt)) - case source.ReifiedType(tpe: ValueType) => - tpe case other => Context.error(pretty"Expected value type, but got ${describeType(other)}.") other match { @@ -859,7 +857,6 @@ object Namer extends Phase[Parsed, NameResolved] { case t: source.FunctionType => resolve(t) case t: source.BlockTypeTree => t.eff case t: source.TypeRef => resolveBlockRef(t) - case source.ReifiedType(tpe: BlockType) => tpe case other => Context.error(pretty"Expected block type, but got ${describeType(other)}.") other match @@ -982,7 +979,6 @@ object Namer extends Phase[Parsed, NameResolved] { // THESE THREE SHOULD NEVER BE USER-VISIBLE! case source.ValueTypeTree(tpe, _) => s"a value type tree ${tpe}" case source.BlockTypeTree(eff, _) => s"a block type tree ${eff}" - case source.ReifiedType(tpe) => "a type in generated code" } private def prettySourceEffectSet(effects: Set[source.TypeRef])(using Context) = diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index 0dd5a3e21a..6f7781656a 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -163,9 +163,13 @@ object GenerateImplicitArgs { 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(ReifiedType(p)), false, Span.missing) } + 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(ReifiedType(p)), false, Span.missing) } + 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) }, @@ -250,7 +254,8 @@ object GenerateImplicitArgs { } val fvpsyms = (vparams zip vps).map { (x, t) => symbols.ValueParam(Name.local(x.id.name), Some(t), false, NoSource) } val fvparams = (vparams zip fvpsyms).map { (x, sym) => - val r: source.ValueParam = source.ValueParam(source.IdDef(x.id.name, source.Span.missing), Some(source.ReifiedType(sym.tpe.get)), false, source.Span.missing) + val r: source.ValueParam = source.ValueParam(source.IdDef(x.id.name, source.Span.missing), + Some(source.ValueTypeTree(sym.tpe.get, source.Span.missing)), false, source.Span.missing) Context.annotate(Annotations.Symbol, r, sym) Context.annotate(Annotations.Symbol, r.id, sym) r @@ -263,7 +268,8 @@ object GenerateImplicitArgs { } val fbpsyms = (bparams zip bps).map { (x, t) => symbols.BlockParam(Name.local(x.id.name), Some(t), x.symbol.capture, false, NoSource) } val fbparams = (bparams zip fbpsyms).map { (x, sym) => - val r: source.BlockParam = source.BlockParam(source.IdDef(x.id.name, source.Span.missing), Some(source.ReifiedType(sym.tpe.get)), false, source.Span.missing) + val r: source.BlockParam = source.BlockParam(source.IdDef(x.id.name, source.Span.missing), + Some(source.BlockTypeTree(sym.tpe.get, source.Span.missing)), false, source.Span.missing) Context.annotate(Annotations.Symbol, r, sym) Context.annotate(Annotations.Symbol, r.id, sym) r diff --git a/effekt/shared/src/main/scala/effekt/source/Tree.scala b/effekt/shared/src/main/scala/effekt/source/Tree.scala index d54f078fb6..93e1886495 100644 --- a/effekt/shared/src/main/scala/effekt/source/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/source/Tree.scala @@ -738,12 +738,6 @@ case class FunctionType(tparams: Many[Id], vparams: Many[ValueType], bparams: Ma */ case class Effectful(tpe: ValueType, eff: Effects, span: Span) extends Type -/** - * For generating code with already resolved types - */ -case class ReifiedType(tpe: symbols.ValueType | symbols.BlockType) extends Type { - val span = Span.missing -} // These are just type aliases for documentation purposes. type BlockType = Type diff --git a/effekt/shared/src/main/scala/effekt/util/DocumentationGenerator.scala b/effekt/shared/src/main/scala/effekt/util/DocumentationGenerator.scala index b057074d90..f7f0610e66 100644 --- a/effekt/shared/src/main/scala/effekt/util/DocumentationGenerator.scala +++ b/effekt/shared/src/main/scala/effekt/util/DocumentationGenerator.scala @@ -52,7 +52,6 @@ trait DocumentationGenerator { // block params case BlockTypeTree(eff, _) => ??? - case ReifiedType(tpe) => ??? case FunctionType(tparams, vparams, bparams, result, effects, span) => obj(HashMap( "kind" -> str("FunctionType"), "tparams" -> generateTparams(tparams.unspan), From e92b903bf64797742b49638dd61625c077023846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Sat, 18 Apr 2026 16:26:04 +0200 Subject: [PATCH 41/46] Simplify generation of refreshed block literal --- .../effekt/source/GenerateImplicitArgs.scala | 67 ++++++++----------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index 6f7781656a..f00accc68b 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -224,6 +224,14 @@ object GenerateImplicitArgs { */ 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. * @@ -241,45 +249,26 @@ object GenerateImplicitArgs { // 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 = tparams.map { x => symbols.TypeParam(Name.local(x.name)) } - val ftparams = (tparams zip ftpsyms).map { (x, sym) => - val r = source.IdDef(x.name, source.Span.missing) - Context.annotate(Annotations.Symbol, r, sym) - r - } - val ftargs = ftpsyms.map { x => - val r = source.TypeRef(source.IdRef(Nil, x.name.name, source.Span.missing), Many(Nil, source.Span.missing), source.Span.missing) - Context.annotate(Annotations.Symbol, r, x) - r - } - val fvpsyms = (vparams zip vps).map { (x, t) => symbols.ValueParam(Name.local(x.id.name), Some(t), false, NoSource) } - val fvparams = (vparams zip fvpsyms).map { (x, sym) => - val r: source.ValueParam = source.ValueParam(source.IdDef(x.id.name, source.Span.missing), - Some(source.ValueTypeTree(sym.tpe.get, source.Span.missing)), false, source.Span.missing) - Context.annotate(Annotations.Symbol, r, sym) - Context.annotate(Annotations.Symbol, r.id, sym) - r - } - val fvargs = fvpsyms.map { x => - val r = source.Var(source.IdRef(Nil, x.name.name, source.Span.missing), source.Span.missing) - Context.annotate(Annotations.Symbol, r, x) - Context.annotate(Annotations.Symbol, r.id, x) - source.ValueArg(None, r, source.Span.missing) - } - val fbpsyms = (bparams zip bps).map { (x, t) => symbols.BlockParam(Name.local(x.id.name), Some(t), x.symbol.capture, false, NoSource) } - val fbparams = (bparams zip fbpsyms).map { (x, sym) => - val r: source.BlockParam = source.BlockParam(source.IdDef(x.id.name, source.Span.missing), - Some(source.BlockTypeTree(sym.tpe.get, source.Span.missing)), false, source.Span.missing) - Context.annotate(Annotations.Symbol, r, sym) - Context.annotate(Annotations.Symbol, r.id, sym) - r - } - val fbargs = fbpsyms.map { x => - val r = source.Var(source.IdRef(Nil, x.name.name, source.Span.missing), source.Span.missing) - Context.annotate(Annotations.Symbol, r, x) - Context.annotate(Annotations.Symbol, r.id, x) - r - } + 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)) From 85be02511491d53857a07776a26996bb3aabab9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Sat, 18 Apr 2026 16:43:59 +0200 Subject: [PATCH 42/46] Simplify generation of value args --- .../scala/effekt/source/GenerateImplicitArgs.scala | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index f00accc68b..ae668f1d15 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -301,6 +301,9 @@ object GenerateImplicitArgs { * 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( @@ -316,15 +319,10 @@ object GenerateImplicitArgs { 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), - ValueArg(None, Literal(from.line.toLong, builtins.TInt, Span.missing), Span.missing), - ValueArg(None, Literal(from.column.toLong, builtins.TInt, Span.missing), Span.missing), - ValueArg(None, Literal(to.line.toLong, builtins.TInt, Span.missing), Span.missing), - ValueArg(None, Literal(to.column.toLong, builtins.TInt, Span.missing), Span.missing), - ), Nil, 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, y) Context.copyAnnotations(x.id, y.id) // and returns the result @@ -333,7 +331,7 @@ object GenerateImplicitArgs { case CallId() => val id = nextCallId nextCallId = nextCallId + 1 - source.ValueArg(Some(v.name), Literal(id, builtins.TInt, Span.missing), Span.missing) + intArg(id, Some(v.name)) case BoxedStencil(name, block) => val symbols.BoxedType(btpe, _) = tpe: @unchecked From 9e3c2e129559a698b6f20e9bb84bf78e49848b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Mon, 20 Apr 2026 17:50:35 +0200 Subject: [PATCH 43/46] Initial draft language tour and minor fix --- .../effekt/source/GenerateImplicitArgs.scala | 1 + examples/tour/name-based-implicits.effekt.md | 133 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 examples/tour/name-based-implicits.effekt.md diff --git a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala index ae668f1d15..e32258634f 100644 --- a/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala +++ b/effekt/shared/src/main/scala/effekt/source/GenerateImplicitArgs.scala @@ -276,6 +276,7 @@ object GenerateImplicitArgs { 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") diff --git a/examples/tour/name-based-implicits.effekt.md b/examples/tour/name-based-implicits.effekt.md new file mode 100644 index 0000000000..93a0de3c45 --- /dev/null +++ b/examples/tour/name-based-implicits.effekt.md @@ -0,0 +1,133 @@ +--- +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]) +``` + +But are still allowed to pass it explicitly: +```effekt:repl +mySort2([3,1,2]){ (x, y) => compare(x, y) } +``` + +Name-based implicit parameters also will be passed +to the functions generated implicitly, 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)]) +``` + +## 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) +``` +```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 From 67de2c7a2accf6414fc288888f3f3247811ab667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Mon, 20 Apr 2026 18:02:32 +0200 Subject: [PATCH 44/46] Update effekt/shared/src/main/scala/effekt/Namer.scala MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jonathan Immanuel Brachthäuser --- effekt/shared/src/main/scala/effekt/Namer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index bbabbc12f8..a8109003ea 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -976,7 +976,7 @@ object Namer extends Phase[Parsed, NameResolved] { case _: source.FunctionType => s"a second-class function type ${t.sourceOf}" case _: source.Effectful => s"a type-and-effect annotation ${t.sourceOf}" - // THESE THREE SHOULD NEVER BE USER-VISIBLE! + // THESE TWO SHOULD NEVER BE USER-VISIBLE! case source.ValueTypeTree(tpe, _) => s"a value type tree ${tpe}" case source.BlockTypeTree(eff, _) => s"a block type tree ${eff}" } From 1aa3829d2fa1a040f48300d42aed0e6642bb366f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Tue, 21 Apr 2026 10:21:35 +0200 Subject: [PATCH 45/46] Minor improvements in the name-based implicits tour --- examples/tour/name-based-implicits.effekt.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/tour/name-based-implicits.effekt.md b/examples/tour/name-based-implicits.effekt.md index 93a0de3c45..7ecb5191ad 100644 --- a/examples/tour/name-based-implicits.effekt.md +++ b/examples/tour/name-based-implicits.effekt.md @@ -45,13 +45,13 @@ Then we can just call it as: mySort2([3,1,2]) ``` -But are still allowed to pass it explicitly: +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 generated implicitly, so if we define +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 { @@ -67,6 +67,11 @@ we can now also call `mySort2` with lists of options (or options of options, ... 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 `?`: From 319e60dcb6ed03774a03203b11b49a3f1676a8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Tue, 21 Apr 2026 13:57:58 +0200 Subject: [PATCH 46/46] Fix example by unboxing manually --- examples/tour/name-based-implicits.effekt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tour/name-based-implicits.effekt.md b/examples/tour/name-based-implicits.effekt.md index 7ecb5191ad..abc3f1c055 100644 --- a/examples/tour/name-based-implicits.effekt.md +++ b/examples/tour/name-based-implicits.effekt.md @@ -105,7 +105,7 @@ 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) + mySort2(x){ (x,y) => (unbox compare)(x, y) } ``` ```effekt:repl mySort3([12,3,42])