domenic / promises-unwrapping

The ES6 promises spec, as per September 2013 TC39 meeting
1.23k stars 94 forks source link

Formalize creating functions that perform abstract operations #36

Closed domenic closed 10 years ago

domenic commented 10 years ago

This occurs with some frequency throughout the spec. The simpler cases are currently in the form:

Let resolve(x) be an ECMAScript function that calls Resolve(p, x).

A more complicated case is:

Let resolver be an ECMAScript function that:

  1. Lets resolve be the value resolver is passed as its first argument.
  2. Lets reject be the value resolver is passed as its second argument.

And Promise.all has an even more complicated one.


The ES6 spec does not create functions and pass them to user code very often. When I asked @allenwb about this, he pointed me to the sole example of the strict-mode function.caller poison pill. The machinery invoked is somewhat formidable:

The %ThrowTypeError% object is a unique function object that is defined once for each Realm as follows:

  1. Assert: %FunctionPrototype% for the current Realm has already been initialized.
  2. Let functionPrototype be the intrinsic object %FunctionPrototype%.
  3. Let scope be the Global Environment.
  4. Let formalParameters be the syntactic production: FormalParameters : [empty] .
  5. Let body be the syntactic production: FunctionBody : ThrowTypeError .
  6. Let F be the result of performing FunctionAllocate with argument functionPrototype.
  7. Let %ThrowTypeError% be F.
  8. Perform the abstract operation FunctionInitialise with arguments F, Normal, formalParameters, body, scope, and true.
  9. Call the [[PreventExtensions]] internal method of F.

The ThrowTypeError syntactic production then has to manifest under Function Definitions, with syntax:

Supplemental Syntax

The following productions are used as an aid in specifying the semantics of certain ECMAScript language features. They are not used when parsing ECMAScript source code.

FunctionBody :
    ThrowTypeError
ThrowTypeError :
    [empty]

and runtime semantics under the subheading [Runtime Semantics: EvaluateBody](Runtime Semantics: EvaluateBody):

FunctionBody : ThrowTypeError

  1. Throw a TypeError exception.

I would prefer not to have to invoke all the above machinery, changing all those different places in the spec, if at all possible. I could wrap up most of the steps in the %ThrowTypeError% definition into a reusable function-creator, but the function body specification---the most important part---would be quite difficult.

At this point I'd love some help from @allenwb on how we can abstract this into something reusable but also of sufficient formality. Our existing language is very imprecise, not specifying the function's prototype, normal vs. method vs. arrow status, scope, length, and so on.

allenwb commented 10 years ago

Some of the steps used to create %ThrowTypeError% can be abstracted into a separate abstract operation if we are going to need to have a number of such dynamically created functions. However, we need to look at the specific functions variations chosen for that %ThrowTypeError% and see if they also apply to your use cases. In particular, you need to think about whether these functions should be non-extensible.

Also, I notice that your functions are parametrized via closure capture fairly frequently. We don't currently have a convenient way in the specification to describe built-ins that use such "own" state. (Remember, built-ins are as likely to be implemented in C++ as JS so we can't just assume that all of the JS semantics are available in these algorithms).

I need to do something similar to specify Revocable Proxies. I try to work on it this week and will try to come up with something that also will work for you.

annevk commented 10 years ago

I don't think there's much benefit in making these functions extensible. The only observable effect is invoking them.

allenwb commented 10 years ago

@annevk you have the question backwards. By default, all ES objects are extensible. If you think one needs to be non-extensible you need to justify it.

domenic commented 10 years ago

Note regarding closure capture: it's actually important for one use case that the captured variables be "let-like" as opposed to "var-like". In particular, for Promise.all I have an index variable that is continually updated, but then inside a "Repeat" step I do "Let currentIndex be the current value of index," and then one of my created functions uses currentIndex under the assumption that subsequent loop iterations do not change currentIndex.

allenwb commented 10 years ago

Hmm.. I thought I'd already respond with this, but I can't find it. So here goes again:

I suggest you follow the pattern establish by this spec fragment:

26.2.2.1 Proxy.revocable ( target, handler ) The Proxy.revocable function takes two arguments target and handler, and performs the following: The Proxy.revocable function is used to create a revocable Proxy object When Proxy.revocable is called with arguments target and handler the following steps are taken:

  1. Let p be ProxyCreate(target, handler).
  2. ReturnIfAbrupt(p).
  3. Let revoker be a new built-in function object as defined in 26.2.2.1.1.
  4. Set the [[RevokableProxy]] internal slot of revoker to p.
  5. Let result be the result of ObjectCreate().
  6. CreateOwnDataProperty(result, "proxy", p).
  7. CreateOwnDataProperty(result, "revoke", revoker).
  8. Return result.

26.2.2.1.1 Proxy Revocation Functions A Proxy revocation function is an anonymous function that has the ability to invalidate a specific Proxy object. Each Proxy revocation function has a [[RevokableProxy]] internal slot. When a Proxy revocation function, F, is called the following steps are taken:

  1. Let p be the value of F’s [[RevokableProxy]] internal slot.
  2. If p is undefined, then return undefined.
  3. Set the value of F’s [[RevokableProxy]] internal slot to undefined.
  4. Assert: p is a Proxy object.
  5. Set the [[ProxyTarget]] internal slot of p to undefined.
  6. Set the [[ProxyHandler]] internal slot of p to undefined.
  7. Return undefined.

Note that step 3 of the first function creates an instance of the the function described in the sub section. The per instance captured state is held in an internal data property (now called internal slots, if you want to make that change) and which is set in step 4. If you instantiate such a function in multiple places it might be worth it to create an abstract operation to do steps 3 and 4, but in this case it wasn't worth it.

domenic commented 10 years ago

Oh awesome, thanks Allen. That looks quite pleasant. Only a couple questions:

allenwb commented 10 years ago

They are built-in functions so all the rules in http://people.mozilla.org/~jorendorff/es6-draft.html#sec-17 apply to them.

Just describe the arguments like you would for any other built-in. For example: When an anonymous resolver function is called with arguments resolve and the following steps are taken: