-
-
Notifications
You must be signed in to change notification settings - Fork 679
feat: Implement WebAssembly exception handling (try-catch-finally) #2965
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
base: main
Are you sure you want to change the base?
feat: Implement WebAssembly exception handling (try-catch-finally) #2965
Conversation
Adds support for WebAssembly exception handling, including throw, try, catch, and finally constructs. The compiler now emits proper throw instructions and manages exception tags when the exception-handling feature is enabled. Try-finally and try-catch-finally blocks are compiled with correct control flow, including deferred returns and pending action dispatch. Flow context is extended to track try-finally state. Adds comprehensive tests for exception handling.
Added the 'enabled' flag for the exception-handling feature in features.json and updated the test logic in compiler.js to check this flag when determining missing features.
Simplified assertions in exception tests by removing redundant comparisons and using direct boolean checks. Updated type assertions in catch blocks for better type safety. Added eslint-disable comments for returns in finally blocks. Adjusted expected line numbers in .wat test outputs to match code changes.
Replaces array spread with manual array construction for block statements, improving clarity and potentially performance when combining tryBlock and dispatchStmts.
Updated the NOTICE file to include Anakun as a contributor.
|
@BlobMaster41, you should probably ignore all exceptions from the runtime itself. For example, function ignoreRtError(): boolean {
let result = true;
try {
__new(usize.MAX_VALUE, idof<ArrayBuffer>());
} catch (e) {
result = false;
}
return result;
}
assert(ignoreRtError());
// not the most devastating example, but could cause problemsNot sure if you want to support catch variables other than function throwString(): boolean {
let result = false;
try {
throw "foo";
} catch (e) {
assert((e as string) == "foo");
result = true;
}
return result;
}
throwString();Should you also handle |
Hmm, if I change the abort, woudnt that be a breaking change? I can try to add a condition only when the feature is on to hook abort and throw instead with Webassembly instructions. It is possible yes to do it for every single unreachable yes, this would probably also give more insight about errors when someone hit a unreachable I guess. On the other side, this would also increase the size of the final compiled code. If thats something we want to do, it is indeed possible. |
Fixes #302.
Related: #2956, #484, #447.
Motivation
The WebAssembly exception handling proposal reached Phase 4 and shipped in V8 9.5 (Chrome 95, October 2021), Firefox 100 (May 2022), and Safari 15.2 (December 2021). All current Node.js LTS versions (18, 20, 22, 24) include V8 engines well past 9.5, meaning exception handling is available without flags in any supported runtime. This implementation brings AssemblyScript in line with these mature runtime capabilities.
This implementation is entirely opt-in and preserves complete backward compatibility. When the
exception-handlingfeature is disabled (the default), the compiler continues to emitabort()calls for throw statements exactly as before. Existing code compiles and runs identically. The new behavior only activates when explicitly enabled via--enable exception-handling, and even then the generated WASM modules only require runtime support that has been shipping unflagged in all major engines for over 4 years.Changes proposed in this pull request:
⯈ Implemented
throwstatement - Compiles to WebAssembly's nativethrowinstruction using a global$errortag that carries an i32 pointer to Error objects⯈ Implemented
try-catchblocks - Full support for catching exceptions with proper catch variable binding, flow analysis, and nested try-catch structures⯈ Implemented
try-finallyandtry-catch-finally- Complete finally support including the complex case ofreturnstatements inside try/catch blocks, using a pending action pattern to ensure finally always runs before control flow exitsImplementation Details
Exception Tag:
$errorcarrying an i32 pointer to Error objectensureExceptionTag()when first neededThrow Statement (
compileThrowStatement):Feature.ExceptionHandlingis enabled, generatesmodule.throw("$error", [valueExpr])abort()when feature is disabled (preserves existing behavior)FlowFlags.Throws | FlowFlags.Terminateson the flowTry-Catch (
compileTryStatement):try/catchblocks via Binaryenmodule.pop()to retrieve the exception value_BinaryenLocalSetto avoid shadow stack interference with pop placementTry-Finally with Return Support:
Uses a "pending action" pattern to defer returns until after finally executes:
pendingActionLocal(i32): tracks pending action (0=none, 1=return)pendingValueLocal: stores the pending return valueReturn in finally block overrides any pending return from try/catch:
Structure generated:
Core changes in
src/compiler.ts:exceptionTagEnsuredfield andensureExceptionTag()method for lazy tag creationcompileThrowStatement()updated to usemodule.throw()when feature enabledcompileTryStatement()completely rewritten with full try-catch-finally supportcompileReturnStatement()updated to check for try-finally contextSupporting changes in
src/flow.ts:tryFinallyPendingActionLocal- local index for pending action trackingtryFinallyPendingValueLocal- local index for pending return valuetryFinallyDispatchLabel- label to branch to for finally dispatchtryFinallyReturnType- return type for the pending valueisInTryFinallygetter andgetTryFinallyContext()methodTest Coverage
Basic Tests:
testThrow()- Basic throw statementtestTryCatch()- Basic try-catchtestCatchVar()- Accessing caught exception variable (e.message)testNoThrow()- Try-catch when no exception is throwntestFinally()- Basic finally blocktestNested()- Nested try-catch blocksFinally with Return Tests:
testReturnInCatchFinally()- Return in catch with finally (finally must run first)testTryCatchFinally()- Try-catch-finally without return in catchtestFinallyWithException()- Finally runs even when exception propagatestestFinallyNormalCompletion()- Finally with no exceptiontestReturnFromTry()- Return from try block with finallytestMultipleReturnsWithFinally()- Multiple return points with finallyClass-Based Tests:
CustomError- Custom error class extending ErrorResource- Resource management class with dispose patternCalculator- Class with try-catch in methods (divide,safeDivide)Outer/Inner- Nested class exception handlingStateMachine- State machine with exception-based error handlingCounter- Counter class with exception limitComplex Tests:
testArrayWithExceptions()- Array operations with exceptionstestRethrowWithFinally()- Rethrow with finally (verifies finally runs)testDeepNesting()- Deeply nested try-catch-finally tracking execution orderReturn in Finally Tests:
testReturnInFinally()- Return in finally overrides return in trytestReturnInFinallyOverridesCatch()- Return in finally overrides return in catchtestReturnInFinallySuppressesException()- Return in finally suppresses thrown exceptionLimitations
This implementation has one known limitation:
Usage
# Enable exception handling feature asc myfile.ts --enable exception-handling