Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invalid transpiled/runtime code with the new disposable/using feature in 1.37.0+ release #20742

Closed
nktpro opened this issue Sep 30, 2023 · 4 comments
Labels
bug Something isn't working correctly swc related to swc (bundling/transpiling) upstream Changes in upstream are required to solve these issues

Comments

@nktpro
Copy link

nktpro commented Sep 30, 2023

As of the latest 1.37.1 release, deno supports the new TypeScript 5.2 Explicit Resource Management feature.

However, it transpiles into invalid code which will cause unexpected runtime errors. The strange thing is test-compiling the same code snippet using tsc directly will produce valid code.

Here's a simple reproducer:

function useFoo() {
  return {
    foo: "foo",
    [Symbol.dispose]() {
      console.log("Dispose foo");
    },
  };
}

using disposableFoo = useFoo();
const { foo } = disposableFoo;

const bar = "bar";

function bug() {
  // Will throw error: Uncaught ReferenceError: bar is not defined
  console.log("bar is", bar, "foo is", foo);
}

bug();

Saving that into a file named using_bug.ts and running it with deno run using_bug.ts will throw:

error: Uncaught ReferenceError: bar is not defined
  console.log("bar is", bar, "foo is", foo);

As an attempt to reveal the transpiled JS code behind the scene, running deno bundle ... will produce:

// Omitted helper methods ....

function useFoo() {
    return {
        foo: "foo",
        [Symbol.dispose] () {
            console.log("Dispose foo");
        }
    };
}
function bug() {
    console.log("bar is", bar, "foo is", foo);
}
try {
    var _stack = [];
    var disposableFoo = _using(_stack, useFoo());
    const { foo: foo1 } = disposableFoo;
    bug();
} catch (_) {
    var _error = _;
    var _hasError = true;
} finally{
    _dispose(_stack, _error, _hasError);
}

Which indeed explains that runtime error. (bar is gone and foo is renamed to foo1 in the wrong scope.

On the contrary, tsc version 5.2.2 (which matches the TypeScript version of deno 1.37.1 exactly) will produce:

// Omitted helper methods ....

function useFoo() {
    return {
        foo: "foo",
        [Symbol.dispose]() {
            console.log("Dispose foo");
        },
    };
}
function bug() {
    console.log("bar is", bar, "foo is", foo);
}
var disposableFoo, foo, bar;
const env_1 = { stack: [], error: void 0, hasError: false };
try {
    disposableFoo = __addDisposableResource(env_1, useFoo(), false);
    ({ foo } = disposableFoo);
    bar = "bar";
    bug();
}
catch (e_1) {
    env_1.error = e_1;
    env_1.hasError = true;
}
finally {
    __disposeResources(env_1);
}

Where foo and bar are hoisted with var and does not share the same bug.

In case it's helpful, here's the content of tsconfig.json when running tsc which produced the output above:

{
  "extends": "@tsconfig/recommended/tsconfig.json",
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2023", "esnext.disposable", "dom"]
  },
  "include": ["src/**/*"],
}
@bartlomieju bartlomieju added bug Something isn't working correctly upstream Changes in upstream are required to solve these issues swc related to swc (bundling/transpiling) labels Sep 30, 2023
@lino-levan
Copy link
Contributor

As you might have been able to deduce from the tags that Bartek set, Deno doesn't actually use TSC to transpile Typescript (they instead use SWC for performance). You can play around in the SWC playground to reproduce the bug.

@nktpro
Copy link
Author

nktpro commented Oct 8, 2023

@lino-levan thanks for the pointer!

The great news for anyone excited about leveraging using in Deno is that this has just been fixed in swc. I confirmed that this was no longer reproducible in swc in its latest release 1.3.92

Hopefully this makes it into the next Deno release as well.

@jsejcksn
Copy link
Contributor

jsejcksn commented Nov 2, 2023

Here's another reproduction case:

Source: example.ts

function main() {
  var _stack = "foo";
  console.log(eval("_stack"));
  using bar = null;
  console.log(bar);
}

main();

Terminal:

% deno --version
deno 1.38.0 (release, aarch64-apple-darwin)
v8 12.0.267.1
typescript 5.2.2

% deno run example.ts
foo
null
error: Uncaught RangeError: Maximum call stack size exceeded
Warning Couldn't format source line: Column 32 is out of bounds (source may have changed at runtime)
    at Function.prepareStackTrace (ext:core/02_error.js:126:28)
    at new SuppressedError1 (file:///Users/deno/example.ts:8:32)
    at err1 (file:///Users/deno/example.ts:34:26)
    at next1 (file:///Users/deno/example.ts:28:16)
    at err1 (file:///Users/deno/example.ts:36:12)
    at next1 (file:///Users/deno/example.ts:28:16)
    at err1 (file:///Users/deno/example.ts:36:12)
    at next1 (file:///Users/deno/example.ts:28:16)
    at err1 (file:///Users/deno/example.ts:36:12)
    at next1 (file:///Users/deno/example.ts:28:16)

Transpiled: example.ts.js

function dispose_SuppressedError(suppressed1, error1) {
  if (typeof SuppressedError1 !== "undefined") {
    dispose_SuppressedError = SuppressedError1;
  } else {
    dispose_SuppressedError = function SuppressedError1(suppressed1, error1) {
      this.suppressed = suppressed1;
      this.error = error1;
      this.stack = new Error().stack;
    };
    dispose_SuppressedError.prototype = Object.create(Error.prototype, {
      constructor: {
        value: dispose_SuppressedError,
        writable: true,
        configurable: true
      }
    });
  }
  return new dispose_SuppressedError(suppressed1, error1);
}
function _dispose(stack1, error1, hasError1) {
  function next1() {
    while(stack1.length > 0){
      try {
        var r1 = stack1.pop();
        var p1 = r1.d.call(r1.v);
        if (r1.a) return Promise.resolve(p1).then(next1, err1);
      } catch (e1) {
        return err1(e1);
      }
    }
    if (hasError1) throw error1;
  }
  function err1(e1) {
    error1 = hasError1 ? new dispose_SuppressedError(e1, error1) : e1;
    hasError1 = true;
    return next1();
  }
  return next1();
}
function _using(stack1, value1, isAwait1) {
  if (value1 === null || value1 === void 0) return value1;
  if (typeof value1 !== "object") {
    throw new TypeError("using declarations can only be used with objects, null, or undefined.");
  }
  if (isAwait1) {
    var dispose1 = value1[Symbol.asyncDispose || Symbol.for("Symbol.asyncDispose")];
  }
  if (dispose1 === null || dispose1 === void 0) {
    dispose1 = value1[Symbol.dispose || Symbol.for("Symbol.dispose")];
  }
  if (typeof dispose1 !== "function") {
    throw new TypeError(`Property [Symbol.dispose] is not a function.`);
  }
  stack1.push({
    v: value1,
    d: dispose1,
    a: isAwait1
  });
  return value1;
}
function main() {
  try {
    var _stack = [];
    var _stack = "foo";
    console.log(eval("_stack"));
    var bar = _using(_stack, null);
    console.log(bar);
  } catch (_) {
    var _error = _;
    var _hasError = true;
  } finally{
    _dispose(_stack, _error, _hasError);
  }
}
main();

The transpiled file reveals that the _stack variable is declared twice in the transformed JavaScript.

@dsherret
Copy link
Member

The original issue is now fixed, so closing.

The _stack issue is probably related to the order of the transforms in deno_ast as I'm unable to reproduce it in swc. I don't think it's worth spending time on fixing because I doubt someone would write that code and v8 should support this soon and so we'll be able to get rid of this transform then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working correctly swc related to swc (bundling/transpiling) upstream Changes in upstream are required to solve these issues
Projects
None yet
Development

No branches or pull requests

5 participants