Skip to content

Commit

Permalink
Support singleton operations better (#590)
Browse files Browse the repository at this point in the history
  • Loading branch information
b-studios authored Sep 12, 2024
2 parents 5c5b53b + fcf3f51 commit 21343a7
Show file tree
Hide file tree
Showing 27 changed files with 218 additions and 205 deletions.
7 changes: 3 additions & 4 deletions effekt/shared/src/main/scala/effekt/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ object Namer extends Phase[Parsed, NameResolved] {
}
Context.define(id, sym)

case source.InterfaceDef(id, tparams, ops, isEffect) =>
case source.InterfaceDef(id, tparams, ops) =>
val effectName = Context.nameFor(id)
// we use the localName for effects, since they will be bound as capabilities
val effectSym = Context scoped {
Expand Down Expand Up @@ -315,7 +315,7 @@ object Namer extends Phase[Parsed, NameResolved] {
}
}

case source.InterfaceDef(id, tparams, operations, isEffect) =>
case source.InterfaceDef(id, tparams, operations) =>
// symbol has already been introduced by the previous traversal
val interface = Context.symbolOf(id).asInterface
interface.operations = operations.map {
Expand Down Expand Up @@ -345,7 +345,6 @@ object Namer extends Phase[Parsed, NameResolved] {
}
}
}
if (isEffect) interface.operations.foreach { op => Context.bind(op) }

case source.NamespaceDef(id, definitions) =>
Context.namespace(id.name) {
Expand Down Expand Up @@ -840,7 +839,7 @@ trait NamerOps extends ContextOps { Context: Context =>
*/
private[namer] def resolveMethodCalltarget(id: IdRef): Unit = at(id) {

val syms = scope.lookupOverloaded(id, term => term.isInstanceOf[BlockSymbol])
val syms = scope.lookupOverloadedMethod(id, term => term.isInstanceOf[BlockSymbol])

if (syms.isEmpty) {
abort(pretty"Cannot resolve function ${id.name}")
Expand Down
12 changes: 6 additions & 6 deletions effekt/shared/src/main/scala/effekt/RecursiveDescent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -452,9 +452,9 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
def operationDef(): Def =
nonterminal:
`effect` ~> operation() match {
case op =>
case op @ Operation(id, tps, vps, bps, ret) =>
// TODO is the `true` flag used at all anymore???
InterfaceDef(IdDef(op.id.name), Nil, List(op), true)
InterfaceDef(IdDef(id.name), tps, List(Operation(id, Nil, vps, bps, ret) withPositionOf op))
}

def operation(): Operation =
Expand All @@ -465,7 +465,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)

def interfaceDef(): InterfaceDef =
nonterminal:
InterfaceDef(`interface` ~> idDef(), maybeTypeParams(), `{` ~> manyWhile(`def` ~> operation(), `def`) <~ `}`, true)
InterfaceDef(`interface` ~> idDef(), maybeTypeParams(), `{` ~> manyWhile(`def` ~> operation(), `def`) <~ `}`)

def namespaceDef(): Def =
nonterminal:
Expand Down Expand Up @@ -697,11 +697,11 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)

// Interface[...] { () => ... }
// Interface[...] { case ... => ... }
def operationImplementation() = idRef() ~ maybeTypeParams() ~ implicitResume ~ functionArg() match {
def operationImplementation() = idRef() ~ maybeTypeArgs() ~ implicitResume ~ functionArg() match {
case (id ~ tps ~ k ~ BlockLiteral(_, vps, bps, body)) =>
val synthesizedId = IdRef(Nil, id.name).withPositionOf(id)
val interface = BlockTypeRef(id, Nil).withPositionOf(id): BlockTypeRef
val operation = OpClause(synthesizedId, tps, vps, bps, None, body, k).withRangeOf(id, body)
val interface = BlockTypeRef(id, tps).withPositionOf(id): BlockTypeRef
val operation = OpClause(synthesizedId, Nil, vps, bps, None, body, k).withRangeOf(id, body)
Implementation(interface, List(operation))
}

Expand Down
2 changes: 1 addition & 1 deletion effekt/shared/src/main/scala/effekt/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ object Typer extends Phase[NameResolved, Typechecked] {
case d @ source.ExternResource(id, tpe) =>
Context.bind(d.symbol)

case d @ source.InterfaceDef(id, tparams, ops, isEffect) =>
case d @ source.InterfaceDef(id, tparams, ops) =>
d.symbol.operations.foreach { op =>
if (op.effects.toList contains op.appliedInterface) {
Context.error("Bidirectional effects that mention the same effect recursively are not (yet) supported.")
Expand Down
2 changes: 1 addition & 1 deletion effekt/shared/src/main/scala/effekt/core/Transformer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
case _: source.VarDef | _: source.RegDef =>
Context.at(d) { Context.abort("Mutable variable bindings not allowed on the toplevel") }

case d @ source.InterfaceDef(id, tparamsInterface, ops, isEffect) =>
case d @ source.InterfaceDef(id, tparamsInterface, ops) =>
val interface = d.symbol
List(core.Interface(interface, interface.tparams, interface.operations.map {
case op @ symbols.Operation(name, tps, vps, bps, resultType, effects, interface) =>
Expand Down
2 changes: 1 addition & 1 deletion effekt/shared/src/main/scala/effekt/source/Tree.scala
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ enum Def extends Definition {

case NamespaceDef(id: IdDef, definitions: List[Def])

case InterfaceDef(id: IdDef, tparams: List[Id], ops: List[Operation], isEffect: Boolean = true)
case InterfaceDef(id: IdDef, tparams: List[Id], ops: List[Operation])
case DataDef(id: IdDef, tparams: List[Id], ctors: List[Constructor])
case RecordDef(id: IdDef, tparams: List[Id], fields: List[ValueParam])

Expand Down
18 changes: 15 additions & 3 deletions effekt/shared/src/main/scala/effekt/symbols/Scope.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ class Namespace(
def getNamespace(name: String): Namespace =
namespaces.getOrElseUpdate(name, Namespace.empty)

def operations: Map[String, Set[Operation]] =
types.values.toSet.flatMap {
case BlockTypeConstructor.Interface(_, _, operations) => operations.toSet
case _ => Set.empty
}.groupMap(_.name.name)(op => op)

/**
* Convert to immutable bindings
*/
Expand Down Expand Up @@ -171,10 +177,16 @@ object scopes {
def lookupOverloaded(id: IdRef, filter: TermSymbol => Boolean)(using ErrorReporter): List[Set[TermSymbol]] =
all(id.path, scope) { _.terms.getOrElse(id.name, Set.empty).filter(filter) }

def lookupOverloadedMethod(id: IdRef, filter: TermSymbol => Boolean)(using ErrorReporter): List[Set[TermSymbol]] =
all(id.path, scope) { namespace =>
// an overloaded method can be a term...
namespace.terms.getOrElse(id.name, Set.empty).filter(filter) ++
// or an operation
namespace.operations.getOrElse(id.name, Set.empty).filter(filter)
}

def lookupOperation(path: List[String], name: String)(using ErrorReporter): List[Set[Operation]] =
all(path, scope) { _.terms.getOrElse(name, Set.empty).collect {
case o: Operation => o
}}
all(path, scope) { _.operations.getOrElse(name, Set.empty) }

// can be a term OR a type symbol
def lookupFirst(path: List[String], name: String)(using E: ErrorReporter): Symbol =
Expand Down
3 changes: 1 addition & 2 deletions effekt/shared/src/main/scala/effekt/symbols/builtins.scala
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,7 @@ object builtins {
"Any" -> TopSymbol,
"Nothing" -> BottomSymbol,
"IO" -> IOSymbol,
"Region" -> RegionSymbol,
"Ref" -> TState.interface
"Region" -> RegionSymbol
)

lazy val globalRegion = ExternResource(name("global"), TRegion)
Expand Down
56 changes: 28 additions & 28 deletions examples/casestudies/buildsystem.effekt.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ This example revolves around a single effect: `Need`.
type Key = String
type Val = Int
effect Need(key: Key): Val
effect need(key: Key): Val
```

The `Need` effect operation requests the value for a specific key. In this example keys are strings and values are integers.
The `need` effect operation requests the value for a specific key. In this example keys are strings and values are integers.

A build system defines rules that specify how to build the values for each key. The values for some keys are inputs. For those we have a second effect, `NeedInput`.
A build system defines rules that specify how to build the values for each key. The values for some keys are inputs. For those we have a second effect, `needInput`.

```
effect NeedInput(key: Key): Val
effect needInput(key: Key): Val
```

With these two effect operations, we can express the rules for the spreadsheet example from the paper. We define the rules for cells `"B1"` and `"B2"` and treats the other cells as inputs.
Expand All @@ -46,22 +46,22 @@ spreadsheet:
| **2** | 20 | B1 * 2 |

```
def example1(key: Key): Val / { Need, NeedInput } = {
def example1(key: Key): Val / { need, needInput } = {
println(key);
if (key == "B1") do Need("A1") + do Need("A2")
else if (key == "B2") do Need("B1") * 2
else do NeedInput(key)
if (key == "B1") do need("A1") + do need("A2")
else if (key == "B2") do need("B1") * 2
else do needInput(key)
}
```

This example explains how to get the value for a given key. It uses the `Need` operation when it needs the result for another key and it uses the `NeedInput` operation when it needs an input.
This example explains how to get the value for a given key. It uses the `need` operation when it needs the result for another key and it uses the `needInput` operation when it needs an input.

A build system is a handler for the `Need` effect.
A build system is a handler for the `need` effect.

```
def build(target: Key) { tasks: Key => Val / { Need } }: Val / {} =
def build(target: Key) { tasks: Key => Val / { need } }: Val / {} =
try { tasks(target) }
with Need { requestedKey =>
with need { requestedKey =>
resume(build(requestedKey) { k => tasks(k) })
}
```
Expand All @@ -71,32 +71,32 @@ This handler function recursively calls itself when a key is needed. It duplicat
Another handler would memoize keys once they are built to avoid duplication.

```
effect KeyNotFound[A](key: Key): A
effect keyNotFound(key: Key): Nothing
type Store = List[(Key,Val)]
def find(store: Store, key: Key): Val / KeyNotFound = {
def find(store: Store, key: Key): Val / keyNotFound = {
store match {
case Nil() => do KeyNotFound(key)
case Nil() => do keyNotFound(key)
case Cons((k, v), xs) => if (k == key) { v } else { find(xs, key) }
}
}
```

For this we need a store, which we represent as a list of pairs of keys and values.

The `memo` handler function tries to look up the needed key in the store. If the key is found it returns the associated value. Otherwise it itself uses `Need`, stores the result, and returns the value.
The `memo` handler function tries to look up the needed key in the store. If the key is found it returns the associated value. Otherwise it itself uses `need`, stores the result, and returns the value.

```
def memo[R] { prog: => R / { Need } }: R / { Need } = {
def memo[R] { prog: => R / { need } }: R / { need } = {
var store: Store = Nil();
try {
prog()
} with Need { (key) =>
} with need { (key) =>
try {
resume(find(store, key))
} with KeyNotFound[A] { (k) =>
val v = do Need(k);
} with keyNotFound { (k) =>
val v = do need(k);
store = Cons((k, v), store);
resume(v)
}
Expand All @@ -107,22 +107,22 @@ def memo[R] { prog: => R / { Need } }: R / { Need } = {
A second example needs the same key twice.

```
// Needing the same key twice
// needing the same key twice
def example2(key: Key) = {
println(key);
if (key == "B1") do Need("A1") + do Need("A2")
else if (key == "B2") do Need("B1") * do Need("B1")
else do NeedInput(key)
if (key == "B1") do need("A1") + do need("A2")
else if (key == "B2") do need("B1") * do need("B1")
else do needInput(key)
}
```

When we run this example without memoization we will see `"B1"`, `"A1"`, and `"A2"` printed twice. When we add the `memo` handler function we do not as the result is reused.

Finally, to supply the inputs, we have a handler for the `NeedInput` effect.
Finally, to supply the inputs, we have a handler for the `needInput` effect.

```
def supplyInput[R](store: Store) { prog: => R / { NeedInput } }: R / { KeyNotFound } = {
try { prog() } with NeedInput { (key) => resume(find(store, key)) }
def supplyInput[R](store: Store) { prog: => R / { needInput } }: R / { keyNotFound } = {
try { prog() } with needInput { (key) => resume(find(store, key)) }
}
```

Expand All @@ -140,6 +140,6 @@ def main() = {
println("");
val result3 = supplyInput(inputs) { build ("B2") { (key) => memo { example2(key) } } };
println(result3)
} with KeyNotFound[A] { (key) => println("Key not found: " ++ key) }
} with keyNotFound { (key) => println("Key not found: " ++ key) }
}
```
4 changes: 2 additions & 2 deletions examples/casestudies/lexer.effekt.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def example1() = {
## Handling the Lexer Effect with a given List
A dummy lexer reading lexemes from a given list can be implemented as a handler for the `Lexer` effect. The definition uses the effect `LexerError` to signal the end of the input stream:
```
effect LexerError[A](msg: String, pos: Position): A
effect LexerError(msg: String, pos: Position): Nothing
val dummyPosition = Position(0, 0, 0)
def lexerFromList[R](l: List[Token]) { program: => R / Lexer }: R / LexerError = {
Expand All @@ -93,7 +93,7 @@ def lexerFromList[R](l: List[Token]) { program: => R / Lexer }: R / LexerError =
We define a separate handler to report lexer errors to the console:
```
def report { prog: => Unit / LexerError }: Unit =
try { prog() } with LexerError[A] { (msg, pos) =>
try { prog() } with LexerError { (msg, pos) =>
println(pos.line.show ++ ":" ++ pos.col.show ++ " " ++ msg)
}
```
Expand Down
2 changes: 1 addition & 1 deletion examples/casestudies/parser.effekt.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def parse[R](input: String) { p: => R / Parser }: ParseResult[R] = try {
case Success(res) => Success(res)
}
def fail[A](msg) = Failure(msg)
} with LexerError[A] { (msg, pos) =>
} with LexerError { (msg, pos) =>
Failure(msg)
}
```
Expand Down
16 changes: 8 additions & 8 deletions examples/llvm/choice.effekt
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@


effect Flip(): Bool
effect Fail[A](): A
effect flip(): Bool
effect fail(): Nothing

def choice(n : Int): Int / { Flip, Fail } =
def choice(n: Int): Int / { flip, fail } =
if (n < 1) {
do Fail()
} else if (do Flip()) {
do fail()
} else if (do flip()) {
n
} else {
choice(n - 1)
}

def handledTriples(n : Int, s : Int) =
def handledTriples(n: Int, s: Int) =
try {
try {
println(choice(1))
} with Fail[A] { () }
} with Flip { resume(true); resume(false) }
} with fail { () }
} with flip { resume(true); resume(false) }

def main() = handledTriples(6, 6)
19 changes: 9 additions & 10 deletions examples/llvm/triples.effekt
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@

effect Flip(): Bool
effect Fail[A](): A
effect flip(): Bool
effect fail(): Nothing

record Triple(a: Int, b: Int, c: Int)

def choice(n : Int): Int / { Flip, Fail } =
def choice(n: Int): Int / { flip, fail } =
if (n < 1) {
do Fail()
} else if (do Flip()) {
do fail()
} else if (do flip()) {
n
} else {
choice(n - 1)
Expand All @@ -20,17 +20,16 @@ def triples(n: Int, s: Int) = {
if ((i + j + k) == s) {
Triple(i, j, k)
} else {
do Fail()
do fail()
}
}

def handledTriples(n : Int, s : Int) =
def handledTriples(n: Int, s: Int) =
try {
try {
val t = triples(n, s);
println(t.a); println(t.b); println(t.c)
} with Fail[A] { () }
} with Flip { resume(true); resume(false) }
} with fail { () }
} with flip { resume(true); resume(false) }

def main() = handledTriples(100, 15)

2 changes: 1 addition & 1 deletion examples/neg/effect_not_part.effekt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module examples/pos/effect_not_part

effect Exception[A](msg: String): A
effect Exception(msg: String): Nothing

// TODO If we move `State` below `state` then it is not resolved
interface State {
Expand Down
3 changes: 0 additions & 3 deletions examples/neg/namer/issue281/issue352.effekt

This file was deleted.

10 changes: 5 additions & 5 deletions examples/neg/namer/shadowing-types.effekt
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ def bar = new Bar {
def op[Int](n) = () // WARN shadows
}

def main(): Int =
try {
def main(): Unit =
try { // WARN not to be used
//...
0
} with Foo[Int] { // WARN shadows
(x: Int) => 12
()
} with Foo[Int] {
(x: Int) => ()
}
Loading

0 comments on commit 21343a7

Please sign in to comment.