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 closure environment allocation, variable capture analysis, and code generation for accessing and storing captured variables in closures. Updates the compiler to prescan function bodies for captured variables, allocate and initialize closure environments, and handle closure function creation and invocation. Extends Flow and Function/Local classes to track closure-related metadata. Includes new tests for closure behavior.
Corrects the calculation of environment slot offsets for captured variables in closures, ensuring proper byte offset handling and consistent environment setup. Updates test WAT files to reflect the new closure environment layout and stack management, improving correctness and coverage for closure, function expression, return, ternary, and typealias scenarios.
Enhances closure support by properly aligning captured local offsets, caching the closure environment pointer in a local to prevent overwrites from indirect calls, and updating environment size calculations. Also adds comprehensive AST node coverage for captured variable analysis and updates related tests to reflect the new closure environment management.
Adds logic to prescan constructor arguments of 'new' expressions for function expressions. This ensures that any function expressions passed as arguments are properly processed during compilation.
Introduce new test files for closure class functionality in the compiler, including TypeScript source, expected JSON output, and debug/release WebAssembly text formats.
Introduces a new 'closures' feature flag to the compiler, updates feature enumeration, and adds checks to ensure closures are only used when the feature is enabled. Test configurations are updated to enable the closures feature for relevant tests.
Refactored the compiler to only emit closure environment setup code when the closures feature is enabled. For builds without closures, indirect calls now use a simpler code path, resulting in smaller and cleaner generated code. Updated numerous test outputs to reflect the reduced stack usage and removed unnecessary closure environment handling.
Reserve slot 0 in closure environments for the parent environment pointer, ensuring correct alignment and traversal for nested closures. Track the owning function for each captured local, update environment access logic to traverse parent chains, and initialize the parent pointer when allocating environments. This enhances support for deeply nested closures and corrects environment memory layout.
Adjusts allocation sizes and field offsets for closure environments in multiple .wat test files, changing from 4 to 8 bytes (and similar increases for larger environments) and updating i32.store/load instructions to use the correct offsets. This aligns the test code with a new closure environment memory layout, likely reflecting changes in the compiler's closure representation.
Updated the NOTICE file to include Anakun <anakun@opnet.org> as a contributor.
Adds logic to properly capture and reference 'this' in closures and methods, ensuring 'this' is stored in the closure environment when needed. Updates compiler and resolver to support lookup and environment slot assignment for captured 'this', improving closure support for methods referencing 'this'.
Removed unnecessary 'self = this' assignments in all closure-returning methods, replacing references to 'self' with 'this'. This simplifies the code and improves readability by leveraging direct 'this' capture in arrow functions.
feat: Add experimental closures support with feature flag
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.
Extended closure variable capture logic in the compiler to handle while, do-while, for, for-of, switch, try/catch/finally, and various expression nodes. Updated test cases to cover closure captures in these new contexts, ensuring correct environment handling and variable scoping for closures in complex control flow and expression scenarios.

Found unhandled case of closure with nested arrays.
Replaces all occurrences of the closure environment global variable from '$$~lib/__closure_env' to '$~lib/__closure_env' in the compiler source and test files. This change ensures consistency in global naming and avoids the use of double dollar signs.
Ensure that default values of function parameters are scanned for variable captures before parameter names are added to the inner function scope. This fixes issues where closures in default parameter expressions could not capture outer variables. Adds new tests for closures in default parameter values.
Replaces the collectCapturedNames function with a unified scanNodeForCaptures that supports both local and name modes for closure variable capture analysis. Updates all relevant call sites and improves handling of various node kinds, making closure capture logic more robust and maintainable. Also updates test WAT files to reflect changes in closure environment global naming.

Refactor closure function creation logic

Removed unused parameters and redundant local set in array rest parameter initialization. Simplified compileClosureFunctionCreation by removing the staticOffset argument, as it is no longer used.
Refactor closure capture analysis and remove collectCapturedNames
@BlobMaster41 BlobMaster41 marked this pull request as draft December 12, 2025 02:18
@BlobMaster41 BlobMaster41 marked this pull request as ready for review January 26, 2026 08:30
@BlobMaster41 BlobMaster41 force-pushed the main branch 3 times, most recently from 5d3a559 to 64e11d9 Compare January 27, 2026 02:29
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