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

Fix decorator evaluation private environment #16325

Merged

Conversation

JLHwung
Copy link
Contributor

@JLHwung JLHwung commented Mar 5, 2024

Q                       A
Fixed Issues? Fixes #16295
Patch: Bug Fix? Yes
Major: Breaking Change?
Minor: New Feature?
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes?
License MIT

In this PR we move the element decorator evaluation from the preface of class declaration into computed key.

Previously they are evaluated outside the class, immediate after class decorator evaluations. However, because the element decorators are defined within the class syntax, they should have access to the private variables defined in the current class.

The current evaluation storage:

  1. The computed key are the ideal place to store element decorator evaluations because they share the same scope.
  2. If a computed key is not available, we will convert the first public key into a computed key.
  3. If there is no public element, we will inject a temporary static field to host the evaluation. The field will be deleted immediately after it is defined.
    If a class has only private elements and static blocks, we will insert the decorator evaluations before the applyDecs call. We will replace all function context usage before moving them into the static block. However, this is still not safe because decorators expressions / computed keys inherit the outer level [Yield] and [Await] production parameter, while the static block where we insert applyDecs call can not be a generator / async function. Given that the scenaraio of private-only class is rare, I would like to trade it with this general private fix.

I suggest review this PR by commits.

@JLHwung JLHwung added PR: Bug Fix 🐛 A type of pull request used for our changelog categories Spec: Decorators labels Mar 5, 2024
@babel-bot
Copy link
Collaborator

babel-bot commented Mar 5, 2024

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/56452

}

expect([...A].map(fn => fn(B)).join(",")).toBe("B#X,B#X,B#X,B#X,B#X,B#X,B#X,B#X,B#X,B#X");
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This edge case test is disabled because I can't figure out how to transform it.

Copy link
Contributor Author

@JLHwung JLHwung Mar 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, can we introduce a new public element and delete it immediately after defined?

class C {
  // for demo purpose, the `delete` expression can be directly inserted into staticInitializerAssignments
  @(function(_, context) { context.addInitializer(function() { delete this._ } })
  static [(/* decorator evaluations */, "_")];
}

Of course using delete is a performance killer, but this approach will be only invoked when there is no public class elements, which should be quite rare. By doing so we can get rid of all function context variable replacement since the decorator expressions are guaranteed to be inserted within a computed key.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this case is rare enough that:

  • the perf hit is ok
  • the perf it doesn't actually matter, because you are not going to access properties on instances of this class (because it, well... has no properties)

for delete, let's put it in a static { delete this._ } block at the beginning of the class?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me.
We can even do it when someone needs it. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for delete, let's put it in a static { delete this._ } block at the beginning of the class?

I plan to insert the delete expression right before the applyDecs call, since its parent block is already the first class element.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is now enabled and merged with decorator-evaluation-yield-private-super-property/exec.js

It also happens to catch a bug in the helper-replace-supers, currently Babel is incorrectly replacing the super in the decorators of the given class method. This should have been handled in the environmentVisitor but the root class method path is not visited because the visitSelf argument of traverse.node is not exposed to the path.traverse.

707b91c

}

B.m();
expect(receivedName).toBe("B");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was renamed to "decorator-evaluation-this" and then augmented.

Comment on lines -1 to +2
var _initStatic, _init_a, _init_a2, _get_a, _set_a, _init_computedKey, _init_computedKey2, _init_computedKey3, _init_computedKey4, _init_computedKey5, _init_computedKey6, _computedKey, _init_computedKey7, _Foo;
let _computedKey;
var _initStatic, _init_a, _init_a2, _get_a, _set_a, _init_computedKey, _init_computedKey2, _init_computedKey3, _init_computedKey4, _init_computedKey5, _init_computedKey6, _init_computedKey7, _Foo;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we revert this change?

Copy link
Contributor Author

@JLHwung JLHwung Mar 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is intentional as it will fix an integration issue with the class properties transform. For context see https://github.com/babel/babel/pull/10029/files#r287785963

In this REPL example

function noop() {}
const classes = [];
for (let i = 0; i < 5; ++i) {
  classes.push(
    class A {
      @noop
      [i] = "my property";
    }
  );
}
var result = [];
for (const clazz of classes) {
  result.push(Object.getOwnPropertyNames(new clazz())[0]);
}
console.log(result.join())

when we apply the decorators, static block and class properties plugin, it should print 0,1,2,3,4 but currently it prints 4,4,4,4,4.

I can add a new test case for this issue.

If the output size is the concern, I can open a new PR to register all variable as the let binding, as it will introduce much more test output changes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please add a test!

Regarding to output size, we should first aim at correctness and then optimization.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I didn't notice this was a bug fix, I thought the increased size didn't matter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolo-ribaudo
Copy link
Member

"I suggest review this PR by commits." ~ 13 commits 😛

const expressions = path.get("expressions");
return getComputedKeyCompletion(expressions[expressions.length - 1]);
}
return skipTransparentExprWrappers(path);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this recourse if the result of skipTransparentExprWrappers is a SequenceExpression?

Comment on lines 311 to 312
const key = fieldPath.get("key") as NodePath<t.Expression>;
expressions.push(key.node);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If key is already a sequence expression maybe we should do expression.push(...key.node.expressions)?

Or -- this is probably so rare that it doesn't matter, but in this case could we have a test showing that we generated a "nested" sequence expression?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a key is already a sequence expression, it could be generated by another Babel plugin. The intention here is try to reuse the uid memoiser, for example the computed keys transform can reuse the memoiser _computedKeys generated by the decorator transform:

class C {
  [decs = f(), _computedKey = g(), another_decs = h(), _computedKey];
}

but at this point there is no class features running before decorators, so there aren't much practical examples. I will add a test case for nested sequence expression.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

);
}
} else {
// If the computed key is an uid reference, treat it as
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why if we get to this else branch the key is an uid reference?

EDIT: Oh I see -- could you rename memoiseComputedKey to memoiseComputedKeyIfNotUid, or add a comment before the call saying that it returns undefined if and only if it's a uid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 136 to 137
if (isUidReference) {
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Move this early return to before const isMemoiseAssignment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

expect([...A].map(fn => fn(B)).join(",")).toBe("B#X,B#X,B#X,B#X,B#X,B#X,B#X,B#X,B#X,B#X");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this case is rare enough that:

  • the perf hit is ok
  • the perf it doesn't actually matter, because you are not going to access properties on instances of this class (because it, well... has no properties)

for delete, let's put it in a static { delete this._ } block at the beginning of the class?


const computedKeysPath = applyDecoratorWrapperPath.get("body")[0];

// Capture lexical this and super call, replace their usage in computed key assignments
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would

function hoistFunctionEnvironment(
work here? If yes, can we export that function and use it for Babel 8, rather than duplicating the logic forever?

Or maybe we could even "polyfill" it for Babel 7, doing terrible stuff:

function hoistFunctionEnvironment(path) {
  Object.defineProperty(path, "isArrowFunctionExpression", {
    get() {
      delete path.isArrowFunctionExpression;
      return () => true;
    }
  });
  return path.unwrapFunctionEnvironment();
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I didn't realize we have such a utility in the traverse library. However I am down to the temporary property approach and hopefully we can remove all the lexical variables capturing logic here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -1659,6 +1672,32 @@ function transformClass(
}

originalClass.body.body.unshift(t.staticBlock([]));
if (computedKeyAssignments.length > 0) {
// todo: the following branch will fail on 2023-05 version
if (version === "2023-11") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process.env.BABEL_8_BREAKING || version === "2023-11"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is eventually removed in bdee20d.

static m() {
@noop
class C {
@(receivedName = super.name, noop) p;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test with a decorator that references both super and a private field in the same decorator?

Copy link
Contributor Author

@JLHwung JLHwung Mar 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this test look good?

class A extends CaptureFactory {
static #X = "A#X";
static *[Symbol.iterator] () {
B = class B {
static #X = "B#X";
@(yield* super.id(_ => _.#X), dummy) method () {}
@(yield* super.id(_ => _.#X), dummy) static method () {}
@(yield* super.id(_ => _.#X), dummy) get getter () {}
@(yield* super.id(_ => _.#X), dummy) static get getter () {}
@(yield* super.id(_ => _.#X), dummy) set setter (v) {}
@(yield* super.id(_ => _.#X), dummy) static set setter (v) {}
@(yield* super.id(_ => _.#X), dummy) property;
@(yield* super.id(_ => _.#X), dummy) static property;
@(yield* super.id(_ => _.#X), dummy) accessor accessor;
@(yield* super.id(_ => _.#X), dummy) static accessor accessor;
}
}
}

Edit: I can add a super-call flavor if you mean super().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ok, I somehow missed it.

@liuxingbaoyu
Copy link
Member

liuxingbaoyu commented Mar 5, 2024

test262 seems to be relevant, while the other CI failures are not.

Considering that this PR rebase may be complicated, we can postpone the #16326 merge.

@JLHwung JLHwung force-pushed the fix-decorator-evaluation-private-environment branch from 0b8f0bc to d837c27 Compare March 5, 2024 18:53
static {
_xDecs = dec;
delete this._;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New PR idea: We can optimize them, potentially reusing the usesFnContext diagnostic info of each decorators sequence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: Bug Fix 🐛 A type of pull request used for our changelog categories Spec: Decorators
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: Element decorator referencing both this and local private field is not compiled properly
4 participants