Skip to content

Simpler desugaring without problematic semantics #25

@erights

Description

@erights

Let's start with the following variation of the original example.

enum E {
  A = 1,
  B = A + f(E).C,
  C = 3,
}

Applying the original desugaring to it, we get

let E = (() => {
  let E = Object.create(null), A, B, C;
  Object.defineProperty(E, "A", { value: A = 1 });
  Object.defineProperty(E, "B", { value: B = A + f(E).C }); // NaN
  Object.defineProperty(E, "C", { value: C = 3 });
  Object.defineProperty(E, Symbol.iterator, {
    value: function* () {
      yield ["A", E.A];
      yield ["B", E.B];
      yield ["C", E.C];
    }
  });
  Object.defineProperty(E, Symbol.toStringTag, { value: "E" });
  Object.preventExtensions(E);
  return E;
})();

This has several problems:

  • f(E) is called before E is fully initialized, making E available in its uninitialized state.
  • Even if f were the identity function, or if we had written A + E.C instead, we'd be getting the value of the C property before it is defined. Our normal expectation should be a thrown TDZ error. Instead, it silently initializes E.B to 1 + undefined, which is NaN.

By the simpler desugaring this issue proposes to use instead, it would desugar to

const E = (() => {
  const A = 1;
  const B = A + f(E).C; // TDZ
  const C = 3;
  return Object.freeze({ __proto__: null, A, B, C });
});

which does produce the TDZ programmers should expect, rather than making E available to user code before it is fully initialized, and rather than mistakenly but silently initializing E.B to NaN.

Even the simpler example

enum E {
  A = 1,
  B = A + C,
  C = 3,
}

would still silently set B to NaN in the original desugaring, whereas it would throw a TDZ error in the desugaring we propose here.

The unproblematic way to express the intent of the original example

enum E {
  A = 1,
  B = 2,
  C = A | B, // 3
}

would work identically under both desugarings. In the simpler desugaring proposed here, it desugars to

const E = (() => {
  const A = 1;
  const B = 2
  const C = A | B;
  return Object.freeze({ __proto__: null, A, B, C });
});

What about TypeScript interoperation?

Unproven claim

Any enum declaration that does not throw under the simpler desugaring would not throw under the original desugaring with no observable difference between the two.

We are indeed uncertain about this. Please try to find a counter-example.

Assuming this claim is true, and assuming the original desugaring accurately captures the semantics of the TypeScript compiler's desugaring, then adopting this simpler desugaring into the JS standard is well aligned with the goals of so-called "Erasable TypeScript" systems like ts-blank-space or Node's --experimental-strip-types option. "Erasable TypeScript" would be a subset of full TypeScript where programs that work in Erasable TypeScript also work in full TypeScript with the same meaning.

Erasable Typescript would thus be able to grow to include such enum declarations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions