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 9 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,6 +108,9 @@ enum TokenKind {
case `is`
case `namespace`
case `pure`
// case `on`
case `suspend`
case `finally`
}
object TokenKind {
// "Soft" keywords
Expand Down Expand Up @@ -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`
)

}

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(`suspend`, false) ~ finalizerClause(`resume`, true) ~ finalizerClause(`return`, true) ~ finalizerClause(`finally`, true) match {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, this breaks the current positions and error messages have the wrong span:

Warning:        |[warning] examples/pos/capture/regions.effekt:10:3: Handling effect Dummy, which seems not to be used by the program.
       |  try { prog() } with Dummy { resume(()) }
       |  ^
       |""".stripMargin
=> Diff (- obtained, + expected)
   try { prog() } with Dummy { resume(()) }
-  ^
+  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

case _ ~ _ ~ None ~ Some(_) ~ _ ~ _ =>
fail("Got `resume` clause but no `suspend` clause.")
case _ ~ _ ~ _ ~ Some(_) ~ _ ~ Some(_) =>
fail("Got both an `resume` and `finally` clause.")
case _ ~ _ ~ _ ~ _ ~ Some(_) ~ Some(_) =>
fail("got both an `return` and `finally` clause.")
case s ~ hs ~ susp ~ None ~ None ~ Some(prog) =>
TryHandle(s, hs, susp, Some(prog), Some(prog))
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(clause: TokenKind, vparam: Boolean): Option[FinalizerClause] =
nonterminal:
backtrack { consume(clause) }
.map { _ =>
braces {
backtrack { lambdaParams() <~ `=>` } ~ stmts() match {
// TODO better erros
case Some(Nil, vps, Nil) ~ body =>
if (!vps.isEmpty != vparam) fail(s"$clause value parameter mismatch")
else FinalizerClause(vps.headOption, body)
case Some(_, _, _) ~ _ =>
fail(s"$clause only expects value parameters")
case None ~ body =>
if (vparam) fail(s"$clause 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
13 changes: 11 additions & 2 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,7 +449,7 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
}

val body: BlockLit = BlockLit(Nil, List(promptCapt), Nil, List(promptParam),
Scope(transformedHandlers, transform(prog)))
Scope(transformedHandlers, transformedProg))

Context.bind(Reset(body))
dvdvgt marked this conversation as resolved.
Show resolved Hide resolved

Expand Down Expand Up @@ -641,6 +645,11 @@ object Transformer extends Phase[Typechecked, CoreTransformed] {
})
}

def transform(clause: source.FinalizerClause)(using Context): FinalizerClause = {
val source.FinalizerClause(vp, prog) = clause
FinalizerClause(vp map transform, transform(prog))
}

def preprocess(sc: ValueVar, clause: source.MatchClause)(using Context): Clause =
preprocess(List((sc, clause.pattern)), clause.guards, transform(clause.body))

Expand Down
2 changes: 2 additions & 0 deletions effekt/shared/src/main/scala/effekt/core/Tree.scala
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ case class Operation(name: Id, tparams: List[Id], cparams: List[Id], vparams: Li
val capt = body.capt -- cparams.toSet
}

case class FinalizerClause(vp: Option[ValueParam], body: Stmt) extends Tree


object Tree {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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 {
Expand All @@ -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) =>
Expand Down
6 changes: 5 additions & 1 deletion effekt/shared/src/main/scala/effekt/source/Tree.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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
// ----------------

Expand Down Expand Up @@ -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)
Expand Down
13 changes: 11 additions & 2 deletions effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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",
Expand Down
Loading