Phase 19. try / catch / panic (Mochi::Runtime::Panic)
| Field | Value |
|---|---|
| MEP | MEP-56 §Phases |
| Status | LANDED |
| Started | 2026-05-29 12:01 (GMT+7) |
| Landed | 2026-05-29 12:01 (GMT+7) |
| Tracking issue | none |
| Tracking PR | #22510 |
| Commit | a1b3d4bcdc |
Gate
TestPhase19TryCatch in transpiler3/ruby/build/phase19_test.go: three subtests (panic_caught, panic_caught_in_fun, try_no_panic). The first verifies that a top-level try { panic(42, "boom") } catch e { print(e) } prints before\ncaught\n42\n (i.e. the catch binding receives the integer panic code, the message is dropped from stdout but survives on the exception). The second wraps the same construct inside a function and confirms control returns normally after the catch. The third verifies the no-panic happy path: the catch arm must not run, and the variable assigned in the try body must survive. Each runs under the resolved Ruby toolchain with -I mochi-runtime/lib.
Lowering decisions
panic(code, msg) lowers to raise Mochi::Runtime::Panic.new(code, msg), and try { ... } catch e { ... } lowers to begin ... rescue Mochi::Runtime::Panic => __exc ... end with e = __exc.code synthesised as the first line of the rescue body (transpiler3/ruby/lower/lower.go lines 318 to 330 and 1301 to 1325):
-
aotir.PanicStmttoRawStmtrenderingraise Mochi::Runtime::Panic.new(code, msg)(lines 320 to 330).Mochi::Runtime::Panicis aStandardErrorsubclass defined inmochi-runtime/lib/mochi/runtime/panic.rbwithattr_reader :code; the message is passed tosuper(msg)so it surfaces via the standardStandardError#messagechannel for diagnostics. -
aotir.TryCatchStmtto a hand-builtRawStmtfromlowerTryCatchStmt(lines 1301 to 1325). The emitted shape is:begin<try body>rescue Mochi::Runtime::Panic => __exce = __exc.code<catch body>endRescuing the specific
Mochi::Runtime::Panicclass (not barerescue, notStandardError) means unrelated Ruby errors (e.g. a stdlib bug) bubble up instead of being silently absorbed. The catch variable is assigned from__exc.code(line 1319) because Mochi'scatch ebinds the integer panic code, not the exception object; pulling.codeoff the typed exception keeps the lowering one-to-one with the Mochi semantics. -
The fresh exception name
__excuses the__-prefix lowerer reservation so it cannot collide with any Mochi-level identifier in the catch body. -
The happy-path subtest (
try_no_panic) needs no lowering glue: with noraise, Ruby'sbegin ... rescuejust falls through and the rescue body is skipped.
Files changed
| File | Purpose |
|---|---|
transpiler3/ruby/lower/lower.go | PanicStmt (lines 320 to 330) and TryCatchStmt (lines 318 to 319, dispatching to lowerTryCatchStmt at lines 1301 to 1325) lower to Ruby raise and begin/rescue |
mochi-runtime/lib/mochi/runtime/panic.rb | Mochi::Runtime::Panic < StandardError with attr_reader :code |
transpiler3/ruby/build/phase19_test.go | TestPhase19TryCatch with 3 subtests |
Test set
TestPhase19TryCatch/panic_caught,panic_caught_in_fun,try_no_panic.
Closeout notes
Phase 19 landed on CRuby 4.0 (Homebrew). The decision to bind the catch variable to __exc.code (an integer) rather than __exc (the exception object) follows MEP-56 §panic-semantics: Mochi treats panic codes as the user-visible payload and message as a diagnostic side-channel. Using a typed exception class (Mochi::Runtime::Panic) instead of a sentinel String keeps unrelated Ruby errors from being swallowed by Mochi's catch and lets the runtime surface the code via a real accessor, not a regex parse on the message.