Skip to content

Conversation

@BlobMaster41
Copy link

Fixes AssemblyScript#798.
Related: AssemblyScript#173, AssemblyScript#563, AssemblyScript#2054.

Changes proposed in this pull request:

Added experimental closures feature - Closures can now capture variables from their enclosing scope. This includes support for:

  • Capturing parameters and local variables (both let and var)
  • Capturing this directly in class methods
  • Read and write access to captured variables (reference semantics)
  • Multiple closures sharing the same environment
  • Deeply nested closures (capturing from grandparent scopes)
  • Higher-order functions (map, filter, reduce patterns)
  • Factory patterns, memoization, and other functional programming idioms

Implemented as an opt-in feature flag - Closures are disabled by default to maintain backwards compatibility and expected behavior. Users must explicitly enable the feature with --enable closures. This ensures:

  • No changes to existing code behavior
  • No additional runtime overhead for code that doesn't use closures
  • Indirect calls without closures enabled use simpler codegen

Added compile-time constant ASC_FEATURE_CLOSURES - Allows conditional compilation based on whether closures are enabled

Added comprehensive test suites:

  • closure.ts - Basic closure patterns (captures, mutations, shared environments)
  • closure-stress.ts - Stress tests covering many edge cases (616 lines)
  • closure-class.ts - Complex class patterns with closures (1000+ lines) including:
    • State management (BankAccount, Counter)
    • Design patterns (Builder, Factory, Observer, State Machine, Iterator)
    • Inheritance with closures
    • Callback-based async patterns
    • Tree traversal with recursive closures

Implementation Details

The implementation follows the approach discussed in AssemblyScript#798:

Closure Environment:

  • Captured variables are stored in a heap-allocated environment structure
  • Multiple closures in the same scope share a single environment (reference semantics)
  • Nested closures maintain a chain of environments via parent pointers
  • Environment structures are managed by the runtime (GC-compatible)

First-Class Functions:

  • Function references contain both a table index and an _env pointer
  • When calling through a function reference, the _env is loaded and made available
  • Non-closure functions have _env = 0, avoiding overhead when closures aren't used

Core changes in src/compiler.ts:

  • Pre-scan phase (prescanForClosures) identifies closures and captured variables before compilation
  • Environment allocation creates heap-allocated storage for captured variables
  • Closure load/store operations route through the environment
  • Dynamic Function objects are created with environment pointers for closures
  • Feature checks gate closure functionality at detection points
  • Indirect calls only set up closure environment handling when the feature is enabled

Supporting changes:

  • std/assembly/shared/feature.ts - Added Feature.Closures enum value
  • src/common.ts - Added ASC_FEATURE_CLOSURES constant name
  • src/program.ts - Registered compile-time constant
  • src/index-wasm.ts - Exported FEATURE_CLOSURES for CLI
  • src/flow.ts - Added flow tracking for captured variables

Limitations

This is an experimental implementation. Known limitations:

  • Performance: Each closure creation involves a heap allocation for the environment

Usage

# Closures disabled (default) - captures produce an error
asc myfile.ts

# Enable closures explicitly
asc myfile.ts --enable closures
// Example closure usage (requires --enable closures)
function makeCounter(): () => i32 {
  let count = 0;
  return (): i32 => {
    count += 1;
    return count;
  };
}

let counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3
  • 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.
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.

Implement closures

2 participants