diff --git a/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala b/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala index 7f426ffa8..57d42787d 100644 --- a/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala +++ b/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala @@ -448,6 +448,37 @@ class RecursiveDescentTests extends munit.FunSuite { with Eff3 { def op(x, y) = { x } def op2() = { () }} """ ) + parseTry( + """try { 42 } + with Empty {} + suspend { println("hello") } + resume { x => println("resuming...") } + return { x => x} + """ + ) + parseTry( + """try 42 with Empty {} + return { (x: Int) => x } + """ + ) + parseTry( + """try 42 + return { (x: Int) => x } + """ + ) + parseTry( + """try "a" + with Empty {} + finally { _ => println("done") } + """ + ) + intercept[Throwable] { + parseTry( + """try { 42 } + resume { println("resuming") } + """ + ) + } } test("Type definition") { diff --git a/effekt/shared/src/main/scala/effekt/Lexer.scala b/effekt/shared/src/main/scala/effekt/Lexer.scala index 53f52c991..90dad9825 100644 --- a/effekt/shared/src/main/scala/effekt/Lexer.scala +++ b/effekt/shared/src/main/scala/effekt/Lexer.scala @@ -108,12 +108,15 @@ enum TokenKind { case `is` case `namespace` case `pure` + case `suspend` + case `finally` } object TokenKind { // "Soft" keywords val `resume` = TokenKind.Ident("resume") val `in` = TokenKind.Ident("in") val `at` = TokenKind.Ident("at") + val `on` = TokenKind.Ident("on") val `__` = Ident("_") def explain(kind: TokenKind): String = kind match { @@ -135,7 +138,8 @@ object TokenKind { `let`, `true`, `false`, `val`, `var`, `if`, `else`, `while`, `type`, `effect`, `interface`, `try`, `with`, `case`, `do`, `fun`, `match`, `def`, `module`, `import`, `export`, `extern`, `include`, `record`, `box`, `unbox`, `return`, `region`, `resource`, `new`, `and`, `is`, - `namespace`, `pure`) + `namespace`, `pure`, `suspend`, `resume`, `finally`, `on` + ) } diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index d97dd4fe4..a5fb0d8e5 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -397,7 +397,7 @@ object Namer extends Phase[Parsed, NameResolved] { case source.BlockStmt(block) => Context scoped { resolveGeneric(block) } - case tree @ source.TryHandle(body, handlers) => + case tree @ source.TryHandle(body, handlers, suspend, resume, ret) => resolveAll(handlers) Context scoped { @@ -411,6 +411,9 @@ object Namer extends Phase[Parsed, NameResolved] { resolveGeneric(body) } + Context scoped { suspend foreach { resolveGeneric } } + Context scoped { resume foreach { resolveGeneric } } + Context scoped { ret foreach { resolveGeneric } } case tree @ source.Region(name, body) => val reg = BlockParam(Name.local(name.name), Some(builtins.TRegion)) @@ -586,6 +589,14 @@ object Namer extends Phase[Parsed, NameResolved] { case leaf => () } + def resolve(c: source.FinalizerClause)(using Context): Unit = { + val source.FinalizerClause(vp, body) = c + Context.scoped { + vp foreach { case v => Context.bind(resolve(v)) } + resolveGeneric(body) + } + } + /** * Resolve Parameters as part of resolving function signatures * diff --git a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala index 8dcffbeb7..be47150c3 100644 --- a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala +++ b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala @@ -659,8 +659,17 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) */ def tryExpr(): Term = nonterminal: - `try` ~> stmt() ~ someWhile(handler(), `with`) match { - case s ~ hs => TryHandle(s, hs) + `try` ~> stmt() ~ manyWhile(handler(), `with`) ~ finalizerClause(`on` ~> consume(`suspend`), "suspend", false) ~ finalizerClause(`on` ~> consume(`resume`), "resume", true) ~ finalizerClause(`on` ~> consume(`return`), "return", true) ~ finalizerClause(consume(`finally`), "finally", false) match { + case _ ~ _ ~ None ~ Some(_) ~ _ ~ None => + fail("Got `resume` clause but no `suspend` clause.") + case _ ~ _ ~ Some(_) ~ _ ~ _ ~ Some(_) => + fail("Got both an `suspend` and `finally` clause.") + case _ ~ _ ~ _ ~ _ ~ Some(_) ~ Some(_) => + fail("got both an `return` and `finally` clause.") + case s ~ hs ~ susp ~ None ~ None ~ Some(fin) => + TryHandle(s, hs, Some(fin), None, Some(FinalizerClause(Some(ValueParam(IdDef("x"), None)), fin.body))) + case s ~ hs ~ susp ~ res ~ retrn ~ fin => + TryHandle(s, hs, susp, res, retrn) } def regionExpr(): Term = @@ -698,6 +707,25 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) Handler(capability, impl) } + def finalizerClause[T](prefix: => T, name: String, vparam: Boolean): Option[FinalizerClause] = + nonterminal: + backtrack { prefix } + .map { _ => + braces { + backtrack { lambdaParams() <~ `=>` } ~ stmts() match { + // TODO better erros + case Some(Nil, vps, Nil) ~ body => + if (!vps.isEmpty != vparam) fail(s"$name value parameter mismatch") + else FinalizerClause(vps.headOption, body) + case Some(_, _, _) ~ _ => + fail(s"$name only expects value parameters") + case None ~ body => + if (vparam) fail(s"$name expects one value parameter but none were found") + else FinalizerClause(None, body) + } + } + } + // This nonterminal uses limited backtracking: It parses the interface type multiple times. def implementation(): Implementation = nonterminal: diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index 48849181c..7f2fea608 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -210,7 +210,14 @@ object Typer extends Phase[NameResolved, Typechecked] { checkStmt(body, expected) } - case tree @ source.TryHandle(prog, handlers) => + case tree @ source.TryHandle(prog, handlers, suspend, resume, retrn) => + /* + try { _: A } + with Eff { (x: B, resume: C => D) => _: D } + suspend { _: E } + resume { (x: E) => _: Unit } + return { (x: A) => _: D } + */ // (1) extract all handled effects and capabilities val providedCapabilities: List[symbols.BlockParam] = handlers map Context.withFocus { h => @@ -226,13 +233,57 @@ object Typer extends Phase[NameResolved, Typechecked] { // All used captures flow into the continuation capture, except the ones handled by this handler. val continuationCaptHandled = Context.without(continuationCapt, providedCapabilities.map(_.capture)) + + val Result(res, _) = Context.bindingCapabilities(tree, providedCapabilities) { + flowingInto(continuationCaptHandled) { + checkStmt(prog, expected) + } + } + + val Result(suspendType, suspendEffs) = suspend.map { + case source.FinalizerClause(None, body) => + checkStmt(body, None) + case source.FinalizerClause(Some(_), _) => + INTERNAL_ERROR("Parser should ensure that the suspend clause has no value parameter") + }.getOrElse { Result(TUnit, Pure) } + + val Result(resumeType, resumeEffs) = resume.map { + case source.FinalizerClause(Some(vp), body) => + // check potentially annotated parameter type matches type of suspend clause + vp.symbol.tpe.foreach { matchExpected(_, suspendType) } + Context.bind(vp.symbol, suspendType) + // ensure that the body returns a result of type Unit + checkStmt(body, Some(TUnit)) + case source.FinalizerClause(None, _) => + INTERNAL_ERROR("Parser should ensure that the resume clause has a value parameter") + }.getOrElse { Result(TUnit, Pure) } + + val Result(ret, retEffs) = retrn.map { + case source.FinalizerClause(Some(vp), body) => + // check potentially annotated parameter type matches type of try body + vp.symbol.tpe.foreach { matchExpected(_, res) } + Context.bind(vp.symbol, res) + checkStmt(body, expected) + case source.FinalizerClause(None, _) => + INTERNAL_ERROR("Parser should ensure that the return clause has a value parameter") + }.getOrElse { + // (2) Check the handled program + val Result(ret, effs) = Context.bindingCapabilities(tree, providedCapabilities) { + flowingInto(continuationCaptHandled) { + checkStmt(prog, expected) + } + } + Result(ret, Pure) + } + + val finalizerEffs = suspendEffs ++ resumeEffs ++ retEffs // (2) Check the handled program - val Result(ret, effs) = Context.bindingCapabilities(tree, providedCapabilities) { + val Result(_, effs) = Context.bindingCapabilities(tree, providedCapabilities) { flowingInto(continuationCaptHandled) { checkStmt(prog, expected) } - } + } // (3) Check the handlers // Also all capabilities used by the handler flow into the capture of the continuation diff --git a/effekt/shared/src/main/scala/effekt/core/Inline.scala b/effekt/shared/src/main/scala/effekt/core/Inline.scala index 18f91c607..d838f3a38 100644 --- a/effekt/shared/src/main/scala/effekt/core/Inline.scala +++ b/effekt/shared/src/main/scala/effekt/core/Inline.scala @@ -98,8 +98,7 @@ object Inline { def blockDefFor(id: Id)(using ctx: InlineContext): Option[Block] = ctx.defs.get(id) map { - // TODO rewriting here leads to a stack overflow in one test, why? - case Definition.Def(id, block) => block //rewrite(block) + case Definition.Def(id, block) => rewrite(block) case Definition.Let(id, _, binding) => INTERNAL_ERROR("Should not happen") } @@ -131,11 +130,10 @@ object Inline { case Stmt.Invoke(b, method, methodTpe, targs, vargs, bargs) => invoke(rewrite(b), method, methodTpe, targs, vargs.map(rewrite), bargs.map(rewrite)) - case Stmt.Reset(body) => - rewrite(body) match { - case BlockLit(tparams, cparams, vparams, List(prompt), body) if !used(prompt.id) => body - case b => Stmt.Reset(b) - } + case Stmt.Reset(body, onSuspend, onResume, onReturn) => + // conjecture: we cannot inline the body of a reset clause, since even if the prompt is unused, we still need to + // account for on suspend and resume clauses when a shift to another prompt occurs. + Stmt.Reset(rewrite(body), onSuspend.map { rewrite }, onResume.map { rewrite }, onReturn.map { rewrite }) // congruences case Stmt.Return(expr) => Return(rewrite(expr)) @@ -144,9 +142,10 @@ object Inline { case Stmt.Match(scrutinee, clauses, default) => patternMatch(rewrite(scrutinee), clauses.map { case (id, value) => id -> rewrite(value) }, default.map(rewrite)) case Stmt.Alloc(id, init, region, body) => Alloc(id, rewrite(init), region, rewrite(body)) - case Stmt.Shift(prompt, b @ BlockLit(tparams, cparams, vparams, List(k), body)) if tailResumptive(k.id, body) => - C.inlineCount.next() - rewrite(removeTailResumption(k.id, body)) + // Note: We cannot inline tail resumptive shifts, otherwise and the suspend and resume clauses are not executed + // case Stmt.Shift(prompt, b @ BlockLit(tparams, cparams, vparams, List(k), body)) if tailResumptive(k.id, body) => + // C.inlineCount.next() + // rewrite(removeTailResumption(k.id, body)) case Stmt.Shift(prompt, body) => Shift(prompt, rewrite(body)) @@ -238,7 +237,7 @@ object Inline { case Stmt.Var(id, init, capture, body) => tailResumptive(k, body) && !freeInExpr(init) case Stmt.Get(id, annotatedCapt, annotatedTpe) => false case Stmt.Put(id, annotatedCapt, value) => false - case Stmt.Reset(BlockLit(tparams, cparams, vparams, bparams, body)) => tailResumptive(k, body) // is this correct? + case Stmt.Reset(body, onSuspend, onResume, onReturn) => onReturn.map { blocklit => tailResumptive(k, blocklit.body) }.getOrElse(tailResumptive(k, body.body)) case Stmt.Shift(prompt, body) => false case Stmt.Resume(k2, body) => k2.id == k // what if k is free in body? case Stmt.Hole() => true @@ -256,7 +255,10 @@ object Inline { case Stmt.Region(_) => ??? case Stmt.Alloc(id, init, region, body) => Stmt.Alloc(id, init, region, removeTailResumption(k, body)) case Stmt.Var(id, init, capture, body) => Stmt.Var(id, init, capture, removeTailResumption(k, body)) - case Stmt.Reset(body) => Stmt.Reset(removeTailResumption(k, body)) + case Stmt.Reset(body, suspend, resume, Some(ret)) => + Stmt.Reset(body, resume, suspend, Some(removeTailResumption(k, ret))) + case Stmt.Reset(body, suspend, resume, None) => + Stmt.Reset(removeTailResumption(k, body), suspend, resume, None) case Stmt.Resume(k2, body) if k2.id == k => body case Stmt.Resume(k, body) => stmt diff --git a/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala b/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala index c2057425c..ababdd672 100644 --- a/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala +++ b/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala @@ -338,8 +338,8 @@ object PolymorphismBoxing extends Phase[CoreTransformed, CoreTransformed] { Stmt.Alloc(id, transform(init), region, transform(body)) case Stmt.Var(id, init, cap, body) => Stmt.Var(id, transform(init), cap, transform(body)) - case Stmt.Reset(body) => - Stmt.Reset(transform(body)) + case Stmt.Reset(body, onSuspend, onResume, onReturn) => + Stmt.Reset(transform(body), onSuspend.map { transform }, onResume.map { transform }, onReturn.map { transform }) case Stmt.Shift(prompt, body) => Stmt.Shift(prompt, transform(body)) case Stmt.Resume(k, body) => diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index 96d11ed33..2658ad93b 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -185,8 +185,8 @@ object PrettyPrinter extends ParenPrettyPrinter { case If(cond, thn, els) => "if" <+> parens(toDoc(cond)) <+> block(toDoc(thn)) <+> "else" <+> block(toDoc(els)) - case Reset(body) => - "reset" <+> toDoc(body) + case Reset(body, onSuspend, onResume, onReturn) => + "reset" <+> toDoc(body) <+> "on suspend" <+> onSuspend.map { toDoc }.getOrElse("{}") <+> "on resume" <+> onResume.map { toDoc }.getOrElse("{}") <+> "on return" <+> onReturn.map { toDoc }.getOrElse("{}") case Shift(prompt, body) => "shift" <> parens(toDoc(prompt)) <+> toDoc(body) diff --git a/effekt/shared/src/main/scala/effekt/core/Reachable.scala b/effekt/shared/src/main/scala/effekt/core/Reachable.scala index 3d51c94f0..590554d3d 100644 --- a/effekt/shared/src/main/scala/effekt/core/Reachable.scala +++ b/effekt/shared/src/main/scala/effekt/core/Reachable.scala @@ -88,7 +88,11 @@ class Reachable( process(body) case Stmt.Get(id, capt, tpe) => process(id) case Stmt.Put(id, tpe, value) => process(id); process(value) - case Stmt.Reset(body) => process(body) + case Stmt.Reset(body, onSuspend, onResume, onReturn) => + process(body) + onSuspend.foreach { process } + onResume.foreach { process } + onReturn.foreach { process } case Stmt.Shift(prompt, body) => process(prompt); process(body) case Stmt.Resume(k, body) => process(k); process(body) case Stmt.Region(body) => process(body) diff --git a/effekt/shared/src/main/scala/effekt/core/Recursive.scala b/effekt/shared/src/main/scala/effekt/core/Recursive.scala index 1e197c89a..bae8b9ad8 100644 --- a/effekt/shared/src/main/scala/effekt/core/Recursive.scala +++ b/effekt/shared/src/main/scala/effekt/core/Recursive.scala @@ -81,7 +81,11 @@ class Recursive( process(body) case Stmt.Get(id, capt, tpe) => () case Stmt.Put(id, tpe, value) => process(value) - case Stmt.Reset(body) => process(body) + case Stmt.Reset(body, suspend, resume, ret) => + process(body) + suspend.foreach { process } + resume.foreach { process } + ret.foreach { process } case Stmt.Shift(prompt, body) => process(prompt); process(body) case Stmt.Resume(k, body) => process(k); process(body) case Stmt.Region(body) => process(body) diff --git a/effekt/shared/src/main/scala/effekt/core/StaticArguments.scala b/effekt/shared/src/main/scala/effekt/core/StaticArguments.scala index 4aabbf42d..f60406849 100644 --- a/effekt/shared/src/main/scala/effekt/core/StaticArguments.scala +++ b/effekt/shared/src/main/scala/effekt/core/StaticArguments.scala @@ -137,11 +137,9 @@ object StaticArguments { case Stmt.Invoke(b, method, methodTpe, targs, vargs, bargs) => invoke(rewrite(b), method, methodTpe, targs, vargs.map(rewrite), bargs.map(rewrite)) - case Stmt.Reset(body) => - rewrite(body) match { - case b => Stmt.Reset(b) - } - + case Stmt.Reset(body, suspend, resume, ret) => + Stmt.Reset(rewrite(body), suspend.map { rewrite }, resume.map { rewrite }, ret.map { rewrite }) + // congruences case Stmt.Return(expr) => Return(rewrite(expr)) case Stmt.Val(id, tpe, binding, body) => valDef(id, tpe, rewrite(binding), rewrite(body)) diff --git a/effekt/shared/src/main/scala/effekt/core/Transformer.scala b/effekt/shared/src/main/scala/effekt/core/Transformer.scala index c32774a1f..fe2800c5f 100644 --- a/effekt/shared/src/main/scala/effekt/core/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/core/Transformer.scala @@ -425,9 +425,13 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { val compiledMatch = PatternMatchingCompiler.compile(clauses ++ defaultClause) Context.bind(compiledMatch) - case source.TryHandle(prog, handlers) => + case source.TryHandle(prog, handlers, suspend, resume, retrn) => val transformedProg = transform(prog) + // TODO + val transformedSuspend = suspend.map { transform } + val transformedResume = resume.map { transform } + val transformedReturn = retrn.map { transform } val answerType = transformedProg.tpe @@ -445,9 +449,8 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { } val body: BlockLit = BlockLit(Nil, List(promptCapt), Nil, List(promptParam), - Scope(transformedHandlers, transform(prog))) - - Context.bind(Reset(body)) + Scope(transformedHandlers, transformedProg)) + Context.bind(Reset(body, transformedSuspend, transformedResume, transformedReturn)) case r @ source.Region(name, body) => val region = r.symbol @@ -641,6 +644,11 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { }) } + def transform(clause: source.FinalizerClause)(using Context): BlockLit = { + val source.FinalizerClause(vp, prog) = clause + BlockLit(Nil, Nil, vp.map { transform(_) }.toList, Nil, transform(prog)) + } + def preprocess(sc: ValueVar, clause: source.MatchClause)(using Context): Clause = preprocess(List((sc, clause.pattern)), clause.guards, transform(clause.body)) diff --git a/effekt/shared/src/main/scala/effekt/core/Tree.scala b/effekt/shared/src/main/scala/effekt/core/Tree.scala index 24d4cdfe9..cadf6260f 100644 --- a/effekt/shared/src/main/scala/effekt/core/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/core/Tree.scala @@ -316,7 +316,7 @@ enum Stmt extends Tree { // binds a fresh prompt as [[id]] in [[body]] and delimits the scope of captured continuations // Reset({ [cap]{p: Prompt[answer] at cap} => stmt: answer}): answer - case Reset(body: BlockLit) + case Reset(body: BlockLit, onSuspend: Option[BlockLit], onResume: Option[BlockLit], onReturn: Option[BlockLit]) // captures the continuation up to the given prompt // Invariant, it always has the shape: @@ -395,12 +395,6 @@ object normal { case other => Invoke(callee, method, methodTpe, targs, vargs, bargs) } - def reset(body: BlockLit): Stmt = body match { - // case BlockLit(tparams, cparams, vparams, List(prompt), - // Stmt.Shift(prompt2, body) if prompt.id == prompt2.id => ??? - case other => Stmt.Reset(body) - } - def make(tpe: ValueType.Data, tag: Id, vargs: List[Pure]): Pure = Pure.Make(tpe, tag, vargs) @@ -711,7 +705,7 @@ object Variables { case Stmt.Put(id, annotatedCapt, value) => Variables.block(id, core.Type.TState(value.tpe), annotatedCapt) ++ free(value) - case Stmt.Reset(body) => free(body) + case Stmt.Reset(body, onSuspend, onResume, onReturn) => free(body) ++ all(onSuspend, free) ++ all(onResume, free) ++ all(onReturn, free) case Stmt.Shift(prompt, body) => free(prompt) ++ free(body) case Stmt.Resume(k, body) => free(k) ++ free(body) case Stmt.Hole() => Variables.empty @@ -822,8 +816,13 @@ object substitutions { case Put(id, capt, value) => Put(substituteAsVar(id), substitute(capt), substitute(value)) - case Reset(body) => - Reset(substitute(body).asInstanceOf[BlockLit]) + case Reset(body, onSuspend, onResume, onReturn) => + Reset( + substitute(body).asInstanceOf[BlockLit], + onSuspend.map { substitute(_).asInstanceOf[BlockLit] }, + onResume.map { substitute(_).asInstanceOf[BlockLit] }, + onReturn.map { substitute(_).asInstanceOf[BlockLit] }, + ) case Shift(prompt, body) => val after = substitute(body).asInstanceOf[BlockLit] diff --git a/effekt/shared/src/main/scala/effekt/core/Type.scala b/effekt/shared/src/main/scala/effekt/core/Type.scala index 2b94fc0ea..2a7e28c27 100644 --- a/effekt/shared/src/main/scala/effekt/core/Type.scala +++ b/effekt/shared/src/main/scala/effekt/core/Type.scala @@ -205,7 +205,7 @@ object Type { case Stmt.Var(id, init, cap, body) => body.tpe case Stmt.Get(id, capt, tpe) => tpe case Stmt.Put(id, capt, value) => TUnit - case Stmt.Reset(body) => body.returnType + case Stmt.Reset(body, onSuspend, onResume, onReturn) => onReturn.map { _.returnType }.getOrElse(body.returnType) case Stmt.Shift(prompt, body) => body.bparams match { case core.BlockParam(id, BlockType.Interface(ResumeSymbol, List(result, answer)), captures) :: Nil => result case _ => ??? @@ -236,7 +236,7 @@ object Type { case Stmt.Var(id, init, cap, body) => body.capt -- Set(cap) case Stmt.Get(id, capt, tpe) => capt case Stmt.Put(id, capt, value) => capt - case Stmt.Reset(body) => body.capt + case Stmt.Reset(body, onSuspend, onResume, onReturn) => body.capt ++ onSuspend.toSet.flatMap(_.capt) ++ onResume.toSet.flatMap(_.capt) ++ onReturn.toSet.flatMap(_.capt) case Stmt.Shift(prompt, body) => prompt.capt ++ body.capt case Stmt.Resume(k, body) => k.capt ++ body.capt case Stmt.Region(body) => body.capt diff --git a/effekt/shared/src/main/scala/effekt/cps/Transformer.scala b/effekt/shared/src/main/scala/effekt/cps/Transformer.scala index a1e1d6cc9..bd86d6805 100644 --- a/effekt/shared/src/main/scala/effekt/cps/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/cps/Transformer.scala @@ -114,13 +114,18 @@ object Transformer { default.map(transform(_, ks, k))) } - case core.Stmt.Reset(core.Block.BlockLit(_, _, _, prompt :: Nil, body)) => + case core.Stmt.Reset(core.Block.BlockLit(_, _, _, prompt :: Nil, body), onSuspend, onResume, onReturn) => val ks2 = Id("ks") val k2 = Id("k") - Reset(Block.BlockLit(Nil, List(prompt.id), ks2, k2, transform(body, ks2, Continuation.Dynamic(k2))), - MetaCont(ks), k.reify) - - case core.Stmt.Reset(body) => sys error "Shouldn't happen" + Reset( + Block.BlockLit(Nil, List(prompt.id), ks2, k2, transform(body, ks2, Continuation.Dynamic(k2))), + onSuspend.map { transformBlockLit }, + onResume.map { transformBlockLit }, + onReturn.map { transformBlockLit }, + MetaCont(ks), k.reify + ) + + case core.Stmt.Reset(body, _, _, _) => sys error "Shouldn't happen" // Only unidirectional, yet // core.Block.BlockLit(tparams, cparams, vparams, List(resume), body) diff --git a/effekt/shared/src/main/scala/effekt/cps/Tree.scala b/effekt/shared/src/main/scala/effekt/cps/Tree.scala index a2dbb18c9..3cfb7bdff 100644 --- a/effekt/shared/src/main/scala/effekt/cps/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/cps/Tree.scala @@ -134,7 +134,7 @@ enum Stmt extends Tree { case Put(ref: Id, value: Pure, body: Stmt) // reset( { (p, ks, k) => STMT }, ks, k) - case Reset(prog: BlockLit, ks: MetaCont, k: Cont) + case Reset(prog: BlockLit, onSuspend: Option[BlockLit], onResume: Option[BlockLit], onReturn: Option[BlockLit], ks: MetaCont, k: Cont) // shift(p, { (resume, ks, k) => STMT }, ks, k) case Shift(prompt: Id, body: BlockLit, ks: MetaCont, k: Cont) @@ -220,7 +220,7 @@ object Variables { case Stmt.Get(ref, id, body) => block(ref) ++ (free(body) -- value(id)) case Stmt.Put(ref, value, body) => block(ref) ++ free(value) ++ free(body) - case Stmt.Reset(prog, ks, k) => free(prog) ++ free(ks) ++ free(k) + case Stmt.Reset(prog, onSuspend, onResume, onReturn, ks, k) => free(prog) ++ all(onSuspend, free) ++ all(onResume, free) ++ all(onReturn, free) ++ free(ks) ++ free(k) case Stmt.Shift(prompt, body, ks, k) => block(prompt) ++ free(body) ++ free(ks) ++ free(k) case Stmt.Resume(r, body, ks, k) => block(r) ++ free(body) ++ free(ks) ++ free(k) case Stmt.Hole() => empty diff --git a/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala index ab983ef7b..fb8248eae 100644 --- a/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala @@ -110,7 +110,7 @@ trait Transformer { case Alloc(id, init, region, body) => chez.Let(List(Binding(nameDef(id), chez.Builtin("fresh", chez.Variable(nameRef(region)), toChez(init)))), toChez(body)) - case Reset(body) => chez.Reset(toChez(body)) + case Reset(body, _, _, _) => chez.Reset(toChez(body)) case Shift(p, body) => chez.Shift(nameRef(p.id), toChez(body)) diff --git a/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala b/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala index 7478c41dd..87bedac96 100644 --- a/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala +++ b/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala @@ -252,14 +252,17 @@ object TransformerCps extends Transformer { toJS(body).run(k) } - case cps.Stmt.Reset(prog, ks, k) => - pure(js.Return(Call(RESET, toJS(prog), toJS(ks), toJS(k)))) + case cps.Stmt.Reset(prog, onSuspend, onResume, onReturn, ks, k) => + val jsSuspend = onSuspend.map { toJS }.getOrElse(js.RawExpr("null")) + val jsResume = onResume.map { toJS }.getOrElse(js.RawExpr("null")) + val jsReturn = onReturn.map { toJS }.getOrElse(js.RawExpr("null")) + pure(js.Return(Call(RESET, toJS(prog), jsSuspend, jsResume, jsReturn, toJS(ks), toJS(k)))) case cps.Stmt.Shift(prompt, body, ks, k) => - pure(js.Return(Call(SHIFT, nameRef(prompt), noThunking { toJS(body) }, toJS(ks), toJS(k)))) + pure(js.Return(Call(SHIFT, nameRef(prompt), noThunking { toJS(body) }, toJS(ks), toJS(k), js.RawExpr("null")))) case cps.Stmt.Resume(r, b, ks2, k2) => - pure(js.Return(js.Call(RESUME, nameRef(r), toJS(b), toJS(ks2), toJS(k2)))) + pure(js.Return(js.Call(RESUME, nameRef(r), toJS(b), js.RawLiteral("false"), toJS(ks2), toJS(k2)))) case cps.Stmt.Hole() => pure(js.Return($effekt.call("hole"))) diff --git a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala index 34cd60ae5..9e8ed51e0 100644 --- a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala @@ -229,7 +229,7 @@ object Transformer { Switch(value, transformedClauses, transformedDefault) } - case core.Reset(core.BlockLit(Nil, cparams, Nil, List(prompt), body)) => + case core.Reset(core.BlockLit(Nil, cparams, Nil, List(prompt), body), _, _, _) => noteParameters(List(prompt)) val variable = Variable(freshName("returned"), transform(body.tpe)) diff --git a/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala b/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala index 2f7e520e9..75b916a03 100644 --- a/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala +++ b/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala @@ -55,11 +55,15 @@ object AnnotateCaptures extends Phase[Typechecked, Typechecked], Query[Unit, Cap val cap = Context.annotation(Annotations.CapabilityReceiver, t) combineAll(vargs.map(query)) ++ combineAll(bargs.map(query)) ++ CaptureSet(cap.capture) - case t @ source.TryHandle(prog, handlers) => + case t @ source.TryHandle(prog, handlers, suspend, resume, retrn) => val progCapture = query(prog) + val suspCapture = suspend map { query } getOrElse CaptureSet() + val resCapture = resume map { query } getOrElse CaptureSet() + val retCapture = retrn map { query } getOrElse CaptureSet() + val boundCapture = boundCapabilities(t) val usedCapture = combineAll(handlers.map(query)) - (progCapture -- boundCapture) ++ usedCapture + (progCapture -- boundCapture) ++ usedCapture ++ suspCapture ++ resCapture ++ retCapture case t @ source.Region(id, body) => query(body) -- captureOf(id.symbol.asBlockSymbol) diff --git a/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala b/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala index 1816ad48c..67709a906 100644 --- a/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala +++ b/effekt/shared/src/main/scala/effekt/source/ExplicitCapabilities.scala @@ -91,8 +91,11 @@ object ExplicitCapabilities extends Phase[Typechecked, Typechecked], Rewrite { Call(receiver, typeArgs, valueArgs, blockArgs ++ capabilityArgs) - case h @ TryHandle(prog, handlers) => + case h @ TryHandle(prog, handlers, suspend, resume, retrn) => val body = rewrite(prog) + val susp = suspend map { rewrite } + val res = resume map { rewrite } + val ret = retrn map { rewrite } val capabilities = Context.annotation(Annotations.BoundCapabilities, h) @@ -107,7 +110,7 @@ object ExplicitCapabilities extends Phase[Typechecked, Typechecked], Rewrite { } } - TryHandle(body, hs) + TryHandle(body, hs, susp, res, ret) case n @ source.New(impl @ Implementation(interface, clauses)) => { val cs = clauses map { @@ -129,6 +132,11 @@ object ExplicitCapabilities extends Phase[Typechecked, Typechecked], Rewrite { source.BlockLiteral(tps, vps, bps ++ capParams, rewrite(body)) } + def rewrite(c: FinalizerClause)(using Context): FinalizerClause = { + val FinalizerClause(vp, body) = c + FinalizerClause(vp, rewrite(body)) + } + override def rewrite(body: ExternBody)(using context.Context): ExternBody = body match { case b @ source.ExternBody.StringExternBody(ff, body) => diff --git a/effekt/shared/src/main/scala/effekt/source/Tree.scala b/effekt/shared/src/main/scala/effekt/source/Tree.scala index 194b551e2..7116d7173 100644 --- a/effekt/shared/src/main/scala/effekt/source/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/source/Tree.scala @@ -393,7 +393,8 @@ enum Term extends Tree { * * Each with-clause is modeled as an instance of type [[Handler]]. */ - case TryHandle(prog: Stmt, handlers: List[Handler]) + case TryHandle(prog: Stmt, handlers: List[Handler], suspend: Option[FinalizerClause], resume: Option[FinalizerClause], retrn: Option[FinalizerClause]) + case Region(id: IdDef, body: Stmt) extends Term, Definition /** @@ -461,6 +462,8 @@ case class Handler(capability: Option[BlockParam] = None, impl: Implementation) // currently the annotation is rejected by [[Typer]] -- after that phase, `ret` should always be `None` case class OpClause(id: IdRef, tparams: List[Id], vparams: List[ValueParam], bparams: List[BlockParam], ret: Option[Effectful], body: Stmt, resume: IdDef) extends Reference +case class FinalizerClause(vparam: Option[ValueParam], body: Stmt) extends Tree + // Pattern Matching // ---------------- @@ -787,6 +790,7 @@ object Tree { def query(h: Handler)(using Context, Ctx): Res = structuralQuery(h) def query(h: Implementation)(using Context, Ctx): Res = structuralQuery(h) def query(h: OpClause)(using Context, Ctx): Res = structuralQuery(h) + def query(c: FinalizerClause)(using Context, Ctx): Res = structuralQuery(c) def query(c: MatchClause)(using Context, Ctx): Res = structuralQuery(c) def query(c: MatchGuard)(using Context, Ctx): Res = structuralQuery(c) def query(b: ExternBody)(using Context, Ctx): Res = structuralQuery(b) diff --git a/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala b/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala index 7b0a47daf..f95ac63ac 100644 --- a/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala +++ b/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala @@ -106,8 +106,12 @@ object BoxUnboxInference extends Phase[NameResolved, NameResolved] { Call(IdTarget(id).inheritPosition(id), targs, rewriteAsExpr(receiver) :: vargsTransformed, bargsTransformed) } - case TryHandle(prog, handlers) => - TryHandle(rewrite(prog), handlers.map(rewrite)) + case TryHandle(prog, handlers, suspend, resume, retrn) => + val rewrittenSusp = suspend.map { rewrite } + val rewrittenRes = resume.map { rewrite } + val rewrittenRet = retrn.map { rewrite } + + TryHandle(rewrite(prog), handlers.map(rewrite), rewrittenSusp, rewrittenRes, rewrittenRet) case Region(name, body) => Region(name, rewrite(body)) @@ -204,6 +208,11 @@ object BoxUnboxInference extends Phase[NameResolved, NameResolved] { OpClause(id, tparams, vparams, bparams, ret, rewrite(body), resume) } + def rewrite(c: FinalizerClause)(using Context): FinalizerClause = visit(c) { + case FinalizerClause(vp, body) => + FinalizerClause(vp, rewrite(body)) + } + def rewrite(c: MatchClause)(using Context): MatchClause = visit(c) { case MatchClause(pattern, guards, body) => MatchClause(pattern, guards.map(rewrite), rewrite(body)) diff --git a/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala b/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala index ecfd7330b..a92f5c122 100644 --- a/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala +++ b/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala @@ -125,7 +125,7 @@ object Wellformedness extends Phase[Typechecked, Typechecked], Visit[WFContext] /** * For handlers we check that the return type does not mention any bound capabilities */ - case tree @ source.TryHandle(prog, handlers) => + case tree @ source.TryHandle(prog, handlers, suspend, resume, retrn) => val bound = Context.annotation(Annotations.BoundCapabilities, tree).map(_.capture).toSet val usedEffects = Context.annotation(Annotations.InferredEffect, tree) val tpe = Context.inferredTypeOf(prog) @@ -136,6 +136,9 @@ object Wellformedness extends Phase[Typechecked, Typechecked], Visit[WFContext] binding(captures = bound) { query(prog) } handlers foreach { query } + suspend foreach { query } + resume foreach { query } + retrn foreach { query } // Now check the return type ... wellformed(tpe, prog, " inferred as return type of the handled statement", diff --git a/examples/neg/unresolved_type.effekt b/examples/neg/unresolved_type.effekt index ceb0c7ef6..d008fb859 100644 --- a/examples/neg/unresolved_type.effekt +++ b/examples/neg/unresolved_type.effekt @@ -1,10 +1,10 @@ interface Defer { - def defer[A, B] { prog: () => A } { finally: A => B }: B + def defer[A, B] { prog: () => A } { fin: A => B }: B } def impl = new Defer { - def defer { prog: () => A } { finally: A => B }: B = { // ERROR Could not resolve type A + def defer { prog: () => A } { fin: A => B }: B = { // ERROR Could not resolve type A val res = prog() - finally(res) + fin(res) } } diff --git a/examples/pos/finalizerSemantics.check b/examples/pos/finalizerSemantics.check new file mode 100644 index 000000000..c04ffd326 --- /dev/null +++ b/examples/pos/finalizerSemantics.check @@ -0,0 +1,9 @@ +suspending inner +Eff +Eff +resuming inner +Eff +returning inner +Eff +returning outer +87 diff --git a/examples/pos/finalizerSemantics.effekt b/examples/pos/finalizerSemantics.effekt new file mode 100644 index 000000000..c2bec4de9 --- /dev/null +++ b/examples/pos/finalizerSemantics.effekt @@ -0,0 +1,31 @@ +effect Eff(): Unit + +def main() = { + val x = + try { + try { + do Eff() + 42 + } on suspend { + println("suspending inner") + do Eff() + } on resume { _ => + println("resuming inner") + do Eff() + } on return { x => + println("returning inner") + do Eff() + x * 2 + } + } with Eff { () => println("Eff"); resume(()) } + on suspend { + println("suspending outer") + } on resume { + _ => println("resuming outer") + } on return { x => + println("returning outer") + x + 3 + } + println(x) + () +} diff --git a/libraries/js/effekt_runtime.js b/libraries/js/effekt_runtime.js index b1b5ff2e1..f3af4aacc 100644 --- a/libraries/js/effekt_runtime.js +++ b/libraries/js/effekt_runtime.js @@ -1,4 +1,3 @@ - // Common Runtime // -------------- function Cell(init, region) { @@ -63,7 +62,7 @@ function Arena(_region) { let _prompt = 1; const TOPLEVEL_K = (x, ks) => { throw { computationIsDone: true, result: x } } -const TOPLEVEL_KS = { prompt: 0, arena: Arena([]), rest: null } +const TOPLEVEL_KS = { prompt: 0, arena: Arena([]), onSuspend: null, onSuspendData: null, onResume: null, rest: null } function THUNK(f) { f.thunk = true @@ -84,10 +83,11 @@ const RETURN = (x, ks) => ks.rest.stack(x, ks.rest) // const x = r.alloc(init); body // HANDLE(ks, ks, (p, ks, k) => { STMT }) -function RESET(prog, ks, k) { +function RESET(prog, onSuspend, onResume, onReturn, ks, k) { const prompt = _prompt++; - const rest = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } - return prog(prompt, { prompt, arena: Arena([]), rest }, RETURN) + const rest = { stack: k, prompt: ks.prompt, arena: ks.arena, onSuspend: ks.onSuspend, onSuspendData: ks.onSuspendData, onResume: ks.onResume, rest: ks.rest } + const onRet = onReturn ? (x, ks) => onReturn(x, ks.rest, ks.rest.stack) : RETURN + return prog(prompt, { prompt, arena: Arena([]), onSuspend: onSuspend, onSuspendData: null, onResume: onResume, rest: rest }, onRet) } function DEALLOC(ks) { @@ -97,38 +97,58 @@ function DEALLOC(ks) { } } -function SHIFT(p, body, ks, k) { +function SHIFT(p, body, ks, k, cont) { // TODO avoid constructing this object - let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } - let cont = null + let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, onSuspend: ks.onSuspend, onSuspendData: ks.onSuspendData, onResume: ks.onResume, rest: ks.rest } while (!!meta && meta.prompt !== p) { - cont = { stack: meta.stack, prompt: meta.prompt, backup: meta.arena.backup(), rest: cont } + cont = { stack: meta.stack, prompt: meta.prompt, backup: meta.arena.backup(), onSuspend: meta.onSuspend, onSuspendData: meta.onSuspendData, onResume: meta.onResume, rest: cont } + if (!!meta.onSuspend) { + return meta.onSuspend(meta.rest, (x, ks) => { + // TODO what about ks here? It is just ignored -- doesn't seem right + // this will probably become a problem once a resumptive effect operation is called in onSuspend + + // just change onSuspendData to retain the value returned by onSuspend + const contt = { ...cont, onSuspendData: x } + return SHIFT(p, body, ks, meta.rest.stack, contt) + }) + } meta = meta.rest } if (!meta) { throw `Prompt not found ${p}` } // package the prompt itself - cont = { stack: meta.stack, prompt: meta.prompt, backup: meta.arena.backup(), rest: cont } + cont = { stack: meta.stack, prompt: meta.prompt, backup: meta.arena.backup(), onSuspend: meta.onSuspend, onSuspendData: meta.onSuspendData, onResume: meta.onResume, rest: cont } meta = meta.rest const k1 = meta.stack - meta.stack = null + // TODO why is this needed? + // meta.stack = null return body(cont, meta, k1) } // Rewind stack `cont` back onto `k` :: `ks` and resume with c -function RESUME(cont, c, ks, k) { - let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } +// TODO I do not like the additional argument b here. +// It is needed because when resuming, that is, rewinding cont onto k :: ks, we would +// execute the on resume clause of the top-most stack frame, however, these are not the desired semantics. +function RESUME(cont, c, b, ks, k) { + let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, onSuspend: ks.onSuspend, onSuspendData: ks.onSuspendData, onResume: ks.onResume, rest: ks.rest } let toRewind = cont while (!!toRewind) { - meta = { stack: toRewind.stack, prompt: toRewind.prompt, arena: toRewind.backup(), rest: meta } + if (!!toRewind.onResume && b) { + return toRewind.onResume(toRewind.onSuspendData, meta, (x, ks) => { + return RESUME(toRewind, c, false, ks, meta.stack) + }) + } + b = true + meta = { stack: toRewind.stack, prompt: toRewind.prompt, arena: toRewind.backup(), onSuspend: toRewind.onSuspend, onSuspendData: toRewind.onSuspendData, onResume: toRewind.onResume, rest: meta } toRewind = toRewind.rest } const k1 = meta.stack // TODO instead copy meta here, like elsewhere? - meta.stack = null + // TODO why is this needed? + // meta.stack = null return () => c(meta, k1) } @@ -212,3 +232,4 @@ $effekt.run = RUN * If a runtime is available, use `$effekt.run`, instead. */ $effekt.runToplevel = RUN_TOPLEVEL +