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

Scopes: support for hiding new injected scopes from the stack trace #113

Open
nicolo-ribaudo opened this issue Jul 2, 2024 · 4 comments

Comments

@nicolo-ribaudo
Copy link
Member

While we focused a lot on function inlining, sometimes Babel does the opposite. For example, given this input:

for (let x of arr) {
  console.trace(x);
  run(() => x);
}

Babel generates this code:

var _iterator = _createForOfIteratorHelper(arr),
  _step;
try {
  var _loop = function _loop() {
    var x = _step.value;
    console.trace(x);
    run(function () {
      return x;
    });
  };
  for (_iterator.s(); !(_step = _iterator.n()).done;) {
    _loop();
  }
} catch (err) {
  _iterator.e(err);
} finally {
  _iterator.f();
}

With the scopes proposal, is it possible to somehow mark _loop so that it does not appear in the stack when placing a breakpoint at console.trace(x)?

@szuend
Copy link
Collaborator

szuend commented Jul 2, 2024

Intuitively I would have said we should emit a generated range for _loop with no definition. That is it does not point to any authored scope back. This is not implemented yet but I can give it a go and see if such an approach is sufficient.

@jridgewell
Copy link
Member

I agree with @szuend, that's a generated range without an original scope. This should already be implemented in my beta scopes branch.

@szuend
Copy link
Collaborator

szuend commented Jul 3, 2024

I didn't implement anything yet but just thinking out loud. I modified the example a bit so we have the following input:

function run(x) {
  x();
}

function foo() {
  for (let x of [1, 2, 3]) {
    console.trace(x);
    run(() => x);
  }
}

foo();

And Babel generates:

function run(x) {
  x();
}
function foo() {
  var _loop = function _loop() {
    var x = _arr[_i];
    console.trace(x);
    run(function () {
      return x;
    });
  };
  for (var _i = 0, _arr = [1, 2, 3]; _i < _arr.length; _i++) {
    _loop();
  }
}
foo();

Now I would expect the following generated ranges (among others): One for foo, that points back the the original foo function scope. A generated range for

    var x = _arr[_i];
    console.trace(x);
    run(function () {
      return x;
    });

that points back to the original block scope for the for loop. And a generated range for the _loop(); call site (or the whole generated for loop body) with no original scope definition.

The stack trace for pausing on the console.trace line would be:

_loop (script.js 7:5)
foo (script.js 13:5)
<anonymous> (script.js 17:1)

For _loop (script.js 7:5) we are inside a generated range with a definition. So we look at that original block scope and follow the scope chain outwards to the original function scope (foo). Using the name of that original function scope and standard "mappings" we would translate _loop (script.js 7:5) to foo (original.js 7:5) because the console.trace call is actually on the same position in both original and authored.

foo (script.js 13:5) is inside a generated range with no definition so we drop the stack frame.

<anonymous> (script.js 17:1) would probably be in a generated range that maps to the script/global scope so we keep and map it.

The resulting stack trace would be:

foo (original.js 7:5)
<anonymous> (origianl.js 12:1)

Note that this approach uses the current proposal only. It would require generators to hide functions from stack traces by "masking" their call-sites with generated ranges that have no definition.

Second note: Emitting a generated range for the whole var _loop = function _loop() ... with no definition doesn't help us since you actually want to hide calls to this function not the actual function body as that corresponds to actual authored code.

Hope all of this makes sense. Please keep more examples coming, this is a great way to think through if the proposal actually works for the various transformations babel and other generators are doing.

@szuend
Copy link
Collaborator

szuend commented Jul 5, 2024

Alternative implementation:

If we add a GeneratedRange.isFunctionScope flag (or rename the existing isScope flag, then we can also implement this differently without generators having to "mask" call-sites to hidden functions.

Then processing a stack trace would look something like this:

  • For the first (top-most) stack frame do the same as outlined above
  • For every other stack frame ("x") find the closest generated range in the previous frame ("x - 1"). Hide the frame "x" from the stack trace if range.isFunctionScope && !range.hasDefinition is true for the found generated range.

For the above example this would mean: Generators would emit a range for var _loop = function _loop() ... with no definition and isFunctionScope: true. When we process the second frame foo (script.js 13:5) we look at the previous frame _loop (script.js 7:5) and find the closest isFunctionScope: true generated range from 7:5 going outwards. We find one and it doesn't have a definition so we can omit the second frame from the stack trace.

Why a isHidden flag won't work

Just annotating generated ranges with isHidden, is not enough. There could be multiple generated ranges in the chain that correspond to JS functions, but we have no way to tell. For example:

function foo() {
   // ...
   function bar() {
     fnThatThrows();
   }
}

bar should be visible but foo should be hidden. With just an isHidden flag this won't since we would find foo as a hidden range when doing the check for the fnThatThrows call-site in bar.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants