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

Static Class Features: Stage 3 #8052

Closed
rricard opened this issue May 25, 2018 · 9 comments
Closed

Static Class Features: Stage 3 #8052

rricard opened this issue May 25, 2018 · 9 comments

Comments

@rricard
Copy link
Contributor

@rricard rricard commented May 25, 2018

Static Class Features: Stage 3

Advance Static Class Features for Stage 3 (Shu-yu Guo @syg) (Daniel Ehrenberg @littledan)

Proposal co-written with @tim-mc and @robpalme. We also intend to follow up and implement it with @rpamely, @tim-mc, @mkubilayk .

Info

Proposed at TC39 Meeting: May 2018

Slides at the meeting: https://docs.google.com/presentation/d/1YzFr7EIGiX2YagfFMjkI-lVR6ouoRfPbTNLY--NGbC4/edit?usp=sharing

Proposal Repo: https://github.com/tc39/proposal-static-class-features/

Complete article on the subject by Shu-yu Guo: https://rfrn.org/%7Eshu/2018/05/02/the-semantics-of-all-js-class-elements.html

Babel contribution recommended by @jridgewell to @robpalme

The work to be done towards Stage 3 will be added in plugin-proposal-class-properties.

Examples

Static Public Fields

class Account {
  static routingNumber = 1017;
  get transactionIdPrefix() {
    return `${Account.routingNumber}-${this.uuid}`;
  }
}

Static Public Methods

class Account {
  static convertToEuro(dollars) {
     return util.USDToEuro(dollars);
  }

  get euroConversionDisplayAmt () {
    return `Current Balance (EUR): ${Account.convertToEuro(this.accountBalance)}`
  }

Static private fields

class Account {
  static #transactions = [];
  static get nbTransactions() {
    return Account.#transactions.length;
  }
  getDollars() {
    return Account.#transactions.reduce(/* ... */);
  }
}

Static private methods

class Account {
   // ...
  static #makeTransaction(dollars, from, to) {
    Account.#transactions = this.#transactions.concat(/* ... */);
  }
  
  transfer(dollars, targetAccount) {
    return Account.#makeTransaction(dollars, this, targetAccount);
  }
}

Basic Rules

  • Check that the <Class Name>.#<static private field/method> notation is within the lexical scope of <Class Name> - Checked during transform
  • Check that the this.#<static private field/method> has a provenance of <Class Name> - See Subclassing Hazard in the slides (around slide 13) - Checked at runtime

ESTree/Parsing

  • For the static keyword, we use the existing parser
  • For the # character, we use (or enhance) the existing parser (thanks to the work done in #7842 - Class private properties)

Transform

Public

  • Public static fields/methods will be added to the <Class Name> object
  • Accessors will be added using Object.assign

Private

  • Transform to use helper functions similar to private instance fields: classStaticPrivateFieldGet and classStaticPrivateFieldSet
  • Add an in-closure _<static private fields/method> variable outside of the class that will be manipulated by the helper functions
  • Here is a potential output of the two last examples put together, in order to explain how to achieve privacy:
var Account =
/*#__PURE__*/
function () {
  "use strict";

  function Account() {}

  babelHelpers.createClass(Account, [{
    key: "transfer",
    value: function transfer(dollars, targetAccount) {
      babelHelpers.classStaticPrivateFieldGet(Account, _makeTransaction)(dollars, targetAccount);
    }
  }, {
    key: "getDollars",
    value: function getDollars() {
      babelHelpers.classStaticPrivateFieldGet(Account, _transactions).reduce(/* ... */);
    }
  }]);
  
  Object.assign(Account, {
    get nbTransactions() {
      return babelHelpers.classStaticPrivateFieldGet(Account, _transactions).length;
    }
  });

  var _makeTransaction = function(dollars, from, to) {
    babelHelpers.classStaticPrivateFieldSet(Account, _transactions, babelHelpers.classStaticPrivateFieldGet(this, _transactions).concat(/* ... */));
  };

  var _transactions = [];

  return Account;
}();

Note that those implementation details are not final but a way to start the conversation and the actual implementation.

cc @jridgewell, @robpalme, @rpamely, @tim-mc, @mkubilayk

@babel-bot
Copy link
Collaborator

@babel-bot babel-bot commented May 25, 2018

Hey @rricard! We really appreciate you taking the time to report an issue. The collaborators
on this project attempt to help as many people as possible, but we're a limited number of volunteers,
so it's possible this won't be addressed swiftly.

If you need any help, or just have general Babel or JavaScript questions, we have a vibrant Slack
community that typically always has someone willing to help. You can sign-up here
for an invite.

@jridgewell
Copy link
Member

@jridgewell jridgewell commented May 25, 2018

Thanks for the write up!

A few notes:

For the # character, we use (or enhance) the existing parser

This we should already parse everything, including private methods/accessors. We just don't yet have a transformer for them.

Accessors will be added using Object.assign

Babel already has the helpers to assign public static accessors using createClass's third param. In fact, all transforms necessary for public fields/methods/accessors should already be done.

If you can find any non-spec behavior, though, please correct it!

Here is a potential output of the two last examples put together, in order to explain how to achieve privacy:

Your output looks great. I just want to confirm that the static private helpers will be using strict equality on the class (and should be using a const reference to the class). This is a neat optimization we can make for them, avoiding the WeakMaps necessary for instance privates.


For the design of private methods/accessors, I had originally thought we could fall back to the fields behavior without any issues. But this won't account for accessors, which which is difficult to handle correctly. We'd have to store them as some object with get/set keys, and call them appropriately:

const _accessor  = {
  get() {
    // user's getter code
  },
  set(v) {
    // user's setter code
  }
};

_accessor.get.call(ClassName)
@rricard
Copy link
Contributor Author

@rricard rricard commented May 25, 2018

Thank you for the information, that will help us a lot. I'll start fiddling around with an implementation this weekend.

@hzoo
Copy link
Member

@hzoo hzoo commented May 25, 2018

I usually post this in babel/proposals#12 but all good since we are already about implementing at this point!

@rricard
Copy link
Contributor Author

@rricard rricard commented May 29, 2018

So I have a work in progress here: master...rricard:static-class-feature-stage-3
I know what to work on but I still have a few questions:

  • What would be a loose version of the code I'm supposed to generate here? I'm targeting a spec implementation first but do I even need to do the loose one ?
  • I have to admit I'm a bit confused on what's missing on the public static implementation of this whole thing. If someone catched a significant thing that differs from babel's public static and the spec, please tell me because I think I didn't get it.

Here's what next on my roadmap:

  • Helpers to handle this.#<static field> accesses
  • Store static private in an object alongside the class
  • Private static setters/getters
  • Update tests
  • Account for a spec version
  • Refactor
  • Add it to stage 3 preset (could be an independent PR)

cc @robpalme, @jridgewell

@rricard
Copy link
Contributor Author

@rricard rricard commented Jun 20, 2018

I opened #8205 for early feedback. Keep in mind that this is far from finished (see prev. comment) but it gives an idea of the architectural choices made and we can discuss them there before going further. Next goal is to be able to run that against test-262 and make sure it passes for that feature (and backport some missing tests in babel from 262)

@nikolay
Copy link

@nikolay nikolay commented Aug 28, 2018

The semantics of # are quite far from 'private'. I recommend using another character (for example, ~) or better yet - the private modifier. Is many language the convetion is using leading underscores - I don't get why this has to be a different character!

@nicolo-ribaudo
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo commented Aug 28, 2018

You can find many discussions about it at https://github.com/tc39/proposal-class-fields

@franktopel
Copy link

@franktopel franktopel commented Aug 15, 2019

Any updates with this? As of now, using @babel/plugin-proposal-class-properties along with babel-loader for this code:

class A {
  #name="A";
  constructor() {
    console.log(this.#name);
  }
}

gives me

SyntaxError: [...] : Unknown PrivateName "#name"

This is my .babelrc:

{
    "presets": [
        "@babel/preset-env"
    ],
    "plugins": [
        "@babel/plugin-transform-classes",
        "@babel/plugin-proposal-class-properties"
    ]
}

along with this browserslist entry in package.json:

"browserslist": [
  "last 1 version",
  "ie 11"
]

This feature has already been shipped in Chrome 74 if I remember correctly.

@lock lock bot added the outdated label Dec 6, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Dec 6, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

7 participants
You can’t perform that action at this time.