-
Notifications
You must be signed in to change notification settings - Fork 29
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
Handler runtime improvements #282
base: hkmc2
Are you sure you want to change the base?
Conversation
0af878c
to
3808175
Compare
// LIFO | ||
handle h = ThreadEffect with | ||
fun fork(thread)(k) = | ||
this.tasks.unshift(thread) | ||
k(()) | ||
fun yld()(k) = | ||
this.tasks.push(k) | ||
globalThis.eval("runtime").enterHandleBlock(this, () => this.tasks.shift()(this)) | ||
fun start(main)(k) = | ||
this.tasks.push(main) | ||
while this.tasks.length != 0 do | ||
globalThis.eval("runtime").enterHandleBlock(this, () => this.tasks.shift()(this)) | ||
k() | ||
in | ||
h.start(main) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is needed, because handler method themselves cannot throw effect of the handler itself. So here it just rebinds the handler and invoke it. Previously, the handler is still binded during the handler method with some weird semantics as the order of which handler is resumed is simply very messy (refer to RecursiveHandler.mls
L56-79). This should be the correct behaviour now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait but that looks like major shift in the official semantics of effect handlers. I'm afraid it might make our support for the feature quite atypical/weird and not aligned with the standard definitions from the literature.
This seems like a significant design decision that should be properly discussed. Could you start by specifying what's the intended semantics of enterHandleBlock
? It doesn't seem to appear anywhere in your project report.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I just saw your other comment:
The semantic of that function is simply entering a handle block with handler h binded, except as opposed to handle block, it does not create the handler h
-
Are you saying this is a way of obtaining deep handler semantics in an otherwise shallow handler implementation?
-
Could you please give examples of what happens if you forget to do it?
-
Why not implicitly wrap all handler blocks inside this call? Would that make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- the handler is still deep, you can still raise effect multiple times in the handler body but not inside the actual handler. The actual handler method cannot raise effects.
- This will be throwing error "Unhandled effect" as if the effect leaks.
- Yes, this is possible. A handle block is simply
let h = new Handler(); runtime.enterHandleBlock(h, () => handlerBody);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what happens here is this: because this.tasks contain some function that will still raise effect due to .fork()
just adding those function directly, those added by .yld
won't raise effects
:re
handle h = Object with
fun write(x)(k) =
if x < 10 do write(10)
[x, k(())]
h.write(1)
h.write(2)
//│ ═══[RUNTIME ERROR] Error: Unhandled effects
handle h = Object with
fun write(x)(k) =
if x < 10 do runtime.enterHandleBlock(this, () => write(10))
[x, k(())]
h.write(1)
h.write(2)
//│ = [1, [2, ()]] |
Co-authored-by: Lionel Parreaux <[email protected]>
Changes:
Runtime.mls
I have some further improvement in mind which should allow some program that cannot be run currently to work fine.(Update: done)Problem:
I added Runtime module in
decl/Prelude.mls
just to get the class symbols. I think this is not correct as it causeRuntime
to pass elab but I don't know how to get the class symbols otherwise. (Runtime.mls has dependence on these classes, and Predef.mls imports Runtime.mls, so I don't think these classes can go to Predef?)TODO:
MLsCompiler.scala
. It should not use full path.enterHandleBlock
toPredef
(as it's intended to be used by programmers directly) and document its semantics thereTest diffs:
UserThreads*.mls
: these tests are malformed, and the new implementation throws Error on themLeakingEffects.mls
: the new test output matches with intended semanticsZCombinator.mls
: the stack depth changes because ofRuntime.stackDepth
is a selection which gets sanity checks. The sanity check moves the selection before the stack depth is increased for the function callconsole.log
.Follow-up:
enterHandleBlock
(...handlerArgs) => return mkEffect(h, (k) => handlerMtdBody);
enterHandleBlock(h, () => handlerBody)
h
andk
is clear in the handler method, i.e. we can even have standalone handlers likeh
is a value, butk
is binded as param, so it is kind of awkward. Also for type checking this means you retroactively add behaviour toh
which is not great and probably won't work.while awkward, Object are not essential for codegen to work,
h
can be anything that can be equality checked, i.e. everything in JS is technically fine (but this is kind of awkward and might not work well with type checking).It might be worthwhile to add yet another keyword for
handler fun
for clarity and conciseness.