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

Reuse computed key memoiser #16159

Merged
merged 1 commit into from Dec 11, 2023

Conversation

JLHwung
Copy link
Contributor

@JLHwung JLHwung commented Dec 6, 2023

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

In this PR we reuse the memoising expression of the computed class element key when transforming class fields. This optimizing trick can be applied to other plugins that requires memoising an expression, such as optional chaining and nullish coalescing as well.

I think eventually we should create a new helper for such purpose. For any given expression, an optimized memoising procedure can be

  1. If the expression is a uid reference, treat it as a constant binding and thus it is "static" within the scope
  2. If the expression is an assignment uid = expression, treat uid as the memoiser id, and re-use the uid = expression assignment
  3. Otherwise create a new uid for expression and construct the assignment uid = expression

Currently we only did the step 3 by scope.maybeGenerateMemoised, so Babel will create quite a few redundant memoisers if various transforms are applied to a given expression that needs memoising. For example, transforming a computed class accessor down to ES5 require 3 plugins: decorators, class properties and class transforms.

@JLHwung JLHwung added the PR: Output optimization 🔬 A type of pull request used for our changelog categories label Dec 6, 2023
@babel-bot
Copy link
Collaborator

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

Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

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

Can we eventually port these changes to maybeGenerateMemoised itself?

@JLHwung
Copy link
Contributor Author

JLHwung commented Dec 8, 2023

The challenge of porting these changes to maybeGenerateMemoised is that we have to change the return structure and the compatibility issue kicks in.

Currently maybeGenerateMemoised returns an Identifier when the expression is cached and null otherwise. In our use cases, we construct the assignment from the identifier and insert the assignment somewhere.

For point 1, maybeGenerateMemoised should return whether the expression is a UID, since a UID must have been assigned to some expression somewhere, we can treat the expression as static and use cloneNode elsewhere.

For point 2, the API should return whether the expression is of the format uid = expression. In this case, the API user should decide whether they want to move the whole assignment along with other created assignment to preserve the execution ordering, and insert the uid as references elsewhere.

I am wondering if we need a broader helper to cover these steps, e.g. memoiseComputedKey or memoiseAccessorComputedKey.

Comment on lines +146 to +151
const isUidReference =
t.isIdentifier(computedKey.node) && scope.hasUid(computedKey.node.name);
const isMemoiseAssignment =
computedKey.isAssignmentExpression({ operator: "=" }) &&
t.isIdentifier(computedKey.node.left) &&
scope.hasUid(computedKey.node.left.name);
Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I didn't understand why hasUid is used, can you explain it? Thanks!

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 question. The hasUid checks whether the identifier is generated from scope.generateUidIdentifier, if it is, it is very likely generated by official Babel plugins, intended to be a memoiser. If not, it is from user code and we should handle it more conservatively.

Copy link
Member

@liuxingbaoyu liuxingbaoyu left a comment

Choose a reason for hiding this comment

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

In the future maybe we can use Set or WeakSet to achieve this.

@nicolo-ribaudo
Copy link
Member

I was going to suggest to re-use this new logic in other places, but looking out out tests I only found https://github.com/nicolo-ribaudo/babel/blob/3caeeb18833c4f04ccd3218af492e6fd3a072818/packages/babel-plugin-transform-class-properties/test/fixtures/nested-class/super-call-in-decorator/output.js where this would be useful (_dec = _this = _super.call(this)). However, it is rare enough that it's not worth blocking this PR on it (feel free to improve other similar cases, but also it's ok to decide that it's not necessary :) )

@nicolo-ribaudo nicolo-ribaudo changed the title reuse computed key memoiser Reuse computed key memoiser Dec 11, 2023
@nicolo-ribaudo nicolo-ribaudo merged commit 93dd407 into babel:main Dec 11, 2023
48 checks passed
@nicolo-ribaudo nicolo-ribaudo deleted the reuse-computed-keys-memoiser branch December 11, 2023 10:44
@JLHwung
Copy link
Contributor Author

JLHwung commented Dec 11, 2023

@nicolo-ribaudo Yes there are more optimizable paths. As you mentioned it helps the decorator memoization, we will see more such patterns once we start interleaving computed keys and decorator evaluations. For example,

let brandCheckC;

function captureClassBrandCheck(fn) {
  brandCheckC = fn;
  return v => v
}
class C {
  [Symbol.iterator]() {}
  @captureClassBrandCheck(o => #p in o) #p
}

Currently Babel transforms it to

var _computedKey, _dec, _init_p;
let brandCheckC;
function captureClassBrandCheck(fn) {
  brandCheckC = fn;
  return v => v;
}
_computedKey = _toPropertyKey(Symbol.iterator);
_dec = captureClassBrandCheck(o => #p in o);
class C {
  static {
    [_init_p] = _applyDecs(this, [[_dec, 0, "p", o => o.#p, (o, v) => o.#p = v]], [], 0, _ => #p in _).e;
  }
  [_computedKey]() {}
  #p = _init_p(this);
}

which is invalid because the private brand check #p in o is moved out of the class (See also part 5 of #16117). To address this we can mix computed keys assignment with decorators:

var _computedKey, _dec, _init_p;
let brandCheckC;
function captureClassBrandCheck(fn) {
  brandCheckC = fn;
  return v => v;
}

class C {
  static {
    [_init_p] = _applyDecs(this, [[_dec, 0, "p", o => o.#p, (o, v) => o.#p = v]], [], 0, _ => #p in _).e;
  }
  [_computedKey = _toPropertyKey(Symbol.iterator), _dec = captureClassBrandCheck(o => #p in o), _computedKey]() {}
  #p = _init_p(this);
}

Now if we further transpile the result above to ES5, we should inform the class transform that _computedKey is already a "static" uid, so we don't have to memoize the key again.

Admittedly this case is not very practical, as users can just capture the brand check from the access property of the decorator context.

Vylpes pushed a commit to Vylpes/random-bunny that referenced this pull request Jan 16, 2024
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@babel/traverse](https://babel.dev/docs/en/next/babel-traverse) ([source](https://github.com/babel/babel)) | resolutions | patch | [`7.23.5` -> `7.23.7`](https://renovatebot.com/diffs/npm/@babel%2ftraverse/7.23.5/7.23.7) |

---

### Release Notes

<details>
<summary>babel/babel (@&#8203;babel/traverse)</summary>

### [`v7.23.7`](https://github.com/babel/babel/blob/HEAD/CHANGELOG.md#v7237-2023-12-29)

[Compare Source](babel/babel@v7.23.6...v7.23.7)

##### 🐛 Bug Fix

-   `babel-traverse`
    -   [#&#8203;16191](babel/babel#16191) fix: Crash when removing without `Program` ([@&#8203;liuxingbaoyu](https://github.com/liuxingbaoyu))
-   `babel-helpers`, `babel-plugin-proposal-decorators`
    -   [#&#8203;16180](babel/babel#16180) fix: Class decorator `ctx.kind` is wrong ([@&#8203;liuxingbaoyu](https://github.com/liuxingbaoyu))
-   `babel-plugin-proposal-decorators`
    -   [#&#8203;16170](babel/babel#16170) Fix decorator initProto usage in derived classes ([@&#8203;JLHwung](https://github.com/JLHwung))
-   `babel-core`
    -   [#&#8203;16167](babel/babel#16167) Avoid unpreventable `unhandledRejection` events ([@&#8203;nicolo-ribaudo](https://github.com/nicolo-ribaudo))

##### 🏠 Internal

-   `babel-helper-create-class-features-plugin`
    -   [#&#8203;16186](babel/babel#16186) chore: Update deps ([@&#8203;liuxingbaoyu](https://github.com/liuxingbaoyu))
-   `babel-helper-create-class-features-plugin`, `babel-plugin-proposal-decorators`
    -   [#&#8203;16177](babel/babel#16177) Merge decorators into class features ([@&#8203;JLHwung](https://github.com/JLHwung))

### [`v7.23.6`](https://github.com/babel/babel/blob/HEAD/CHANGELOG.md#v7236-2023-12-11)

[Compare Source](babel/babel@v7.23.5...v7.23.6)

##### 👓 Spec Compliance

-   `babel-generator`, `babel-parser`, `babel-types`
    -   [#&#8203;16154](babel/babel#16154) Remove `TSPropertySignature.initializer` ([@&#8203;fisker](https://github.com/fisker))
-   `babel-helpers`, `babel-plugin-proposal-decorators`, `babel-plugin-transform-class-properties`, `babel-plugin-transform-class-static-block`, `babel-plugin-transform-runtime`, `babel-preset-env`, `babel-runtime-corejs2`, `babel-runtime-corejs3`, `babel-runtime`, `babel-types`
    -   [#&#8203;16139](babel/babel#16139) Apply `toPropertyKey` on decorator context name ([@&#8203;JLHwung](https://github.com/JLHwung))

##### 🐛 Bug Fix

-   `babel-generator`
    -   [#&#8203;16166](babel/babel#16166) fix: Correctly indenting when `retainLines` is enabled ([@&#8203;liuxingbaoyu](https://github.com/liuxingbaoyu))
-   `babel-helpers`, `babel-plugin-proposal-explicit-resource-management`
    -   [#&#8203;16150](babel/babel#16150) `using`: Allow looking up `Symbol.dispose` on a function ([@&#8203;odinho](https://github.com/odinho))
-   `babel-plugin-proposal-decorators`, `babel-plugin-transform-class-properties`
    -   [#&#8203;16161](babel/babel#16161) Ensure the `[[@&#8203;@&#8203;toPrimitive]]` call of a decorated class member key is invoked once ([@&#8203;JLHwung](https://github.com/JLHwung))
    -   [#&#8203;16148](babel/babel#16148) Support named evaluation for decorated anonymous class exp ([@&#8203;JLHwung](https://github.com/JLHwung))
-   `babel-plugin-transform-for-of`, `babel-preset-env`
    -   [#&#8203;16011](babel/babel#16011) fix: `for of` with `iterableIsArray` and shadowing variable  ([@&#8203;liuxingbaoyu](https://github.com/liuxingbaoyu))
-   `babel-helpers`, `babel-plugin-proposal-decorators`, `babel-runtime-corejs2`, `babel-runtime-corejs3`, `babel-runtime`
    -   [#&#8203;16144](babel/babel#16144) Set function name for decorated private non-field elements ([@&#8203;JLHwung](https://github.com/JLHwung))
-   `babel-plugin-transform-typescript`
    -   [#&#8203;16137](babel/babel#16137) Fix references to enum values with merging ([@&#8203;nicolo-ribaudo](https://github.com/nicolo-ribaudo))

##### 🔬 Output optimization

-   `babel-helper-create-class-features-plugin`, `babel-plugin-transform-class-properties`
    -   [#&#8203;16159](babel/babel#16159) Reuse computed key memoiser ([@&#8203;JLHwung](https://github.com/JLHwung))
-   `babel-helpers`, `babel-plugin-proposal-decorators`
    -   [#&#8203;16160](babel/babel#16160) Optimize decorator helper size ([@&#8203;liuxingbaoyu](https://github.com/liuxingbaoyu))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4wLjAiLCJ1cGRhdGVkSW5WZXIiOiIzNy4wLjAiLCJ0YXJnZXRCcmFuY2giOiJkZXZlbG9wIn0=-->

Reviewed-on: https://gitea.vylpes.xyz/RabbitLabs/random-bunny/pulls/129
Co-authored-by: Renovate Bot <renovate@vylpes.com>
Co-committed-by: Renovate Bot <renovate@vylpes.com>
@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Mar 12, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 12, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: Output optimization 🔬 A type of pull request used for our changelog categories
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants