tc39 / ecma262

Status, process, and documents for ECMA-262
https://tc39.es/ecma262/
Other
15.03k stars 1.28k forks source link

How should other specs indicate that they create built-in functions that support [[Construct]]? #2209

Open domenic opened 4 years ago

domenic commented 4 years ago

In https://github.com/heycam/webidl/issues/698, we've come to the conclusion that all web platform classes should have a [[Construct]], even if it just throws a TypeError immediately. However, we're unsure how to express that in spec language.

Currently (in create an interface object step 4) we do

Let F be ! CreateBuiltinFunction(steps, « [[Unforgeables]] », realm, constructorProto).

However this doesn't appear to give us [[Construct]] automatically.

For the ES spec itself, it seems things are done fairly implicitly: https://tc39.es/ecma262/#sec-built-in-function-objects says

The behaviour specified for each built-in function via algorithm steps or other means is the specification of the function body behaviour for both [[Call]] and [[Construct]] invocations of the function. However, [[Construct]] invocation is not supported by all built-in functions.

then later

Built-in function objects that are not identified as constructors do not implement the [[Construct]] internal method unless otherwise specified in the description of a particular function.

which manifests in individual built-in functions via clauses like "the Number constructor" versus "the isFinite function".

For web specifications, how would you suggest specifying our counterpart? Especially given our more-imperative realm creation routine. My current guess is to add a step like

  1. Identify F as a constructor. NOTE: this means it implements the default [[Construct]] internal method for built-in functions.

although this feels a bit informal and wishy-washy. (The fact that we have to explain it with a note, instead of e.g. a hyperlink, is a sign of my discomfort.) Would that be the best approach?

devsnek commented 4 years ago

Within the definition of builtin functions, webidl saying "constructor steps" seems like enough to satisfy ecma262. Confusion beyond that (for the web editors) could probably be handled by a note saying "this creates a function with a [[Construct]] slot because we identify it as such".

jmdyck commented 4 years ago

(Side note: The ES spec defines the operation MakeConstructor, which might seem to be what you want, but it only operates on ordinary functions, and built-in functions aren't required to be ordinary.)

syg commented 4 years ago

Good question, I'll bring this up in the next editor call.

I like the direction @jmdyck is going with. Where the other spec built-ins are ordinary (most of the time, I hope?), MakeConstructor should be used, since the example @domenic gives, a function object F is readily available.

@domenic, is there a concrete need for, or an example of, an exotic built-in constructor that I can look at?

devsnek commented 4 years ago

MakeConstructor is currently only valid on ECMAScript functions, since it implies OrdinaryCallEvaluateBody

syg commented 4 years ago

MakeConstructor is currently only valid on ECMAScript functions, since it implies OrdinaryCallEvaluateBody

Ah yes, good point. Then I think it's most clear to add a path to MakeConstructor (or a new AO largely the same as MakeConstructor) that invokes the function object's [[Call]] than to have prose like "Identify F as a constructor".

domenic commented 4 years ago

Then I think it's most clear to add a path to MakeConstructor (or a new AO largely the same as MakeConstructor) that invokes the function object's [[Call]] than to have prose like "Identify F as a constructor".

+1, that would be very nice and explicit!

@domenic, is there a concrete need for, or an example of, an exotic built-in constructor that I can look at?

I'm not sure what this is exactly getting at. From what I can tell of the current spec, every built-in function object is exotic:

An exotic object is an object that is not an ordinary object.

An ordinary object is an object that satisfies all of the following criteria: [...] If the object has a [[Call]] internal method, it uses the one defined in 9.2.1.

and built-in functions have the [[Call]] from 9.3.1, not 9.2.1.

ExE-Boss commented 4 years ago

and built-in functions have the [[Call]] from 9.3.1, not 9.2.1.

That’s only if they’re not implemented using self‑hosted JS, like what SpiderMonkey does for some of its implementation.

syg commented 4 years ago

I'm not sure what this is exactly getting at. From what I can tell of the current spec, every built-in function object is exotic:

Sorry, the question was worded confusingly. I was wondering if there is an example of a built-in constructor in other specs where the [[Construct]] behavior is not a simple wrapper around calling [[Call]]. That is, are there examples where the [[Call]] behavior is completely separate from [[Construct]] behavior?

But I think your first sentence actually answers my question: "...we've come to the conclusion that all web platform classes should have a [[Construct]], even if it just throws a TypeError immediately".

So MakeConstructor does the simple wrapper-around-[[Call]]-behavior. Is that the intended common use case, or is passing custom steps (like throwing a TypeError) the intended common use case?

domenic commented 4 years ago

Sorry, the question was worded confusingly. I was wondering if there is an example of a built-in constructor in other specs where the [[Construct]] behavior is not a simple wrapper around calling [[Call]]. That is, are there examples where the [[Call]] behavior is completely separate from [[Construct]] behavior?

Ah! In that case, the answer is no: all web platform classes want to use the same delegating-to-[[Call]] behavior. In "non-constructible" cases (e.g. Node), both [[Call]] and [[Construct]] will immediately throw a TypeError.

I can see now that my OP wording was confusing. It's reflecting the history of that https://github.com/heycam/webidl/issues/698 , and not super-helpful as an introduction of the problem for this issue tracker. (Briefly: all Web IDL "non-constructible classes" have always been specified as built-in functions whose function steps are to immediately throw a TypeError. https://github.com/heycam/webidl/issues/698 was debating whether attempting to construct them should throw a TypeError because they have no [[Construct]] at all, or should throw a TypeError because they have a [[Construct]] that delegates to their [[Call]]. This is not observable when doing new Node(), but it is observable when doing an IsConstructor check on Node. We came to the conclusion to prefer the delegation version.)

syg commented 4 years ago

Thanks, that's very helpful!

ljharb commented 4 years ago

@domenic its not clear to me in that issue why to prefer the delegation version; can you elaborate or link me to the relevant comments?

jmdyck commented 4 years ago

I like the direction @jmdyck is going with.

I was actually saying "Don't bother going in that direction, it's a dead end."

Where the other spec built-ins are ordinary (most of the time, I hope?), ...

My guess would be that other specs don't mandate the implementation strategy for built-ins.

Then I think it's most clear to add a path to MakeConstructor (or a new AO largely the same as MakeConstructor) that invokes the function object's [[Call]] than to have prose like "Identify F as a constructor".

Presumably you don't mean that the new AO itself would invoke the function's [[Call]], but rather that it would set the function's [[Construct]] to invoke [[Call]]. However, it's unnecessary (non-conformant) to define a new [[Construct]] -- there are only two valid [[Construct]] methods for a built-in function: 9.2.2 or 9.3.2. So the new AO would boil down to:

1. If _F_ is implemented as an ordinary function, then
  1. Set F.[[Construct]] to the definition specified in 9.2.2.
1. Else,
  1. Set F.[[Construct]] to the definition specified in 9.3.2.

I'm not sure that's worth making into an operation, but I guess it's an option.

syg commented 4 years ago

Presumably you don't mean that the new AO itself would invoke the function's [[Call]], but rather that it would set the function's [[Construct]] to invoke [[Call]].

Yes, that's what I meant.

However, it's unnecessary (non-conformant) to define a new [[Construct]] -- there are only two valid [[Construct]] methods for a built-in function: 9.2.2 or 9.3.2. So the new AO would boil down to:

This is indeed true, but I think "Set F.[[Consntruct]] to the definition specified in 9.3.2" could use editorial improvement. My understanding is that the steps passed to https://tc39.es/ecma262/#sec-createbuiltinfunction are slotted in to step 7 of 9.3.1 via step 5 of 9.3.3. This new AO I'm envisioning would make that explicit, hopefully.

jmdyck commented 3 years ago

Step 7 of 9.3.1 sets the ScriptOrModule of _calleeContext_, which I don't think is relevant. If you mean step 10, then yes, in part...

In CreateBuiltinFunction's step 5, it creates a new built-in function object that when called performs the action described by _steps_. So it's implicit in this creation that the function's [[Call]] is set to either 9.2.1 or 9.3.1:

(And if applicable, all that similarly for [[Construct]].)

This new AO I'm envisioning would make that explicit, hopefully.

(So this new AO would handle [[Call]] as well as [[Construct]]?)

You could easily make the selection between 9.2.1 and 9.3.1 explicit, similar to the pseudocode for choosing between 9.2.2 and 9.3.2 I gave above. But note that @ExE-Boss proposed something like that in PR #2115, and got some pushback from @bakkot.

As for the magic of how _steps_ filters down to 9.2.1 or 9.3.1: well, I guess you could make it (more) explicit, but I'm not sure it would be a net benefit.

domenic commented 3 years ago

What's the latest here? It looks like maybe MakeConstructor can be used for this now?

domenic commented 3 years ago

Hmm, I guess not, since MakeConstructor sets [[Construct]] to 10.2.2, but we'd instead want to set it to 10.3.2 I believe.