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

Partial application plugin #9474

Merged
merged 16 commits into from Mar 13, 2019
Merged

Conversation

@byara
Copy link
Contributor

byara commented Feb 8, 2019

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

This PR addresses the proposal regarding Partial Application plugin.

The syntax rules that was suggested in the proposal to be covered:

    • Given f(?), the expression f is evaluated immediately.
    • Given f(?, x), the non-placeholder argument x is evaluated immediately and fixed in its position.
    • Given f(?), excess arguments supplied to the partially applied function result are ignored.
    • Given f(?, ?) the partially applied function result will have a parameter for each placeholder token that is supplied in that token's position in the argument list.
    • Given f(this, ?), the this in the argument list is the lexical this.
    • Given f(?), the this receiver of the function f is fixed as undefined in the partially applied function result.
    • Given f(?), the length of the partially applied function result is equal to the number of ? placeholder tokens in the argument list.
    • Given f(?), the name of the partially applied function result is f.name.
    • Given o.f(?), the references to o and o.f are evaluated immediately.
    • Given o.f(?), the this receiver of the function o.f is fixed as o in the partially applied function result.
    • Given f(g(?)), the result is equivalent to f(_0 => g(_0)) not _0 => f(g(_0)). This is because the ? is directly part of the argument list of g and not the argument list of f.
  • I'm not 100% sure about 5, 6, 7 and 8 and how to address them.
  • I welcome any suggestions and reviews to improve this, I'm doing this as part of my thesis and anything that could teach me something is great.
@babel-bot

This comment has been minimized.

Copy link
Collaborator

babel-bot commented Feb 8, 2019

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

@nicolo-ribaudo

This comment has been minimized.

Copy link
Member

nicolo-ribaudo commented Feb 8, 2019

(I'm marking this as Low priority until the parser PR is merged - don't worry! 😉 )

t.variableDeclaration("const", [
t.variableDeclarator(
receiverLVal,
t.identifier(receiverRVal(node)),

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 8, 2019

Member

You can use path.scope.push to generate those vars and avoid the iife:

const g = o.f(?, x, 1);

// becomes

var _receiver, _func, _param, _param2; // generated by scope.push
const g = (_receiver = o, _func = o.f, _param = x, _param2 = 1, _arg => _func.call(_receiver, _arg, _param, _param2);

Btw, there is probably some scope/types helper function that says that 1 is a pure constant value and can thus can be transpiled as

const g = (_receiver = o, _func = o.f, _param = x, _arg => _func.call(_receiver, _arg, _param, 1);

This comment has been minimized.

Copy link
@byara

byara Feb 13, 2019

Author Contributor

I would like to ask for a bit of clarification:

  • what is the advantage of not using the iife?
  • could you explain how pushing to scope creates the declarations for me?
Copy link

danielcaldas left a comment

Just a left a few minor comments that might make some parts of the implementation more compact. Great job man! 👍

}

/**
* a recursive function that unfolds MemberExpressions within MemberExpression

This comment has been minimized.

Copy link
@danielcaldas

danielcaldas Feb 9, 2019

Maybe a rewording to a recursive function that unfolds nested MemberExpressions

return true;
}
}
return false;

This comment has been minimized.

Copy link
@danielcaldas

danielcaldas Feb 9, 2019

Maybe refactor to something like:

function hasArgumentPlaceholder(node) {
  return node.arguments.some(t.isArgumentPlaceholder)
}

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 9, 2019

Member

is* functions can accept two arguments, so you'll need to wrap it in an arrow to only use one parameter (or use partial application lol)

function receiverRVal(node) {
let rVal = unfold(node).split(".");
rVal.pop();
rVal = rVal.join(".");

This comment has been minimized.

Copy link
@danielcaldas

danielcaldas Feb 9, 2019

We can return immediately rVal.join(".");

* @returns {Array<Expression>}
*/
function unwrapArguments(node) {
const nonPlaceholder = node.arguments.filter(

This comment has been minimized.

Copy link
@danielcaldas

danielcaldas Feb 9, 2019

We can also return immediately here.

*/
function unwrapAllArguments(node, scope) {
const clone = t.cloneNode(node);
clone.arguments.forEach(argument => {

This comment has been minimized.

Copy link
@danielcaldas

danielcaldas Feb 9, 2019

Maybe refactor into a .map

*/
function argsToVarDeclarator(inits, scope) {
let declarator = [];
declarator = inits.map(expr =>

This comment has been minimized.

Copy link
@danielcaldas

danielcaldas Feb 9, 2019

We could also return immediately here return inits.map...

* @param {Array<Arguments>} args
*/
function mapNonPlaceholderToLVal(nonPlaceholderDecl, allArgsList) {
const clone = Array.from(allArgsList);

This comment has been minimized.

Copy link
@danielcaldas

danielcaldas Feb 9, 2019

Array.from(allArgsList)
  .map(cl => ...);
@byara

This comment has been minimized.

Copy link
Contributor Author

byara commented Feb 13, 2019

@nicolo-ribaudo @danielcaldas thank you for all the reviews, I'll try to address them as soon as possible.

Meanwhile, I have a question:
Regarding this syntactic rule of the proposal:

  1. Given f(?), the name of the partially applied function result is f.name.

What do you think about this solution:

// if we have this:
const foo = bar(1, ?, 3, ?);

// currently converted to this:
const foo = (() => {
  const _func = bar;
  const _param = 1,
        _param2 = 3;
  return (_argPlaceholder, _argPlaceholder2) => _func(_param, _argPlaceholder, _param2, _argPlaceholder2);
})();

bar.name; // => bar
foo.name; // => empty

Instead we do this:

const foo = (() => {
  const _func = bar;
  const _param = 1,
        _param2 = 3;
{
  const bar = (_argPlaceholder, _argPlaceholder2) => _func(_param, _argPlaceholder, _param2, _argPlaceholder2);
  return bar;
}
})();

bar.name; // => bar
foo.name; // => bar

or maybe we can get the foo.name to be foo?

@nicolo-ribaudo

This comment has been minimized.

Copy link
Member

nicolo-ribaudo commented Feb 13, 2019

Can we use normal functions?

const add1 = add(1, ?);

// ->

var _func, _param;
const add1 = (_func = add, _param = 1, function add(_placeholder) { return _func(_param, _placeholder) });
@byara

This comment has been minimized.

Copy link
Contributor Author

byara commented Feb 13, 2019

Can we use normal functions?

const add1 = add(1, ?);

// ->

var _func, _param;
const add1 = (_func = add, _param = 1, function add(_placeholder) { return _func(_param, _placeholder) });

It works.
That's a good idea and it looks better. I'll change the plugin accordingly.

t.variableDeclaration("const", [
t.variableDeclarator(
receiverLVal,
t.identifier(receiverRVal(node)),

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 14, 2019

Member

I don't understand what receiverRVal (which, btw, doesn't generate a valid identifier since the result can contain .) is needed for: isn't using node.callee (and node.callee.property for functionLVal) enough?

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 14, 2019

Member

Also, it doesn't work:

a.b.fn().c["d"].e(?);

// ->

(() => {
  const _receiver = a.b.fn.c.undefined;
  const _func = a.b.fn.c.undefined.e;
  return _argPlaceholder => _func.call(_receiver, _argPlaceholder);
})();

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 14, 2019

Member

Oh, and instead of using newFuncLVal/newReceiverLVal you could use path.scope.generateUidIdentifierBasedOnNode(node.callee) and path.scope.generateUidIdentifierBasedOnNode(node.callee.object). It's up to you to decide.

object.get().fn(?);

// ->

(() => {
  const _object$get = object.get;
  const _object$get$fn = object.get.fn;
  return _argPlaceholder => _object$get$fn.call(_object$get, _argPlaceholder);
})();

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 14, 2019

Member

Oh, and the previous example should be something like this, otuherwise we might execute some accessors twice:

(() => {
  const _object$get = object.get;
  const _object$get$fn = _object$get.fn;
  return _argPlaceholder => _object$get$fn.call(_object$get, _argPlaceholder);
})();
const h = (_p = p, _p$b = p.b, _param2 = y, _param3 = x, function b(_argPlaceholder2) {
return _p$b.call(_p, 1, _param2, _param3, 2, _argPlaceholder2);
});
const j = (_a$b$c$d$e = a.b.c.d.e, _a$b$c$d$e$foo = a.b.c.d.e.foo, function foo(_argPlaceholder3) {

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 15, 2019

Member

This should be

const j = (_a$b$c$d$e = a.b.c.d.e, _a$b$c$d$e$foo = _a$b$c$d$e.foo, ...

because a.b.c.d.e could be getters which must be accessed exactly once.

* @param {Array<Node>} args
*/
function mapNonPlaceholderToLVal(nonPlaceholderArgs, allArgsList) {
const clonedArgs = Array.from(allArgsList);

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 15, 2019

Member

Why do you need to clone this array? You are only cloning the array, not its elements.

This comment has been minimized.

Copy link
@byara

byara Feb 16, 2019

Author Contributor

you are correct, this is useless. I removed it.

*/
function mapNonPlaceholderToLVal(nonPlaceholderArgs, allArgsList) {
const clonedArgs = Array.from(allArgsList);
clonedArgs.map(arg => {

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 15, 2019

Member

Nit: you can use forEach

let cloneList = [];
allArgsList.forEach(item => {
if (item.name && item.name.includes("_argPlaceholder")) {
cloneList = cloneList.concat(item);

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 15, 2019

Member

cloneList.push(item)

node.callee,
);
const receiverLVal = path.scope.generateUidIdentifierBasedOnNode(
node.callee.object,

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 15, 2019

Member

This should only be generated when node.callee is a member expression.

@nicolo-ribaudo

This comment has been minimized.

Copy link
Member

nicolo-ribaudo commented Feb 15, 2019

Does ...spread work?

foo(a, ...b, ?);

Since the spec doesn't define how it should behave (or I can't find it), we can just throw an error when we find it. Or maybe it uses one of the following semantics? (cc @rbuckton)

var _foo, _a, _b;
_foo = foo, _a = a, _b = b, function foo(_arg) { return _foo(_a, ..._b, _arg) }

// or maybe it calls [Symbol.iterator] earlier

_foo = foo, _a = a, _b = [...b], function foo(_arg) { return _foo(_a, ..._b, _arg) }
@rbuckton

This comment has been minimized.

Copy link

rbuckton commented Feb 15, 2019

@nicolo-ribaudo: Anything usable in a normal function call should be usable here. All evaluation should be eager, so the second example is correct.

@nicolo-ribaudo

This comment has been minimized.

Copy link
Member

nicolo-ribaudo commented Feb 15, 2019

(From the original post)

  • I'm not 100% sure about 5, 6, 7 and 8 and how to address them.

Don't they already work?

@byara

This comment has been minimized.

Copy link
Contributor Author

byara commented Feb 16, 2019

(From the original post)

  • I'm not 100% sure about 5, 6, 7 and 8 and how to address them.

Don't they already work?

I added a few tests for 7 and 8. Regarding 5, I think, I sort of understand what "lexical this" means.
For 6, I don't think I understand the "this receiver" for f(?) 😅
For other reviews, I'll address them ASAP.

@rbuckton

This comment has been minimized.

Copy link

rbuckton commented Feb 16, 2019

Ignoring partial application for a second, when you call f() in strict mode, the receiver (the value used for this inside of f) is undefined. When you call o.f(), the receiver is o. When you do f.call(o), the receiver is o.

6 basically means that when you do g = o.f(?); g(), the value o is preserved as the receiver.

@byara

This comment has been minimized.

Copy link
Contributor Author

byara commented Feb 16, 2019

Ignoring partial application for a second, when you call f() in strict mode, the receiver (the value used for this inside of f) is undefined. When you call o.f(), the receiver is o. When you do f.call(o), the receiver is o.

6 basically means that when you do g = o.f(?); g(), the value o is preserved as the receiver.

@rbuckton Thank you for the explanation. I think 6 is also addressed.

@nicolo-ribaudo nicolo-ribaudo added this to the v7.4.0 milestone Feb 18, 2019
remove unnecessary error message and hasPartial function from parseNewArguments

add types for PartialExpression

Update the tests

rename PartialExpression to Partial

move Partial from expressions to types and rename to ArgumentPlaceholder

add tests for ArgumentPlaceholder in babel-generator

rename Partial to ArgumentPlaceholder

update the tests

remove alias from the type and undo changes in generated folder

adds a nice error message

better definition for the type

auto-generated files

update the conditional for allowPlaceholder message and tests

update CallExpression definition to accept ArgumentPlaceholder

change description

clean up

indent ArgumentPlaceholder entry and revert unwanted changes
@byara byara force-pushed the byara:partial-application-plugin branch 3 times, most recently from e4b72a9 to 9196d67 Feb 20, 2019
@byara

This comment has been minimized.

Copy link
Contributor Author

byara commented Feb 20, 2019

@nicolo-ribaudo one of the builds for travis times out. Would you retrigger it?

@nicolo-ribaudo

This comment has been minimized.

Copy link
Member

nicolo-ribaudo commented Feb 20, 2019

It seems that after the rebase the plugin disappeared? If you need it, you can restore from https://github.com/nicolo-ribaudo/babel/tree/pr/byara/9474-backup (it is 6 days old so commits after #9474 (review) are not included). If you prefer, I suggest using git reflog to restore the lost commits.

@byara byara force-pushed the byara:partial-application-plugin branch from 9196d67 to bb7068a Feb 20, 2019
Behrang Yarahmadi
if (t.isArgumentPlaceholder(arg)) {
const id = scope.generateUid("_argPlaceholder");
placeholders.push(t.identifier(id));
args.push(t.cloneNode(t.identifier(id)));

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 21, 2019

Member

No need to cloneNode here, t.identifier returns a new node.

@nicolo-ribaudo

This comment has been minimized.

Copy link
Member

nicolo-ribaudo commented Feb 21, 2019

The failing tests are fixed by #9558

{
"name": "@babel/plugin-proposal-partial-application",
"version": "7.2.0",
"description": "Transform pipeline operator into call expressions",

This comment has been minimized.

Copy link
@nicolo-ribaudo

nicolo-ribaudo Feb 21, 2019

Member

This needs to be updated to match the readme.

Copy link
Member

nicolo-ribaudo left a comment

🎉

@nicolo-ribaudo

This comment has been minimized.

Copy link
Member

nicolo-ribaudo commented Mar 4, 2019

Could you rebase (or merge master) this PR, since the parser PR has been merged?

Copy link
Member

danez left a comment

Nice work.

Does this work with awaiting async functions? Can we add a test for this? Thinking about it more, I guess that the partial application does not really touch with async functions as await foo(?) is nonsense. await foo(?)(1) could work, but doesn't make much sense. So not sure if a test makes sense.

Also it would be nice to add a test where the result from the call is not assigned but directly chained: foo(?, a)(b).then(()=>{}); Doesn't make much sense either, but I guess it should work.

@@ -0,0 +1,3 @@
{
"plugins": ["proposal-partial-application"]
}

This comment has been minimized.

Copy link
@danez

danez Mar 5, 2019

Member

nit: The options.json files are the same for all tests as I can see, so the config file could be moved on level up into the general folder. Then all the fixtures in general would share the config.

This comment has been minimized.

Copy link
@byara

byara Mar 6, 2019

Author Contributor

@danez Thanks for the feedback.
I moved options.json one level up and added a new test.

compare(a, b) {
if (a > b) {
return a;
};

This comment has been minimized.

Copy link
@existentialism

existentialism Mar 13, 2019

Member

Nit, but this semi seems unnecessary?

This comment has been minimized.

Copy link
@byara

byara Mar 13, 2019

Author Contributor

yep, unnecessary, I'll remove it.

false,
),
]);
path.replaceWith(finalExpression);

This comment has been minimized.

Copy link
@existentialism

existentialism Mar 13, 2019

Member

Not big deal by any means, but since both of these end with replacing the path with a sequenceExpression, we could do:

const sequenceParts = [];

if (node.callee.type === "MemberExpression") {
  sequenceParts.push(
    /* stuff */
  );
} else {
  sequenceParts.push(
    /* stuff */
  );
}

path.replaceWith(t.sequenceExpression(sequenceParts));

This comment has been minimized.

Copy link
@byara

byara Mar 13, 2019

Author Contributor

good idea! made the necessary changes.

Copy link
Member

existentialism left a comment

Benefit of coming late to the party is the code looks awesome already :)

Nice work!

@nicolo-ribaudo

This comment has been minimized.

Copy link
Member

nicolo-ribaudo commented Mar 13, 2019

Awesome work!

@nicolo-ribaudo nicolo-ribaudo merged commit 29cd27b into babel:master Mar 13, 2019
3 checks passed
3 checks passed
babel/repl REPL preview is available
Details
ci/circleci Your tests passed on CircleCI!
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
mAAdhaTTah added a commit to mAAdhaTTah/babel that referenced this pull request Mar 15, 2019
* master: (58 commits)
  Remove dependency on home-or-tmp package (babel#9678)
  [proposal-object-rest-spread] fix templateLiteral in extractNormalizedKeys (babel#9628)
  Partial application plugin (babel#9474)
  Private Static Class Methods (Stage 3) (babel#9446)
  gulp-uglify@3.0.2
  rollup@1.6.0
  eslint@5.15.1
  jest@24.5.0
  regexpu-core@4.5.4
  Remove input and length from state (babel#9646)
  Switch from rollup-stream to rollup and update deps (babel#9640)
  System modules - Hoist classes like other variables (babel#9639)
  fix: Don't transpile ES2018 symbol properties (babel#9650)
  Add WarningsToErrorsPlugin to webpack to avoid missing build problems on CI (babel#9647)
  Update regexpu-core dependency (babel#9642)
  Add placeholders support to @babel/types and @babel/generator (babel#9542)
  Generate plugins file
  Make babel-standalone an ESModule and enable flow (babel#9025)
  Reorganize token types and use a map for them (babel#9645)
  [TS] Allow context type annotation on getters/setters (babel#9641)
  ...
@rbuckton rbuckton mentioned this pull request Apr 2, 2019
@lock lock bot added the outdated label Oct 4, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Oct 4, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
You can’t perform that action at this time.