Skip to content

Conversation

@BlobMaster41
Copy link

@BlobMaster41 BlobMaster41 commented Dec 11, 2025

Fixes AssemblyScript#302.
Related: AssemblyScript#2956, AssemblyScript#484, AssemblyScript#447.

Changes proposed in this pull request:

Implemented throw statement - Compiles to WebAssembly's native throw instruction using a global $error tag that carries an i32 pointer to Error objects

Implemented try-catch blocks - Full support for catching exceptions with proper catch variable binding, flow analysis, and nested try-catch structures

Implemented try-finally and try-catch-finally - Complete finally support including the complex case of return statements inside try/catch blocks, using a pending action pattern to ensure finally always runs before control flow exits

Implementation Details

Exception Tag:

  • Single global exception tag $error carrying an i32 pointer to Error object
  • Matches JavaScript semantics where catch catches all exceptions
  • Tag is lazily created via ensureExceptionTag() when first needed

Throw Statement (compileThrowStatement):

  • When Feature.ExceptionHandling is enabled, generates module.throw("$error", [valueExpr])
  • Falls back to abort() when feature is disabled (preserves existing behavior)
  • Sets FlowFlags.Throws | FlowFlags.Terminates on the flow

Try-Catch (compileTryStatement):

  • Generates WebAssembly try/catch blocks via Binaryen
  • Catch variable is bound using module.pop() to retrieve the exception value
  • Uses direct _BinaryenLocalSet to avoid shadow stack interference with pop placement
  • Proper flow analysis merging try and catch paths

Try-Finally with Return Support:

  • Uses a "pending action" pattern to defer returns until after finally executes:

    1. pendingActionLocal (i32): tracks pending action (0=none, 1=return)
    2. pendingValueLocal: stores the pending return value
    3. Return statements branch to dispatch label instead of returning directly
    4. After finally code runs, dispatch logic performs the actual return
  • Return in finally block overrides any pending return from try/catch:

    • Flow tracking detects when finally contains a return statement
    • Dispatch logic is skipped when finally terminates
    • Properly suppresses exceptions (matches JavaScript semantics)
  • Structure generated:

    (block $finally_dispatch
      (try $try_finally
        (do ...)
        (catch_all
          ;; finally code
          (rethrow $try_finally)
        )
      )
    )
    ;; finally code (normal/return path)
    (if (i32.eq (local.get $pendingAction) (i32.const 1))
      (return (local.get $pendingValue))
    )

Core changes in src/compiler.ts:

  • exceptionTagEnsured field and ensureExceptionTag() method for lazy tag creation
  • compileThrowStatement() updated to use module.throw() when feature enabled
  • compileTryStatement() completely rewritten with full try-catch-finally support
  • compileReturnStatement() updated to check for try-finally context

Supporting changes in src/flow.ts:

  • tryFinallyPendingActionLocal - local index for pending action tracking
  • tryFinallyPendingValueLocal - local index for pending return value
  • tryFinallyDispatchLabel - label to branch to for finally dispatch
  • tryFinallyReturnType - return type for the pending value
  • isInTryFinally getter and getTryFinallyContext() method

Test Coverage

Basic Tests:

  • testThrow() - Basic throw statement
  • testTryCatch() - Basic try-catch
  • testCatchVar() - Accessing caught exception variable (e.message)
  • testNoThrow() - Try-catch when no exception is thrown
  • testFinally() - Basic finally block
  • testNested() - Nested try-catch blocks

Finally with Return Tests:

  • testReturnInCatchFinally() - Return in catch with finally (finally must run first)
  • testTryCatchFinally() - Try-catch-finally without return in catch
  • testFinallyWithException() - Finally runs even when exception propagates
  • testFinallyNormalCompletion() - Finally with no exception
  • testReturnFromTry() - Return from try block with finally
  • testMultipleReturnsWithFinally() - Multiple return points with finally

Class-Based Tests:

  • CustomError - Custom error class extending Error
  • Resource - Resource management class with dispose pattern
  • Calculator - Class with try-catch in methods (divide, safeDivide)
  • Outer/Inner - Nested class exception handling
  • StateMachine - State machine with exception-based error handling
  • Counter - Counter class with exception limit

Complex Tests:

  • testArrayWithExceptions() - Array operations with exceptions
  • testRethrowWithFinally() - Rethrow with finally (verifies finally runs)
  • testDeepNesting() - Deeply nested try-catch-finally tracking execution order

Return in Finally Tests:

  • testReturnInFinally() - Return in finally overrides return in try
  • testReturnInFinallyOverridesCatch() - Return in finally overrides return in catch
  • testReturnInFinallySuppressesException() - Return in finally suppresses thrown exception

Limitations

This implementation has one known limitation:

  • Break/continue in try-finally: Not yet implemented (would need action codes 2/3 and label management)

Usage

# Enable exception handling feature
asc myfile.ts --enable exception-handling
// Example: Resource cleanup pattern
class Resource {
  dispose(): void { /* cleanup */ }
}

function useResource(): i32 {
  let r = new Resource();
  try {
    // Do work that might throw
    return processData();
  } catch (e) {
    // Handle error
    return -1;
  } finally {
    // Always runs - cleanup resource
    r.dispose();
  }
}

// Example: Custom error
class ValidationError extends Error {
  constructor(message: string, public field: string) {
    super(message);
  }
}

function validate(value: i32): void {
  if (value < 0) {
    throw new ValidationError("Value must be positive", "value");
  }
}
  • I've read the contributing guidelines
  • I've added my name and email to the NOTICE file

Adds support for compiling throw, try, catch, and finally statements using WebAssembly exception handling when the feature is enabled. Introduces logic to emit exception tags, throw, try, catch, and rethrow instructions, and provides fallback to abort() when exception handling is disabled. Includes new tests for exception handling.
Adds support for deferring return statements until after finally blocks in try-finally constructs. This is achieved by tracking pending actions and values in dedicated locals and dispatching control flow after finally execution. Updates flow context and test cases to verify correct behavior for returns in try-finally and try-catch-finally scenarios.
Updates the compiler to detect if a finally block always returns or terminates, and skips dispatch logic accordingly. Adds tests to verify that return statements in finally blocks override returns in try/catch and suppress exceptions.
@BlobMaster41 BlobMaster41 marked this pull request as draft December 12, 2025 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Question] Try catch support

2 participants