From eedcefe7e931a071fa88b88451e6e902f6227c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Brachtha=CC=88user?= Date: Wed, 23 Jul 2025 15:18:40 +0200 Subject: [PATCH 1/3] Implement operations symmetrically and simplify their lookup --- .../jvm/src/test/scala/effekt/LSPTests.scala | 9 ++++ .../shared/src/main/scala/effekt/Namer.scala | 45 ++++++++++--------- .../shared/src/main/scala/effekt/Parser.scala | 10 ++--- .../shared/src/main/scala/effekt/Typer.scala | 2 +- .../main/scala/effekt/core/Transformer.scala | 2 +- .../effekt/source/AnnotateCaptures.scala | 2 +- .../effekt/source/ExplicitCapabilities.scala | 6 +-- .../src/main/scala/effekt/source/Tree.scala | 5 +-- .../src/main/scala/effekt/symbols/Scope.scala | 11 ++--- .../effekt/typer/BoxUnboxInference.scala | 4 +- .../scala/effekt/typer/Wellformedness.scala | 2 +- examples/pos/namespaced_constructors.check | 1 + examples/pos/namespaced_constructors.effekt | 20 +++++++++ examples/pos/namespaced_operations.check | 2 + examples/pos/namespaced_operations.effekt | 21 +++++++++ 15 files changed, 96 insertions(+), 46 deletions(-) create mode 100644 examples/pos/namespaced_constructors.check create mode 100644 examples/pos/namespaced_constructors.effekt create mode 100644 examples/pos/namespaced_operations.check create mode 100644 examples/pos/namespaced_operations.effekt diff --git a/effekt/jvm/src/test/scala/effekt/LSPTests.scala b/effekt/jvm/src/test/scala/effekt/LSPTests.scala index 7889438c0..e81a925aa 100644 --- a/effekt/jvm/src/test/scala/effekt/LSPTests.scala +++ b/effekt/jvm/src/test/scala/effekt/LSPTests.scala @@ -1883,6 +1883,15 @@ class LSPTests extends FunSuite { ), kind = "Term" ), + TermBinding( + qualifier = List("Bar"), + name = "Bar", + origin = "Defined", + `type` = Some( + value = "Int => Bar" + ), + kind = "Term" + ), TermBinding( qualifier = Nil, name = "main", diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index 844f88370..8575c8f1c 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -219,7 +219,7 @@ object Namer extends Phase[Parsed, NameResolved] { ExternInterface(Context.nameFor(id), tps, decl) }) - case d @ source.ExternDef(capture, id, tparams, vparams, bparams, ret, bodies, doc, span) => { + case d @ source.ExternDef(capture, id, tparams, vparams, bparams, ret, bodies, doc, span) => val name = Context.nameFor(id) val capt = resolve(capture) Context.define(id, Context scoped { @@ -234,7 +234,6 @@ object Namer extends Phase[Parsed, NameResolved] { ExternFunction(name, tps.unspan, vps.unspan, bps.unspan, tpe, eff, capt, bodies, d) }) - } case d @ source.ExternResource(id, tpe, doc, span) => val name = Context.nameFor(id) @@ -346,14 +345,14 @@ object Namer extends Phase[Parsed, NameResolved] { } } - case source.InterfaceDef(id, tparams, operations, doc, span) => + case source.InterfaceDef(interfaceId, tparams, operations, doc, span) => // symbol has already been introduced by the previous traversal - val interface = Context.symbolOf(id).asInterface + val interface = Context.symbolOf(interfaceId).asInterface interface.operations = operations.map { case op @ source.Operation(id, tparams, vparams, bparams, ret, doc, span) => Context.at(op) { val name = Context.nameFor(id) - Context.scopedWithName(id.name) { + val opSym = Context.scopedWithName(id.name) { // the parameters of the interface are in scope interface.tparams.foreach { p => Context.bind(p) } @@ -370,10 +369,16 @@ object Namer extends Phase[Parsed, NameResolved] { // 2) the annotated type parameters on the concrete operation val (result, effects) = resolve(ret) - val opSym = Operation(name, interface.tparams ++ tps.unspan, resVparams, resBparams, result, effects, interface, op) + Operation(name, interface.tparams ++ tps.unspan, resVparams, resBparams, result, effects, interface, op) + } + + // define in namespace ... + Context.namespace(interfaceId.name) { Context.define(id, opSym) - opSym } + // ... and bind outside + Context.bind(opSym) + opSym } } @@ -383,7 +388,7 @@ object Namer extends Phase[Parsed, NameResolved] { } // The type itself has already been resolved, now resolve constructors - case d @ source.DataDef(id, tparams, ctors, doc, span) => + case d @ source.DataDef(typeId, tparams, ctors, doc, span) => val data = d.symbol data.constructors = ctors map { case c @ source.Constructor(id, tparams, ps, doc, span) => @@ -392,7 +397,13 @@ object Namer extends Phase[Parsed, NameResolved] { val tps = tparams map resolve Constructor(name, data.tparams ++ tps.unspan, Nil, data, c) } - Context.define(id, constructor) + // define in namespace ... + Context.namespace(typeId.name) { + Context.define(id, constructor) + } + // ... and bind outside + Context.bind(constructor) + constructor.fields = resolveFields(ps.unspan, constructor, false) constructor } @@ -528,8 +539,8 @@ object Namer extends Phase[Parsed, NameResolved] { vargs foreach resolve bargs foreach resolve - case source.Do(effect, target, targs, vargs, bargs, _) => - Context.resolveEffectCall(effect map resolveBlockRef, target) + case source.Do(target, targs, vargs, bargs, _) => + Context.resolveEffectCall(target) targs foreach resolveValueType vargs foreach resolve bargs foreach resolve @@ -1138,15 +1149,9 @@ trait NamerOps extends ContextOps { Context: Context => /** * Resolves a potentially overloaded call to an effect */ - private[namer] def resolveEffectCall(eff: Option[InterfaceType], id: IdRef): Unit = at(id) { - - val syms = eff match { - case Some(tpe) => - val interface = tpe.typeConstructor.asInterface - val operations = interface.operations.filter { op => op.name.name == id.name } - if (operations.isEmpty) Nil else List(operations.toSet) - case None => scope.lookupOperation(id.path, id.name) - } + private[namer] def resolveEffectCall(id: IdRef): Unit = at(id) { + + val syms = scope.lookupOperation(id.path, id.name) if (syms.isEmpty) { abort(pretty"Cannot resolve effect operation ${id}") diff --git a/effekt/shared/src/main/scala/effekt/Parser.scala b/effekt/shared/src/main/scala/effekt/Parser.scala index d19c271b0..c93eef0bc 100644 --- a/effekt/shared/src/main/scala/effekt/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/Parser.scala @@ -292,8 +292,8 @@ class Parser(positions: Positions, tokens: Seq[Token], source: Source) { case Var(id, varSpan) => val tgt = IdTarget(id) Return(Call(tgt, Nil, Nil, (BlockLiteral(Nil, vparams, bparams, body, body.span.synthesized)) :: Nil, varSpan), withSpan.synthesized) - case Do(effect, id, targs, vargs, bargs, doSpan) => - Return(Do(effect, id, targs, vargs, bargs :+ BlockLiteral(Nil, vparams, bparams, body, body.span.synthesized), doSpan), withSpan.synthesized) + case Do(id, targs, vargs, bargs, doSpan) => + Return(Do(id, targs, vargs, bargs :+ BlockLiteral(Nil, vparams, bparams, body, body.span.synthesized), doSpan), withSpan.synthesized) case term => Return(Call(ExprTarget(term), Nil, Nil, (BlockLiteral(Nil, vparams, bparams, body, body.span.synthesized)) :: Nil, term.span.synthesized), withSpan.synthesized) } @@ -814,7 +814,7 @@ class Parser(positions: Positions, tokens: Seq[Token], source: Source) { def doExpr(): Term = nonterminal: (`do` ~> idRef()) ~ arguments() match { - case id ~ (targs, vargs, bargs) => Do(None, id, targs, vargs, bargs, span()) + case id ~ (targs, vargs, bargs) => Do(id, targs, vargs, bargs, span()) } /* @@ -1271,10 +1271,10 @@ class Parser(positions: Positions, tokens: Seq[Token], source: Source) { case id ~ Template(strs, args) => val target = id.getOrElse(IdRef(Nil, "s", id.span.synthesized)) val doLits = strs.map { s => - Do(None, IdRef(Nil, "literal", Span.missing(source)), Nil, List(ValueArg.Unnamed(StringLit(s, Span.missing(source)))), Nil, Span.missing(source)) + Do(IdRef(Nil, "literal", Span.missing(source)), Nil, List(ValueArg.Unnamed(StringLit(s, Span.missing(source)))), Nil, Span.missing(source)) } val doSplices = args.map { arg => - Do(None, IdRef(Nil, "splice", Span.missing(source)), Nil, List(ValueArg.Unnamed(arg)), Nil, Span.missing(source)) + Do(IdRef(Nil, "splice", Span.missing(source)), Nil, List(ValueArg.Unnamed(arg)), Nil, Span.missing(source)) } val body = interleave(doLits, doSplices) .foldRight(Return(UnitLit(Span.missing(source)), Span.missing(source))) { (term, acc) => ExprStmt(term, acc, Span.missing(source)) } diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 9f7fea86c..2e318d7c9 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -148,7 +148,7 @@ object Typer extends Phase[NameResolved, Typechecked] { case c @ source.Select(receiver, field, _) => checkOverloadedFunctionCall(c, field, Nil, List(source.ValueArg.Unnamed(receiver)), Nil, expected) - case c @ source.Do(effect, op, targs, vargs, bargs, _) => + case c @ source.Do(op, targs, vargs, bargs, _) => // (1) first check the call val Result(tpe, effs) = checkOverloadedFunctionCall(c, op, targs map { _.resolveValueType }, vargs, bargs, expected) // (2) now we need to find a capability as the receiver of this effect operation diff --git a/effekt/shared/src/main/scala/effekt/core/Transformer.scala b/effekt/shared/src/main/scala/effekt/core/Transformer.scala index e7d31c3d9..0dbffcdee 100644 --- a/effekt/shared/src/main/scala/effekt/core/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/core/Transformer.scala @@ -518,7 +518,7 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { case c @ source.Call(s: source.ExprTarget, targs, vargs, bargs, _) => Context.panic("Should not happen. Unbox should have been inferred.") - case source.Do(effect, id, targs, vargs, bargs, _) => + case source.Do(id, targs, vargs, bargs, _) => Context.panic("Should have been translated away (to explicit selection `@CAP.op()`) by capability passing.") } diff --git a/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala b/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala index 5fff21bfe..9b799dfb6 100644 --- a/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala +++ b/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala @@ -51,7 +51,7 @@ object AnnotateCaptures extends Phase[Typechecked, Typechecked], Query[Unit, Cap } query(term) ++ capt - case t @ source.Do(effect, op, targs, vargs, bargs, _) => + case t @ source.Do(op, targs, vargs, bargs, _) => val cap = Context.annotation(Annotations.CapabilityReceiver, t) combineAll(vargs.map(query)) ++ combineAll(bargs.map(query)) ++ CaptureSet(cap.capture) diff --git a/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala b/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala index 36b0f25b4..ec9bb0d6e 100644 --- a/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala +++ b/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala @@ -42,7 +42,7 @@ object ExplicitCapabilities extends Phase[Typechecked, Typechecked], Rewrite { override def expr(using Context) = { // an effect call -- translate to method call on the inferred capability - case c @ Do(effect, id, targs, vargs, bargs, span) => + case c @ Do(id, targs, vargs, bargs, span) => val transformedValueArgs = vargs.map(rewrite) val transformedBlockArgs = bargs.map(rewrite) @@ -130,7 +130,7 @@ object ExplicitCapabilities extends Phase[Typechecked, Typechecked], Rewrite { source.BlockLiteral(tps, vps, bps ++ capParams, rewrite(body), span) } - override def rewrite(body: ExternBody)(using context.Context): ExternBody = + override def rewrite(body: ExternBody)(using context.Context): ExternBody = body match { case b @ source.ExternBody.StringExternBody(ff, body, span) => val rewrittenTemplate = @@ -139,7 +139,7 @@ object ExplicitCapabilities extends Phase[Typechecked, Typechecked], Rewrite { ) b.copy(template = rewrittenTemplate) case b @ source.ExternBody.EffektExternBody(ff, body, span) => - val rewrittenBody = rewrite(body) + val rewrittenBody = rewrite(body) b.copy(body = rewrittenBody) case u: source.ExternBody.Unsupported => u } diff --git a/effekt/shared/src/main/scala/effekt/source/Tree.scala b/effekt/shared/src/main/scala/effekt/source/Tree.scala index 86e39bdb8..bbe71c41f 100644 --- a/effekt/shared/src/main/scala/effekt/source/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/source/Tree.scala @@ -499,11 +499,8 @@ enum Term extends Tree { /** * A call to an effect operation, i.e., `do raise()`. - * - * The [[effect]] is the optionally annotated effect type (not possible in source ATM). In the future, this could - * look like `do Exc.raise()`, or `do[Exc] raise()`, or do[Exc].raise(), or simply Exc.raise() where Exc is a type. */ - case Do(effect: Option[TypeRef], id: IdRef, targs: List[ValueType], vargs: List[ValueArg], bargs: List[Term], span: Span) extends Term, Reference + case Do(id: IdRef, targs: List[ValueType], vargs: List[ValueArg], bargs: List[Term], span: Span) extends Term, Reference /** * A call to either an expression, i.e., `(fun() { ...})()`; or a named function, i.e., `foo()` diff --git a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala index ec47ada3f..d5bf15645 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala @@ -50,12 +50,6 @@ case class Bindings( def getNamespace(name: String): Option[Bindings] = namespaces.get(name) - - def operations: Map[String, Set[Operation]] = - types.values.toSet.flatMap { - case BlockTypeConstructor.Interface(_, _, operations, _) => operations.toSet - case _ => Set.empty - }.groupMap(_.name.name)(op => op) } object Bindings { @@ -228,12 +222,13 @@ object scopes { def lookupOverloadedMethod(id: IdRef, filter: TermSymbol => Boolean)(using ErrorReporter): List[Set[Operation]] = all(id.path, scope) { namespace => - namespace.operations.getOrElse(id.name, Set.empty).filter(filter) + namespace.terms.getOrElse(id.name, Set.empty).collect { case op: Operation if filter(op) => op } } + // 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 => - namespace.operations.getOrElse(name, Set.empty) + namespace.terms.getOrElse(name, Set.empty).collect { case op: Operation => op } }.filter { namespace => namespace.nonEmpty } def lookupFunction(path: List[String], name: String)(using ErrorReporter): List[Set[Callable]] = diff --git a/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala b/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala index 7d6e4c0bd..89110a44a 100644 --- a/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala +++ b/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala @@ -79,8 +79,8 @@ object BoxUnboxInference extends Phase[NameResolved, NameResolved] { case s @ Select(recv, name, span) => C.abort("selection on blocks not supported yet.") - case Do(effect, id, targs, vargs, bargs, span) => - Do(effect, id, targs, vargs.map(rewriteAsExpr), bargs.map(rewriteAsBlock), span) + case Do(id, targs, vargs, bargs, span) => + Do(id, targs, vargs.map(rewriteAsExpr), bargs.map(rewriteAsBlock), span) case Call(fun, targs, vargs, bargs, span) => Call(rewrite(fun), targs, vargs.map(rewriteAsExpr), bargs.map(rewriteAsBlock), span) diff --git a/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala b/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala index b77db4ff5..52245a67a 100644 --- a/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala +++ b/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala @@ -212,7 +212,7 @@ object Wellformedness extends Phase[Typechecked, Typechecked], Visit[WFContext] vargs.foreach(query) bargs.foreach(query) - case tree @ source.Do(effect, id, targs, vargs, bargs, _) => + case tree @ source.Do(id, targs, vargs, bargs, _) => val inferredTypeArgs = Context.typeArguments(tree) inferredTypeArgs.zipWithIndex.foreach { case (tpe, index) => wellformed(tpe, tree, pp" inferred as ${showPosition(index + 1)} type argument") diff --git a/examples/pos/namespaced_constructors.check b/examples/pos/namespaced_constructors.check new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/examples/pos/namespaced_constructors.check @@ -0,0 +1 @@ +1 diff --git a/examples/pos/namespaced_constructors.effekt b/examples/pos/namespaced_constructors.effekt new file mode 100644 index 000000000..47c62c815 --- /dev/null +++ b/examples/pos/namespaced_constructors.effekt @@ -0,0 +1,20 @@ +type Foo { + Bar() +} + +type Boo { + Bar() +} +type Bars { + Foo(f: Foo) + Boo(b: Boo) +} + +def consume(b: Bars) = b match { + case Foo(Foo::Bar()) => 1 + case Boo(Boo::Bar()) => 2 +} + +def main() = { + println(consume(Bars::Foo(Foo::Bar()))) +} \ No newline at end of file diff --git a/examples/pos/namespaced_operations.check b/examples/pos/namespaced_operations.check new file mode 100644 index 000000000..a5c880627 --- /dev/null +++ b/examples/pos/namespaced_operations.check @@ -0,0 +1,2 @@ +3 +3 diff --git a/examples/pos/namespaced_operations.effekt b/examples/pos/namespaced_operations.effekt new file mode 100644 index 000000000..36af2be4f --- /dev/null +++ b/examples/pos/namespaced_operations.effekt @@ -0,0 +1,21 @@ +interface Foo { + def bar(): Int +} + +interface Boo { + def bar(): Int +} + +def useBothEffect() = + println(do Foo::bar() + do Boo::bar()) + +def useBothExplicit {f: Foo} {b: Boo} = + println(f.Foo::bar() + b.Boo::bar()) + +def main() = + try { + useBothEffect() + useBothExplicit {f} {b} + } + with f: Foo { def bar() = resume(1) } + with b: Boo { def bar() = resume(2) } \ No newline at end of file From b79d8ea8384a03b75799ed4621a00649e55479e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Brachtha=CC=88user?= Date: Thu, 24 Jul 2025 09:25:28 +0200 Subject: [PATCH 2/3] slightly adjust documentation --- effekt/shared/src/main/scala/effekt/Namer.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index 8575c8f1c..011eb36cf 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -390,23 +390,24 @@ object Namer extends Phase[Parsed, NameResolved] { // The type itself has already been resolved, now resolve constructors case d @ source.DataDef(typeId, tparams, ctors, doc, span) => val data = d.symbol - data.constructors = ctors map { + val constructors = ctors map { case c @ source.Constructor(id, tparams, ps, doc, span) => val constructor = Context scoped { val name = Context.nameFor(id) val tps = tparams map resolve Constructor(name, data.tparams ++ tps.unspan, Nil, data, c) } - // define in namespace ... + // DataType::Constructor() Context.namespace(typeId.name) { Context.define(id, constructor) } - // ... and bind outside - Context.bind(constructor) - constructor.fields = resolveFields(ps.unspan, constructor, false) constructor } + // export DataType::{Constructor1, ...} + constructors.foreach { c => Context.bind(c) } + + data.constructors = constructors // The record has been resolved as part of the preresolution step case d @ source.RecordDef(id, tparams, fs, doc, span) => From ad78f8fb667c936535fa649088134e48a3cf7bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Brachtha=CC=88user?= Date: Thu, 31 Jul 2025 18:01:44 +0200 Subject: [PATCH 3/3] Try to fix tests --- .../jvm/src/test/scala/effekt/LSPTests.scala | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/LSPTests.scala b/effekt/jvm/src/test/scala/effekt/LSPTests.scala index e81a925aa..ef1f796ab 100644 --- a/effekt/jvm/src/test/scala/effekt/LSPTests.scala +++ b/effekt/jvm/src/test/scala/effekt/LSPTests.scala @@ -48,14 +48,14 @@ class LSPTests extends FunSuite { def withClientAndServer(testBlock: (MockLanguageClient, Server) => Unit): Unit = { withClientAndServer(true)(testBlock) } - + /** Normalize the output of the IR by replacing the generated identifiers and stripping all whitespace */ def normalizeIRString(ir: String): String = { ir.replaceAll("_\\d+", "_whatever") .replaceAll("\\s+", "") } - + def assertIREquals(ir: String, expected: String): Unit = { val normalizedIR = normalizeIRString(ir) val normalizedExpected = normalizeIRString(expected) @@ -1410,7 +1410,7 @@ class LSPTests extends FunSuite { assertEquals(receivedHoles.head.holes.length, 1) } } - + test("Server publishes hole id for nested defs") { withClientAndServer { (client, server) => val source = @@ -1778,11 +1778,14 @@ class LSPTests extends FunSuite { assertEquals(innerBindings(4).name, "b2") val outerBindings = hole.scope.outer.get.bindings - - assertEquals(outerBindings.length, 3) - assertEquals(outerBindings(0).name, "e1") - assertEquals(outerBindings(1).name, "foo") - assertEquals(outerBindings(2).name, "e2") + println(outerBindings) + + assertEquals(outerBindings.length, 5) + assertEquals(outerBindings(0).name, "e1") // the type `effect e1(): Int / {}` + assertEquals(outerBindings(1).name, "e1") // the operation term + assertEquals(outerBindings(2).name, "e1") // the operation term e1::e1, these are duplicate bindings, introduced by https://github.com/effekt-lang/effekt/pull/1092 + assertEquals(outerBindings(3).name, "foo") + assertEquals(outerBindings(4).name, "e2") // only the type since, but why? } }