From protos at guillermomolina.com Fri Sep 11 11:26:31 2026 From: protos at guillermomolina.com (protos at guillermomolina.com) Date: Fri, 11 Sep 2026 13:26:31 +0200 Subject: [graalvm-dev] [External] : Truffle Bytecode DSL production cutover with continuations and structured control flow Message-ID: <1004319ec8e7a3d4131541be41ca1b30@guillermomolina.com> Hi, I'm Guillermo Adri?n Molina, author of Protos, a prototype-based language implemented on Truffle 25.3.4.1. I originally intended to ask about how to represent dynamic guest control flow around Bytecode DSL continuations, but the implementation has progressed quite a bit since then, so I thought a more useful question would be about the final production cutover and any pitfalls we may be missing. Protos is migrating from an AST-based cooperative replay mechanism to a Bytecode DSL backend. The Bytecode path now supports: * source-backed closure calls composed across Bytecode roots; * Task/Future suspension and resumption through ContinuationResult, without replaying completed guest effects; * non-local return, guest Error propagation/handlers, suspendible ensure, cancellation and while; * debugger scopes through TagTreeNode / NodeLibrary; * StatementTag/CallTag source identity and logical BytecodeLocation preservation across suspension/resume. The control-flow model we ended up with is: * normal guest values remain ordinary values; * suspension is distinct from unwind and is represented by the Bytecode continuation mechanism; * guest Error remains a guest-exception lane; * non-local return and cancellation are rare internal control-flow transfers; * a narrow interceptControlFlowException bridge allows those internal transfers to cross Bytecode EH regions so that structured cleanup can run, while retaining the exact original transfer; * pending transfers live in resumable Bytecode/frame state while cleanup executes, including when cleanup itself suspends; * there is no replay tape, global continuation registry, semantic ThreadLocal, or parked thread on the Bytecode path. At this point the remaining work is mostly production cutover: complete a couple of hosting/native boundaries, switch normal TruffleLanguage.parse() execution to the Bytecode backend, retire the old replay path, and then enable/measure the optimizing Truffle runtime in ordinary execution. I would be interested in feedback on three points: * Does the separation above -- Bytecode continuations for suspension, structured Bytecode EH/finally for unwind, and a narrow ControlFlowException bridge for internal transfers -- match the intended direction of the Bytecode DSL, or is there another pattern you would recommend for a production language? * Are there known caveats when making a continuation-enabled Bytecode DSL root the normal production backend, particularly for runtime compilation / partial evaluation? For example, does enabling yield have meaningful cost for roots that never actually suspend? * Are there specific diagnostics or invariants you recommend checking during the final AST-to-Bytecode cutover to make sure resumed paths, structured control flow, source metadata and runtime compilation remain optimization-friendly? The project is guillermomolina/protos on GitHub if concrete implementation details are useful. Thanks, Guillermo Adri?n Molina Protos -------------- next part -------------- An HTML attachment was scrubbed... URL: From christian.humer at oracle.com Fri Sep 11 15:33:51 2026 From: christian.humer at oracle.com (Christian Humer) Date: Fri, 11 Sep 2026 15:33:51 +0000 Subject: [graalvm-dev] [External] : Truffle Bytecode DSL production cutover with continuations and structured control flow In-Reply-To: <1004319ec8e7a3d4131541be41ca1b30@guillermomolina.com> References: <1004319ec8e7a3d4131541be41ca1b30@guillermomolina.com> Message-ID: <008E1832-5971-4CB6-B14C-DBDF2A6D4F0A@oracle.com> Hi Guillermo, Always glad to learn about a new bytecode DSL user. If you also have some general feedback, that is always appreciated. The separation between suspension and unwinding sounds sensible. Generally, the intended direction is to avoid ControlFlowException for ordinary control flow: it was primarily a workaround for AST interpreters, whereas bytecode can usually express that control flow directly. From you docs I read that you explicitly decided to not use Truffle?s safe point mechanism for cancellation. That can be a sensible decision if you want your users to recover from cancellation. Your cross-call non-local returns are one of the rarer cases where CFE can still make sense. Desugaring them into ordinary returns would require propagating an explicit transfer through intermediate calls. The conversion to AbstractTruffleException to run ensure cleanup is not ideal, though we should probably support CFEs directly in the relevant Bytecode exception-handling machinery as an opt-in. This would allow you handle CFEs like truffle exceptions but don?t pay the cost for capturing stack frames. Or is this something you need anyway? For continuation-enabled roots, the main performance caveat is that operand-stack values are virtualized after runtime compilation, but continuation locals are not, making local accesses slower. Entering a continuation with many live stack values also has a cost to bring those values into compiled execution and virtualize them. That of course only applies if the continuation target is not inlined. Getting a continuation-enabled bytecode interpreter right for partial evaluation is tricky, but the Bytecode DSL handles that machinery. So no problems are expected here. I would still inspect IGV graphs, particularly for resumed paths and cleanup that itself suspends to double check everything is as expected. You?re also welcome to also join the community Slack to discuss this further directly. (#truffle channel) https://www.graalvm.org/slack-invitation/ Hope this was helpful. Christian On 11 Sep 2026, at 13:26, Guillermo Molina wrote: Hi, I'm Guillermo Adri?n Molina, author of Protos, a prototype-based language implemented on Truffle 25.3.4.1. I originally intended to ask about how to represent dynamic guest control flow around Bytecode DSL continuations, but the implementation has progressed quite a bit since then, so I thought a more useful question would be about the final production cutover and any pitfalls we may be missing. Protos is migrating from an AST-based cooperative replay mechanism to a Bytecode DSL backend. The Bytecode path now supports: * source-backed closure calls composed across Bytecode roots; * Task/Future suspension and resumption through ContinuationResult, without replaying completed guest effects; * non-local return, guest Error propagation/handlers, suspendible ensure, cancellation and while; * debugger scopes through TagTreeNode / NodeLibrary; * StatementTag/CallTag source identity and logical BytecodeLocation preservation across suspension/resume. The control-flow model we ended up with is: * normal guest values remain ordinary values; * suspension is distinct from unwind and is represented by the Bytecode continuation mechanism; * guest Error remains a guest-exception lane; * non-local return and cancellation are rare internal control-flow transfers; * a narrow interceptControlFlowException bridge allows those internal transfers to cross Bytecode EH regions so that structured cleanup can run, while retaining the exact original transfer; * pending transfers live in resumable Bytecode/frame state while cleanup executes, including when cleanup itself suspends; * there is no replay tape, global continuation registry, semantic ThreadLocal, or parked thread on the Bytecode path. At this point the remaining work is mostly production cutover: complete a couple of hosting/native boundaries, switch normal TruffleLanguage.parse() execution to the Bytecode backend, retire the old replay path, and then enable/measure the optimizing Truffle runtime in ordinary execution. I would be interested in feedback on three points: * Does the separation above -- Bytecode continuations for suspension, structured Bytecode EH/finally for unwind, and a narrow ControlFlowException bridge for internal transfers -- match the intended direction of the Bytecode DSL, or is there another pattern you would recommend for a production language? * Are there known caveats when making a continuation-enabled Bytecode DSL root the normal production backend, particularly for runtime compilation / partial evaluation? For example, does enabling yield have meaningful cost for roots that never actually suspend? * Are there specific diagnostics or invariants you recommend checking during the final AST-to-Bytecode cutover to make sure resumed paths, structured control flow, source metadata and runtime compilation remain optimization-friendly? The project is guillermomolina/protos on GitHub if concrete implementation details are useful. Thanks, Guillermo Adri?n Molina Protos -------------- next part -------------- An HTML attachment was scrubbed... URL: -------------- next part -------------- An HTML attachment was scrubbed... URL: From protos at guillermomolina.com Fri Sep 11 16:32:15 2026 From: protos at guillermomolina.com (protos at guillermomolina.com) Date: Fri, 11 Sep 2026 18:32:15 +0200 Subject: [graalvm-dev] [External] : Truffle Bytecode DSL production cutover with continuations and structured control flow In-Reply-To: <008E1832-5971-4CB6-B14C-DBDF2A6D4F0A@oracle.com> References: <1004319ec8e7a3d4131541be41ca1b30@guillermomolina.com> <008E1832-5971-4CB6-B14C-DBDF2A6D4F0A@oracle.com> Message-ID: <3597a9455742181cd52b1e559ad5d784@guillermomolina.com> Hi Christian, Thanks, this is very helpful. On the ControlFlowException point: no, Protos does not need stack-frame capture for those bridged internal transfers. Guest Error is a separate lane and remains an AbstractTruffleException. The transfers currently going through the Bytecode EH bridge are only non-local return and cooperative cancellation. In both cases we preserve the exact original ControlFlowException and unwrap it again at the owning call/resume boundary. So the AbstractTruffleException envelope currently exists only to let those transfers cross Bytecode EH regions so that structured cleanup can run. We do not otherwise need guest exception stack information for it. An opt-in mechanism allowing ControlFlowException to participate directly in the relevant Bytecode EH machinery would therefore fit Protos very well, and I would prefer that over the current envelope when available. Your comments about continuation locals are also particularly useful. We will add explicit IGV inspection for: - ordinary hot continuation-enabled roots that do not suspend; - resumed Future.value() paths; - ensure cleanup that itself suspends and resumes; - continuation-local usage and live operand-stack values at resume points; - whether continuation targets are being inlined as expected. The cancellation point also matches our intent. Cancellation is cooperative and language-controlled rather than host-thread cancellation; it participates in structured unwind and cleanup, so we need to retain that semantic control. I'll also join the #truffle Slack channel. As we complete the production cutover and get optimizing-runtime measurements, I'm happy to share any Bytecode DSL feedback or minimized cases that look useful. Thanks again, Guillermo Molina Protos https://github.com/guillermomolina/protos On 2026-09-11 17:33, Christian Humer wrote: > Hi Guillermo, > > Always glad to learn about a new bytecode DSL user. If you also have > some general feedback, that is always appreciated. > > The separation between suspension and unwinding sounds sensible. > Generally, the intended direction is to avoid ControlFlowException for > ordinary control flow: it was primarily a workaround for AST > interpreters, whereas bytecode can usually express that control flow > directly. From you docs I read that you explicitly decided to not use > Truffle's safe point mechanism for cancellation. That can be a sensible > decision if you want your users to recover from cancellation. > > Your cross-call non-local returns are one of the rarer cases where CFE > can still make sense. Desugaring them into ordinary returns would > require propagating an explicit transfer through intermediate calls. > The conversion to AbstractTruffleException to run ensure cleanup is not > ideal, though we should probably support CFEs directly in the relevant > Bytecode exception-handling machinery as an opt-in. This would allow > you handle CFEs like truffle exceptions but don't pay the cost for > capturing stack frames. Or is this something you need anyway? > > For continuation-enabled roots, the main performance caveat is that > operand-stack values are virtualized after runtime compilation, but > continuation locals are not, making local accesses slower. Entering a > continuation with many live stack values also has a cost to bring those > values into compiled execution and virtualize them. That of course only > applies if the continuation target is not inlined. > > Getting a continuation-enabled bytecode interpreter right for partial > evaluation is tricky, but the Bytecode DSL handles that machinery. So > no problems are expected here. I would still inspect IGV graphs, > particularly for resumed paths and cleanup that itself suspends to > double check everything is as expected. > > You're also welcome to also join the community Slack to discuss this > further directly. (#truffle channel) > https://www.graalvm.org/slack-invitation/ > > Hope this was helpful. > > Christian > >> On 11 Sep 2026, at 13:26, Guillermo Molina >> wrote: >> >> Hi, >> >> I'm Guillermo Adri?n Molina, author of Protos, a prototype-based >> language implemented on Truffle 25.3.4.1. >> >> I originally intended to ask about how to represent dynamic guest >> control flow around Bytecode DSL continuations, but the implementation >> has progressed quite a bit since then, so I thought a more useful >> question would be about the final production cutover and any pitfalls >> we >> may be missing. >> >> Protos is migrating from an AST-based cooperative replay mechanism to >> a >> Bytecode DSL backend. >> >> The Bytecode path now supports: >> >> * source-backed closure calls composed across Bytecode roots; >> * Task/Future suspension and resumption through ContinuationResult, >> without replaying completed guest effects; >> * non-local return, guest Error propagation/handlers, suspendible >> ensure, cancellation and while; >> * debugger scopes through TagTreeNode / NodeLibrary; >> * StatementTag/CallTag source identity and logical BytecodeLocation >> preservation across suspension/resume. >> >> The control-flow model we ended up with is: >> >> * normal guest values remain ordinary values; >> * suspension is distinct from unwind and is represented by the >> Bytecode continuation mechanism; >> * guest Error remains a guest-exception lane; >> * non-local return and cancellation are rare internal control-flow >> transfers; >> * a narrow interceptControlFlowException bridge allows those internal >> transfers to cross Bytecode EH regions so that structured cleanup can >> run, while retaining the exact original transfer; >> * pending transfers live in resumable Bytecode/frame state while >> cleanup executes, including when cleanup itself suspends; >> * there is no replay tape, global continuation registry, semantic >> ThreadLocal, or parked thread on the Bytecode path. >> >> At this point the remaining work is mostly production cutover: >> complete >> a couple of hosting/native boundaries, switch normal >> TruffleLanguage.parse() execution to the Bytecode backend, retire the >> old replay path, and then enable/measure the optimizing Truffle >> runtime >> in ordinary execution. >> >> I would be interested in feedback on three points: >> >> * Does the separation above -- Bytecode continuations for suspension, >> structured Bytecode EH/finally for unwind, and a narrow >> ControlFlowException bridge for internal transfers -- match the >> intended >> direction of the Bytecode DSL, or is there another pattern you would >> recommend for a production language? >> * Are there known caveats when making a continuation-enabled Bytecode >> DSL root the normal production backend, particularly for runtime >> compilation / partial evaluation? For example, does enabling yield >> have >> meaningful cost for roots that never actually suspend? >> * Are there specific diagnostics or invariants you recommend checking >> during the final AST-to-Bytecode cutover to make sure resumed paths, >> structured control flow, source metadata and runtime compilation >> remain >> optimization-friendly? >> >> The project is guillermomolina/protos on GitHub if concrete >> implementation details are useful. >> >> Thanks, >> >> Guillermo Adri?n Molina >> Protos >> -------------- next part -------------- >> An HTML attachment was scrubbed... >> URL: >> -------------- next part -------------- An HTML attachment was scrubbed... URL: