tc39 / proposal-bind-operator

This-Binding Syntax for ECMAScript
1.75k stars 30 forks source link

Add accessor for `new` #28

Closed dead-claudia closed 8 years ago

dead-claudia commented 9 years ago

Brought up in passing in #26, but was never actually in the spec (feature more important than syntax).

I believe this works similarly to ::obj.foo, but specifically calls the constructor, as in ::Class[new]. It's unambiguously the constructor, and would fail in any other context.

dead-claudia commented 8 years ago

@zenparsing What do you think? If we change the syntax for this binding, this could be easily changed later.

zenparsing commented 8 years ago

Thanks for the detailed work here. I think it's probably best to leave this out for now (minimal proposals have a better chance of getting committee support), but this is a cool extension of the syntax and it would be nice to have.

I'm not super keen on the ::MyClass[new] syntax, though. I wonder if we can do something like:

MyClass::new;
// More or less:
MyClass::function(...args) { return new this(...args) };

I'm just thinking out loud here, but I think we'd have to restrict the right hand side such that it can't be new MemberExpression or new NewExpression.

In general, I don't like those kind of special case grammar rules, but maybe it's worth it in this case.

dead-claudia commented 8 years ago

I'll agree with that, but it may have to change if we go with option 1 in my comparison, as it conflicts with what would currently be ::obj.new (which is a perfectly valid property, semantically no different than obj.foo or obj.be234f09cjs3knr1). Like I said, it can be changed later. I'll make the appropriate edit now.

dead-claudia commented 8 years ago

@zenparsing You okay with how this looks?

zenparsing commented 8 years ago

@isiahmeadows I'd like to work this in, but in a slightly different way. I'm going to push a change which adds lookahead restrictions to the grammar, and then we can start working from there.

I don't want to create a new type of exotic bound function. Rather, I'd like to create some sort of built-in function object which basically just does:

  1. Let C be the this value.
  2. Assert: IsConstructor(C) is true.
  3. Return Construct(C, arguments).

And which does not have a [[Construct]] internal method.

The runtime semantics for

BindExpression :
    LeftHandSideExpression :: new

Would verify that the left operand IsConstructor, create an instance of that built-in function and then do the BoundFunctionCreate against it like in the other forms. It would set the "length" and "name" properties based on the wrapped constructor though.

What do you think?

dead-claudia commented 8 years ago
  1. So, don't include a [[Construct]] implementation (and by extension, its __proto__)? FWIW normal binding does result in a function that (redundantly) implements [[Construct]] if the target implements it, since it uses BoundFunctionCreate, so would you like me to create a new version to special-case the whole thing?
  2. It does currently set the name and length, using the same logic as BoundFunctionCreate for consistency.
  3. The part of [[BoundFunctionArguments]] is redundant, so I'm pulling that now.
dead-claudia commented 8 years ago
  1. Technically, bound functions do (redundantly) implement [[Construct]] if the backing function does, because they call BoundFunctionCreate. Do you want that behavior to go away as well? Also, would you want me to remove the prototype of both?
  2. Since neither can possibly be bound with arguments, would you prefer I create a common special case for both of them? I can treat [[Call]] and [[Construct]] similarly, to simplify the algorithm (it shouldn't affect anything, and it would make a potential optimization more obvious to implementors).
  3. I'm removing some of the redundancy, such as [[BoundArguments]].
dead-claudia commented 8 years ago

I went ahead and made the constructor version only callable and without a prototype (to make it internally consistent). I do feel that the other should be made similarly.

dead-claudia commented 8 years ago

Also, the left operand is checked to be a constructor, unless I majorly overlooked something. And I disagree that BoundFunctionCreate should be used. It's like using a sledgehammer to pound in a small nail. There's never any arguments, and the ability to call them depends on their type.

It's redundant, and removing that ability allows for a variety of optimizations. It will become almost weightless to bind functions and constructors, but by using BoundFunctionCreate, many of those optimizations are partially obscured and/or limited. Also, checking the type at creation (throwing a TypeError if they aren't right) would lead to better engine optimization of it and more type-safe code.

zenparsing commented 8 years ago

I believe the prototype of a bound constructor should be Function.prototype.

I was trying to minimize the difference between X::new and the other forms by re-using BoundFunctionCreate. I'm not particularly worried about implementation burden, but there is another way it could be done, and which might be more clear to the reader.

We could define a new built-in anonymous function (see http://www.ecma-international.org/ecma-262/6.0/#sec-promise-reject-functions for an example) which would have a [[TargetConstructor]] internal slot, and would just perform Construct against it. The runtime semantics would create an instance of this anonymous function, set the [[TargetConstructor]] internal slot, and then set the "name" and "length" properties like in the other forms.

See http://www.ecma-international.org/ecma-262/6.0/#sec-createresolvingfunctions for an example of creating an anonymous function instance.

dead-claudia commented 8 years ago

If we go that route, there's little benefit to just creating our own closure. Also, [[Construct]] doesn't have the standard properties of a function, because it's not an actual ECMAScript function complete with name and length. I had to special case the constructor.

And when I referred to the prototype, I meant the prototype property, not the own __proto__ property of the function. In this sense, arrow functions don't have a prototype, and always having the Function prototype doesn't make sense in this context.

On Wed, Dec 9, 2015, 09:47 zenparsing notifications@github.com wrote:

I believe the prototype of a bound constructor should be Function.prototype.

I was trying to minimize the difference between X::new and the other forms by re-using BoundFunctionCreate. I'm not particularly worried about implementation burden, but there is another way it could be done, and which might be more clear to the reader.

We could define a new built-in anonymous function (see http://www.ecma-international.org/ecma-262/6.0/#sec-promise-reject-functions for an example) which would have a [[TargetConstructor]] internal slot, and would just perform Construct against it. The runtime semantics would create an instance of this anonymous function, set the [[TargetConstructor]] internal slot, and then set the "name" and "length" properties like in the other forms.

See http://www.ecma-international.org/ecma-262/6.0/#sec-createresolvingfunctions for an example of creating an anonymous function instance.

— Reply to this email directly or view it on GitHub https://github.com/zenparsing/es-function-bind/pull/28#issuecomment-163269403 .

zenparsing commented 8 years ago

it's not an actual ECMAScript function complete with name and length

Right, so you can't actually pull it off and hand it to BoundConstructorCreate or whatever. The name and length properties would come from the actual constructor function object.

Non-constructor functions don't have a "prototype" property. Being an anonymous built-in function takes care of that.

dead-claudia commented 8 years ago

@zenparsing

And as far as I can tell in the ES6 spec, only the user-visible anonymous functions have a length specified. And some aren't visible to ECMAScript code at all, such as MakeArgGetter and MakeArgSetter. To be honest, the way I have [[Call]] defined for bound constructors is much like an anonymous function, anyways, even though I don't actually use the term. Or in JS pseudocode, here's basically the way I have it spec'd:

function %BoundConstructorCreate(targetFunction) {
    const obj = {}
    %SetInternal(obj, "[[Call]]", function (thisArgument, argumentsList) {
        return targetFunction.[[Construct]](argumentsList, target)
    })
    %SetInternal(obj, "[[Extensible]]", true)
    return obj
}

// BindExpression:
//     LeftHandSideExpression[?Yield] :: new
function %createBoundConstructor(target) {
    // Assert: we already have the reference from parsing
    if (!%IsConstructible(target)) {
        throw new TypeError("target must be constructible")
    }

    const F = %BoundConstructorCreate(target)
    let L = 0

    if (%HasOwnProperty(target, "length")) {
        let targetLen = target.length
        if (typeof targetLen === "number") L = %ToInteger(targetLen)
    }

    %DefinePropertyOrThrow(F, "length", PropertyDescriptor({
        value: L,
        writable: false,
        enumerable: false,
        configurable: true,
    }))

    let targetName = target.name
    if (typeof name !== "string") targetName = ""
    %SetFunctionName(F, targetName, "bound")

    return F
}
zenparsing commented 8 years ago

Since these bound factories don't have [[Construct]], there's really no need for an exotic object. And I want to keep this proposal lightweight.

Basically, just replace your call to BoundConstructorCreate with the creation and initialization of the anonymous function.

dead-claudia commented 8 years ago

@zenparsing Okay. I see what you're thinking. Something to the effect of this:

Bound Constructor Wrapper Functions

A bound constructor wrapper function is an anonymous built-in function with a [[BoundConstructorFunction]] internal slot.

When a bound constructor wrapper function F is called with 0 or more args, it performs the following steps:

  1. Assert: F has a [[BoundConstructorFunction]] internal slot.
  2. Let target be the value of F's [[BoundConstructorFunction]] internal slot.
  3. Assert: IsConstrucible(F).
  4. Let args be a List consisting of all the arguments passed to this function.
  5. Let res be target.[[Construct]](args, target)
  6. ReturnIfAbrupt(res).
  7. Return res.
zenparsing commented 8 years ago

Yep, that looks about right. Some little stuff:

zenparsing commented 8 years ago

@isiahmeadows can you rebase? i'll add comments to the diff when you're ready.

dead-claudia commented 8 years ago

Sure...I've had to do it once already.

dead-claudia commented 8 years ago

@zenparsing Done.

zenparsing commented 8 years ago

Left some comments. Thanks for refactoring out the "length" and "name" setting!

Also, in my most recent commit, I've switch to using the ? shorthand, (e.g. "Let status be ? GetValue(...)"). It's a new shorthand for doing the ReturnIfAbrupt thing. I noticed that some of those changes are reverted in your diff. Can we keep those shorthands?

dead-claudia commented 8 years ago

Sure. I'll just make a note of it above, as it isn't obvious at first sight.

zenparsing commented 8 years ago

Thanks for putting up with all of the nitpicks : )

dead-claudia commented 8 years ago

Thanks for putting up with all of the nitpicks : )

@zenparsing No problem

zenparsing commented 8 years ago

Looks good. Now squash these commits into one and I'll pull it in!

dead-claudia commented 8 years ago

@zenparsing Squashed and pushed (pun totally intended :smile:)

zenparsing commented 8 years ago

Merged - thanks!

dead-claudia commented 8 years ago

@zenparsing Welcome!