ECMAScript explicit resource management
Clone or download
Latest commit 42be50c Nov 8, 2018
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.vscode Initial commit Jan 25, 2018
docs Initial commit Jan 25, 2018
src Initial commit Jan 25, 2018
.gitattributes Initial commit Jan 25, 2018
.gitignore Initial commit Jan 25, 2018
.yo-rc.json Initial commit Jan 25, 2018
LICENSE Initial commit Jan 25, 2018
README.md Update link to spec Nov 8, 2018
gulpfile.js Initial commit Jan 25, 2018
package-lock.json Update gulp-live-server Jul 25, 2018
package.json Update gulp-live-server Jul 25, 2018

README.md

ECMAScript explicit resource management

This proposal intends to address a common pattern in software development regarding the lifetime and management of various resources (memory, I/O, etc.). This pattern generally includes the allocation of a resource and the ability to explicitly release critical resources.

For example, ECMAScript Generator Functions expose this pattern through the return method, as a means to explicitly evaluate finally blocks to ensure user-defined cleanup logic is preserved:

function * g() {
  const handle = acquireFileHandle(); // critical resource
  try {
    ...
  }
  finally {
    handle.release(); // cleanup
  }
}

const obj = g();
try {
  const r = obj.next();
  ...
}
finally {
  obj.return(); // calls finally blocks in `g`
}

As such, we propose the adoption of a syntax to simplify this common pattern:

function * g() {
  using (const handle = acquireFileHandle()) { // critical resource
    ...
  } // cleanup
}

using (const obj = g()) {
  const r = obj.next();
  ...
} // calls finally blocks in `g`

Status

Stage: 0
Champion: Ron Buckton (@rbuckton)

For more information see the TC39 proposal process.

Authors

  • Ron Buckton (@rbuckton)

Motivations

This proposal is motivated by a number of cases:

  • Inconsistent patterns for resource management:
    • ECMAScript Iterators: iterator.return()
    • WHATWG Stream Readers: reader.releaseLock()
    • NodeJS FileHandles: handle.close()
  • Avoiding common footguns when managing resources:
    const reader = stream.getReader();
    ...
    reader.releaseLock(); // Oops, should have been in a try/finally
  • Scoping resources:
    const handle = ...;
    try {
      ... // ok to use `handle`
    }
    finally {
      handle.close();
    }
    // not ok to use `handle`, but still in scope
  • Avoiding common footguns when managing multiple resources:
    const a = ...;
    const b = ...;
    try {
      ...
    }
    finally {
      a.close(); // Oops, issue if `b.close()` depends on `a`.
      b.close(); // Oops, `b` never reached if `a.close()` throws.
    }
  • Avoiding lengthy code when managing multiple resources correctly:
    { // block avoids leaking `a` or `b` to outer scope
      const a = ...;
      try {
        const b = ...;
        try {
          ...
        }
        finally {
          b.close(); // ensure `b` is closed before `a` in case `b`
                     // depends on `a`
        }
      }
      finally {
        a.close(); // ensure `a` is closed even if `b.close()` throws
      }
    }
    // both `a` and `b` are out of scope
    Compared to:
    // avoids leaking `a` or `b` to outer scope
    // ensures `b` is disposed before `a` in case `b` depends on `a`
    // ensures `a` is disposed even if disposing `b` throws
    using (const a = ..., b = ...) { 
      ...
    }
  • Non memory/IO applications:
    import { ReaderWriterLock } from "prex";
    const lock = new ReaderWriterLock(); 
    
    export async function readData() {
      // wait for outstanding writer and take a read lock
      using (await lock.read()) { 
        ... // any number of readers
        await ...; 
        ... // still in read lock after `await`
      } // release the read lock
    }
    
    export async function writeData(data) {
      // wait for all readers and take a write lock
      using (await lock.write()) { 
        ... // only one writer
        await ...;
        ... // still in write lock after `await`
      } // release the write lock
    }

Prior Art

Syntax

// using expression
using (obj) {
  ...
}

// using with local binding
using (const x = expr1) {
  ...
}

// using with multiple local bindings
using (const x = expr1, y = expr2) {
  ...
}

Grammar

UsingStatement[Yield, Await, Return] :
    // NOTE: This will require a cover grammar to handle ambiguity between a call to a 
    // function named `using` and a UsingStatement head.
    `using` `(` [lookahead ∉ { `let [` }] Expression[+In, ?Yield, ?Await] `)` [no LineTerminator here] Block[?Yield, ?Await, ?Return]
    `using` `(` `var` VariableDeclarationList[+In, ?Yield, ?Await] `)` [no LineTerminator here] Block[?Yield, ?Await, ?Return]
    `using` `(` LetOrConst BindingList[+In, ?Yield, ?Await] `)` [no LineTerminator here] Block[?Yield, ?Await, ?Return]

Notes:

  • We define using as requiring a [no LineTerminator here] restriction to avoid backwards compatibility issues due to ASI, as using is not a reserved word.
  • In addition using requires a Block rather than allowing Statement, as it has more in common with try, catch, or finally than statements with a similar grammar.
  • To avoid ambiguity with CallExpression, a cover grammar would be needed.
  • We may opt to instead augment TryStatement syntax in a fashion similar to Java's try-with-resources, e.g. try (expr) {} or try (let x = expr) {}, however the oddity of the implied finally might be a source of confusion for users.
  • We allow var declarations for consistency with other control-flow statements that support binding declarations in a parenthesized head, such as for, for..in, and for..of.

Semantics

using existing resources

UsingStatement : 
  `using` `(` Expression `)` Block

When using is parsed with an Expression, an implicit block-scoped binding is created for the result of the expression. When the using block is exited, whether by an abrupt or normal completion, [Symbol.dispose]() is called on the local binding as long as it is neither null nor undefined.

using (expr) {
  ...
}

The above example has the same approximate runtime semantics as the following transposed representation:

{ 
  const $$temp = expr;
  try {
    ...
  }
  finally {
    if ($$temp !== null && $$temp !== undefined) $$temp[Symbol.dispose]();
  }
}

The local block-scoped binding ensures that if expr above is reassigned, we still correctly close the resource we are explicitly tracking.

using with explicit local bindings

UsingStatement: 
  `using` `(` `var` VariableDeclarationList `)` Block
  `using` `(` LexicalDeclaration `)` Block

When using is parsed with either a VariableDeclarationList or a LexicalDeclaration, we again create implicit block-scoped bindings for the initializers of each VariableDeclaration or LexicalBinding:

using (let x = expr1, y = expr2) {
  ...
}

These implicit bindings are again used to perform resource disposal when the Block exits, however in this case [Symbol.dispose]() is called on the implicit bindings in the reverse order of their declaration. This is equivalent to the following:

using (let x = expr1) {
  using (let y = expr2) {
    ...
  }
}

Both of the above cases would have the same runtime semantics as the following transposed representation:

{
  const $$temp1 = expr1;
  try {
    let x = $$temp1;
    {
      const $$temp2 = expr2;
      try {
        let y = $$temp2;
        ...
      }
      finally {
        if ($$temp2 !== null && $$temp2 !== undefined) $$temp2[Symbol.dispose]();
      }
    }
  }
  finally {
    if ($$temp1 !== null && $$temp1 !== undefined) $$temp1[Symbol.dispose]();
  }
}

Since we must always ensure that we properly release resources, we must ensure that any abrupt completion that might occur during binding initialization results in evaluation of the cleanup step. This also means that when there are multiple declarations in the list we must create a new try/finally-like protected region for each declaration. As a result, we must release resources in reverse order.

using with binding patterns

The using statement always creates implicit local bindings for the Initializer of the VariableDeclaration or LexicalBinding. For binding patterns this means that we store the value of expr in the example below, rather than y:

using (let { x, y } = expr) {
}

This aligns with how destructuring would work in the same scenario, as the completion value for a destructuring assignment is always the right-hand value:

let x, y;
using ({ x, y } = expr) {
}

This behavior also avoids possible refactoring hazards as you might switch between various forms of semantically equivalent code. For example, consider the following changes as they might occur over time:

// before:
let obj = expr, x, y;
using (obj) {
  x = obj.x;
  y = obj.y;
  ...
}


// after refactor into binding pattern:
let obj = expr;
using (obj) {
  let { x, y } = obj; // `obj` is otherwise unused
  ...
}


// after inline `obj` declaration into `using` statement:
using (let obj = expr) {
  let { x, y } = obj; // `obj` is otherwise unused
  ...
}


// after refactor away single use of `obj`:
using (let { x, y } = expr) {
  ...
}

In the above example, in all four cases the value of expr is what is disposed.

The same result could also be achieved through other refactorings in which each step also results in semantically equivalent code:

// before:
let obj = expr, x, y;
using (obj) {
  x = obj.x;
  y = obj.y;
  ...
}


// after refactor into assignment pattern:
let obj = expr, x, y;
using (obj) {
  ({ x, y } = obj);
  ...
}


// after move assignment pattern into head of `using`:
let obj = expr, x, y;
using ({ x, y } = obj) {
  ...
}


// after refactor away single use of `obj`:
let x, y;
using ({ x, y } = expr) {
  ...
}

As with the first set of refactorings, in all four cases it is the value of expr that is disposed.

using on null or undefined values

This proposal has opted to ignore null and undefined values provided to the using statement. This is similar to the behavior of using in languages like C# that also allow null. One primary reason for this behavior is to simplify a common case where a resource might be optional, without requiring duplication of work:

using (const resource = isResourceAvailable() ? getResource() : undefined) {
  ... // (1) do some work with or without resource
  if (resource) resource.doSomething();
  ... // (2) do some other work with or without resource
}

Compared to:

if (isResourceAvailable()) {
  using (const resource = getResource()) {
    ... // (1) above
    resource.doSomething()
    ... // (2) above
  }
}
else {
  ... // (1) above
  ... // (2) above
}

using on values without [Symbol.dispose]

If a resource does not have a callable [Symbol.dispose] member, a TypeError would be thrown at the end of the Block when the member would be invoked.

using in AsyncFunction or AsyncGeneratorFunction

In an AsyncFunction or an AsyncGeneratorFunction, at the end of a using block we first look for a [Symbol.asyncDispose] method before looking for a [Symbol.dispose] method. If we found a [Symbol.asyncDispose] method, we Await the result of calling it.

Examples

WHATWG Streams API

using (const reader = stream.getReader()) {
  const { value, done } = reader.read();
  ...
}

NodeJS FileHandle

using (const f1 = fs.promises.open(f1, constants.O_RDONLY), 
             f2 = fs.promises.open(f2, constants.O_WRONLY)) {
  const buffer = Buffer.alloc(4092);
  const { bytesRead } = await f1.read(buffer);
  await f2.write(buffer, 0, bytesRead);
}

Transactional Consistency (ACID)

// roll back transaction if either action fails
using (const tx = transactionManager.startTransaction(account1, account2)) {
  await account1.debit(amount);
  await account2.credit(amount);

  // mark transaction success
  tx.succeeded = true;
}

Other uses

// audit privileged function call entry and exit
function privilegedActivity() {
  using (auditLog.startActivity("privilegedActivity")) {
    ...
  }
}

API

This proposal adds the properties dispose and asyncDispose to the Symbol constructor whose values are the @@dispose and @@asyncDispose internal symbols, respectively:

interface SymbolConstructor {
  readonly dispose: symbol;
  readonly asyncDispose: symbol;
}

In addition, the methods [Symbol.dispose] and [Symbol.asyncDispose] methods would be added to %GeneratorPrototype% and %AsyncGeneratorPrototype%, respectively. Each method, when called, calls the return method on those prototypes.

TODO

The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:

Stage 1 Entrance Criteria

  • Identified a "champion" who will advance the addition.
  • Prose outlining the problem or need and the general shape of a solution.
  • Illustrative examples of usage.
  • High-level API.

Stage 2 Entrance Criteria

Stage 3 Entrance Criteria

Stage 4 Entrance Criteria

  • Test262 acceptance tests have been written for mainline usage scenarios and merged.
  • Two compatible implementations which pass the acceptance tests: [1], [2].
  • A pull request has been sent to tc39/ecma262 with the integrated spec text.
  • The ECMAScript editor has signed off on the pull request.