Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resource finalization to handlers #701

Draft
wants to merge 27 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
645db98
add parser and lexer support for new try clauses
dvdvgt Nov 18, 2024
110bfd0
refactor finalize clause parsing and AST rep.
dvdvgt Nov 19, 2024
b507f3a
adapt Namer
dvdvgt Nov 19, 2024
f59f5b5
adapt core and Transformer
dvdvgt Nov 19, 2024
8d89d80
adapt Typer
dvdvgt Nov 19, 2024
6165ac0
adapt Wellformedness
dvdvgt Nov 19, 2024
9b7b13d
adapt BoxUnboxInference
dvdvgt Nov 19, 2024
180e9d4
adapt ExplicitCapabilities
dvdvgt Nov 19, 2024
a557b15
adapt AnnotateCaptures
dvdvgt Nov 19, 2024
4e2d7cf
cherry-pick from #708: Extract `RESUME` function
dvdvgt Nov 22, 2024
37ef6a1
adapt JS runtime
dvdvgt Nov 28, 2024
c39f550
add finally clause desugaring
dvdvgt Nov 28, 2024
d4a5a6b
adapt backend transformations
dvdvgt Nov 28, 2024
4f0bffa
add basic test
dvdvgt Nov 28, 2024
19bce8e
add 'on' as a soft keyword
dvdvgt Dec 3, 2024
468bd90
change SHIFT function such that an empty continuation is passed
dvdvgt Dec 3, 2024
6cde193
add some missing cases for Reachable and PrettyPrinter
dvdvgt Dec 3, 2024
996a17f
change tests to account for 'finally' being a keyword now
dvdvgt Dec 3, 2024
e8f88af
account for effect operations in finalizer clauses
dvdvgt Dec 3, 2024
cc3b594
Merge branch 'master' into feature/finalization
dvdvgt Dec 3, 2024
3d99558
resolve conflicts
dvdvgt Dec 12, 2024
da78963
Merge branch 'master' into feature/finalization
dvdvgt Dec 19, 2024
b5b10e9
add missing calls in free variable computation
dvdvgt Dec 19, 2024
06e95d9
fix unwinding and rewinding when effect operations are involved
dvdvgt Dec 19, 2024
b2bfebc
do not inline tail resumptive shifts
dvdvgt Dec 19, 2024
770829c
add effect op call in new clauses
dvdvgt Dec 19, 2024
9dd5638
update check file
dvdvgt Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
6 changes: 5 additions & 1 deletion effekt/shared/src/main/scala/effekt/Lexer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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`
)

}

Expand Down
13 changes: 12 additions & 1 deletion effekt/shared/src/main/scala/effekt/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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))
Expand Down Expand Up @@ -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
*
Expand Down
32 changes: 30 additions & 2 deletions effekt/shared/src/main/scala/effekt/RecursiveDescent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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:
Expand Down
57 changes: 54 additions & 3 deletions effekt/shared/src/main/scala/effekt/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand All @@ -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
Expand Down
26 changes: 14 additions & 12 deletions effekt/shared/src/main/scala/effekt/core/Inline.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down Expand Up @@ -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))
Expand All @@ -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))

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
4 changes: 2 additions & 2 deletions effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion effekt/shared/src/main/scala/effekt/core/Reachable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion effekt/shared/src/main/scala/effekt/core/Recursive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
16 changes: 12 additions & 4 deletions effekt/shared/src/main/scala/effekt/core/Transformer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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))

Expand Down
Loading
Loading