Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Normative: Fix extending null#1321

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
devsnek wants to merge1 commit intotc39:main
base:main
Choose a base branch
Loading
fromdevsnek:normative/extending-null

Conversation

devsnek
Copy link
Member

@devsnekdevsnek commentedOct 9, 2018
edited
Loading

Right now if you extend null it isn't a problem until [[Construct]] which would expectderived to already havethis bound which isn't going to be the case because there is never a super call. We can just bypass this directly by keeping the ConstructorKind set to base.

This PR also allowssuper() and keepssuper() === this.

Fixes#1036

kyranet, ExE-Boss, and ShinMini reacted with thumbs up emojiExE-Boss reacted with rocket emoji
@ljharb
Copy link
Member

See#781 and#543

@ljharbljharb added normative changeAffects behavior required to correctly evaluate some ECMAScript source text needs consensusThis needs committee consensus before it can be eligible to be merged. labelsOct 9, 2018
@devsnek
Copy link
MemberAuthor

after reading through all this history, it seems the solution needs to additionally include making super() a no-op?

@devsnekdevsnekforce-pushed thenormative/extending-null branch 3 times, most recently from862d902 tod941b5cCompareOctober 9, 2018 16:37
@devsnekdevsnek changed the titleNormative: Don't set ConstructorKind to derived if the class heritage is null.Normative: Fix extending nullOct 9, 2018
@littledan
Copy link
Member

Maybe, though I can imagine arguments against that as well... I am not sure what solution would be acceptable to all objections raised.

@devsnek
Copy link
MemberAuthor

personally i would expectsuper() in anextends null to throw, but from what i read from the other issues and meeting notes, people wanted it to not throw because its usually enforced with syntax.

@devsnekdevsnekforce-pushed thenormative/extending-null branch 2 times, most recently from6fc9a49 to2e6764bCompareOctober 31, 2018 03:47
@devsnekdevsnekforce-pushed thenormative/extending-null branch from2e6764b to6477326CompareNovember 1, 2018 22:15
@bakkot
Copy link
Contributor

bakkot commentedNov 15, 2018
edited
Loading

See also#699 and#755 and thenotes.

To evaluate this, it would be helpful to describe what happens in a variety of cases. For example:

  • new class extends null {}
  • new class extends null { constructor() { } }
  • new class extends null { constructor() { super(); } }
  • class Base {}; class Derived extends Base {}; Object.setPrototypeOf(Derived, null); new Derived;
  • class Base {}; class Derived extends Base { constructor() { } }; Object.setPrototypeOf(Derived, null); new Derived;
  • class Base {}; class Derived extends Base { constructor() { super(); } }; Object.setPrototypeOf(Derived, null); new Derived;
  • class Base {}; class Derived extends null {}; Object.setPrototypeOf(Derived, Base); new Derived;
  • class Base {}; class Derived extends null { constructor() { } }; Object.setPrototypeOf(Derived, Base); new Derived;
  • class Base {}; class Derived extends null { constructor() { super(); } }; Object.setPrototypeOf(Derived, Base); new Derived;
  • function Base() { this.x = 1; } Base.prototype = null; new class extends Base {}
  • function Base() { this.x = 1; } Base.prototype = null; class Derived extends null {}; Object.setPrototypeOf(Derived, Base); new Derived;

@devsnek
Copy link
MemberAuthor

devsnek commentedNov 15, 2018
edited
Loading

@bakkot thanks for putting that together.

all of these except case 10 are passing, which i'm looking into now.

print('1');{constx=newclassextendsnull{}();if(x.hasOwnProperty){thrownewError('1 should not inherit');}}print('2');{constx=newclassextendsnull{constructor(){}}();if(x.hasOwnProperty){thrownewError('2 should not inherit');}}print('3');{constx=newclassextendsnull{constructor(){super();}}();if(x.hasOwnProperty){thrownewError('3 should not inherit');}}print('4');{try{classBase{}classDerivedextendsBase{}Object.setPrototypeOf(Derived,null);newDerived();thrownewError('4 should have failed at super() in the default derived constructor');}catch{}}print('5');{try{classBase{}classDerivedextendsBase{constructor(){}}Object.setPrototypeOf(Derived,null);newDerived();thrownewError('5 should have failed at thisER.GetThisBinding()');}catch{}}print('6');{try{classBase{}classDerivedextendsBase{constructor(){super();}}Object.setPrototypeOf(Derived,null);newDerived();thrownewError('6 should fail, null is not a constructor');}catch{}}print('7');{classBase{}classDerivedextendsnull{}Object.setPrototypeOf(Derived,Base);constx=newDerived();if(xinstanceofBase){thrownewError('7 should have base constructor kind, locked prototype');}}print('8');{classBase{}classDerivedextendsnull{constructor(){}}Object.setPrototypeOf(Derived,Base);constx=newDerived();if(xinstanceofBase){thrownewError('8 should have base constructor kind, locked prototype');}}print('9');{classBase{}classDerivedextendsnull{constructor(){super();}}Object.setPrototypeOf(Derived,Base);constx=newDerived();if(xinstanceofBase){thrownewError('9 should have base constructor kind, locked prototype');}}print('10');{functionBase(){this.x=1;}Base.prototype=null;classDerivedextendsBase{};constx=newDerived();if(x.x!==1){thrownewError('10 should work after spec change');}}print('11');{functionBase(){this.x=1;}Base.prototype=null;classDerivedextendsnull{}Object.setPrototypeOf(Derived,Base);constx=newDerived();if(Object.getPrototypeOf(Object.getPrototypeOf(x))===Base||x.x===1){thrownewError('11 should have base constructor kind, locked prototype');}}

@bakkot
Copy link
Contributor

I expect the reason#10 is not behaving as you expect is because this patch usesprotoParent instead ofsuperclass. See#755.

@devsnekdevsnekforce-pushed thenormative/extending-null branch from6477326 to1d04de0CompareNovember 15, 2018 23:51
@devsnek
Copy link
MemberAuthor

alright case 10 is passing 👌

@bakkot
Copy link
Contributor

@devsnek

If _protoParent_ is *null* and _constructorParent_ is %FunctionPrototype%, let _trueNullParent_ be *true*, otherwise let _trueNullParent_ be *false*.
If |ClassHeritage_opt| is present and _trueNullParent_ is *false*, set _F_.[[ConstructorKind]] to `"derived".

This has the effect that

Function.prototype.prototype=null;(classextendsFunction.prototype{})

will result in a class which is considered a"base" class, which is probably not desirable. Why not just switch onsuperclass?

@devsnek
Copy link
MemberAuthor

oh crap i was confusing Function.prototype and Function.prototype.prototype 😆 thanks for the pointer

8eecf0d2 and ExE-Boss reacted with laugh emoji

@devsnekdevsnekforce-pushed thenormative/extending-null branch from1d04de0 to47a65e4CompareNovember 16, 2018 00:04
@bakkot
Copy link
Contributor

bakkot commentedNov 16, 2018
edited
Loading

So, given all the above, let me check if my understanding is correct / try to summarize:

Whether a class is considered"base" or"derived" is fixed at the time it is defined."base" classes are those which lack aClassHeritage and those whoseClassHeritage evaluates to the valuenull; all others are"derived".

There are roughly four distinct kinds of classes:

  1. classes which lack aClassHeritage (these are"base")
  2. classes which have aClassHeritage which evaluates to a non-null value and whose prototype has not been changed tonull (these are"derived")
  3. classes which haveClassHeritage which evaluates tonull, regardless of whether the class's prototype has been changed (these are"base")
  4. classes which have a ClassHeritage which evaluates to a non-null value and whose prototype has been changed tonull (these are"derived")

1 and 2 are not changed, but I'll summarize anyway.

For 1, theconstructor is syntactically prevented from callingsuper(). The default constructor does not call it. This is true even if the class's prototype is changed to a non-null value (which is more relevant than you might at first think, since in 2super() may be called inside ofeval). The class is always instantiable and (assuming no return-override) always results in a new object whose prototype is the class's.prototype property. Changing the class's prototype has no effects on instances.

For 2, theconstructor is required to callsuper. The default constructor callssuper(...args). If the class's prototype is changed to some other constructor, thesuper call will invoke that constructor and result in the return value, but will (assuming no return-override) still result in an object whose prototype is the original class's.prototype property. Changing the class's prototype (to some other constructor) has the effect of changing whatsuper() returns and therefore what the constructor returns.

For 3, theconstructor may or may not callsuper at its discretion, and may call it multiple times. The default constructor callssuper(..args), which is observable because it performs an observable access toArray.prototype[Symbol.iterator]. Callingsuper() has no effects even if the class's prototype has been changed to some constructor, but its arguments are evaluated and the call returnsthis.this is bound for the lifetime of the class's constructor, even before the first call tosuper(). The class is always instantiable and (assuming no return-override) always results in a new object whose prototype is the class's.prototype property. Changing the class's prototype has no effects on instances.

For 4, the class is not instantiable (assuming no return-override):super() cannot be called in the constructor (because thesuper constructor is not a constructor), butthis is not bound and, becausesuper() cannot be called, cannot be bound. The default constructor callssuper(...args) and hence throws,without an observable access toArray.prototype[Symbol.iterator] (cf#1351).

Do I have that right?

@devsnek
Copy link
MemberAuthor

devsnek commentedNov 16, 2018
edited
Loading

@bakkot looks good

@bakkot
Copy link
Contributor

bakkot commentedNov 16, 2018
edited
Loading

Great. I think 3 is pretty weird - is there a reason to make the behavior ofsuper() in that case "do nothing" rather than "throw a TypeError"? Given the other cases, that makes the most sense to me. (There might well be a good reason; I don't have the whole history here paged in.)

Edit: throwingsuper() requires some shenanigans to make the default constructor not throw, I suppose, which might be enough reason to avoid it on its own.

Edit2: actually, it's pretty easy to avoid the above problem, because the decision about what to make the default constructor happens after evaluating the heritage. Step 10a inClassDefinitionEvaluation could just be "If ClassHeritage_opt is present _andsuperclass is notnull, then" and then the default constructor for 3 would not attempt to invokesuper().

@devsnek
Copy link
MemberAuthor

@bakkot the reason to allowsuper() was to keep the distinction that syntax controls when super is allowed. there was also some other stuff about like class factories or something, but i don't remember which issue it was brought up in.

@bakkot
Copy link
Contributor

"Allowed" is a funny term. Making it legal but throwing is still "allowed" in some sense. I think it's less confusing to have it legal syntactically but forbidden at runtime rather than also legal at runtime but not invoke the class's constructor, given the behavior in 2.

ExE-Boss reacted with thumbs up emoji

@devsnek
Copy link
MemberAuthor

would it be breaking to remove the early error forbidding super in classes without heritage? we could remove the special case in SuperCall fornull, prototypes of base classes being set wouldn't be ignored, etc.

@bakkot
Copy link
Contributor

would it be breaking to remove the early error forbidding super in classes without heritage?

No, but it would make me sad.

ExE-Boss reacted with thumbs up emoji

@moztcampbell
Copy link

WhenClassBody_opt is not present, this proposed spec text does not appear to definesuperclass, yet in the synthesized default constructor we close-over and compare this uninitialized value tonull. Issuperclass expected to be the abstractempty and distinct fromnull?

@mhofman
Copy link
Member

I am really concerned about allowing the dynamic extend scenario as that would be a breaking change, and IMO making a staticextends null allowable without allowingextends thingThatEvaluatesToNull is not right. At this point I believe that if we want to explicitly allow bare base classes that do not inherit fromObject.prototype, it should be done through a different syntax.

@erights suggestedextends void for this case, which was also suggested in#1036 (comment)

class ExNihilo extends void could be transpiled into the following approximation*:

functionNull(){returnObject.create(new.target.prototype);}Null.prototype=null;classExNihiloextendsNull{}

Asuper usage inextends void classes would be a syntax error, the same as it's not allowed in other base classes.

class extends thingThatEvaluatesToUndefined would continue to not be allowed.

[*] It would only be an approximation since the transpilation would result inObject.getPrototypeOf(ExNihilo) === Null instead ofFunction.prototype.

@devsnek
Copy link
MemberAuthor

can you expand on howextends thingThatEvaluatesToNull is causing breakage for you?

@ljharb
Copy link
Member

How is havingextends void but notextends (null) any different than havingextends null and notextends (null)?

No matter what we do here, we shouldn't distinguishnull from something that evaluates to null.

@bakkot
Copy link
Contributor

How is havingextends void but notextends (null) any different than havingextends null and notextends (null)?

void is not an expression and so doesn't evaluate to anything.

mhofman reacted with thumbs up emoji

@mhofman
Copy link
Member

I think I misunderstood@bathos's example.

Regardless I am still uncomfortable changing the behavior ofextends null to dynamically allow the construction of bare instances.

A class that doesn't want to inherit fromObject.prototype should be considered a base class, and syntactically disallow anysuper usage.

@bathos
Copy link
Contributor

bathos commentedOct 23, 2021
edited
Loading

Regardless I am still uncomfortable changing the behavior of extends null to dynamically allow the construction of bare instances.

This is how it works currently, unless I'm misunderstanding? The last example I gave is of code that has worked for the last six years. The changes proposed here would make that currently-valid code throw rather than enable it to work.

@mhofman
Copy link
Member

mhofman commentedOct 23, 2021
edited
Loading

How is havingextends void but notextends (null) any different than havingextends null and notextends (null)?

No matter what we do here, we shouldn't distinguishnull from something that evaluates to null.

"having" seem a little unclear.

extends void is currently a syntax error. I propose we allow it. Howeverextends undefined would stay a runtime class declaration error.

extends null andextends (null) both are currently construction-time errors (unless the prototypes are explicitly overridden later, or the constructor leverages the return override, i.e the few in the wild use cases mentioned above). I agree we should not treat them differently and argue that at this point we should keep the current default behavior of throwing at construction time.

@bathos
Copy link
Contributor

bathos commentedOct 23, 2021
edited
Loading

extends null and extends (null) both are currently construction-time errors (unless the prototypes are explicitly overridden later in the few in the wild use cases mentioned above).

Just for the sake of making sure this is well-understood, what is a construction time error currently issuper() where no super constructor exists. Either setting the super constructor after class declarationor return override are currently valid ways to employextends null. The former would stop working entirely with this change. The latter would continue working but with subtly different behavior.

AFAICT, if super() were changed to behave as Object.create(new.target.prototype) when the prototype of the function is null,extends null would then work more broadlywithout breaking any existing code. That is not what's being proposed. I'm guessing it must have been discussed at some point but I couldn't find it so am not sure why it was not the path taken.

mhofman and nicolo-ribaudo reacted with thumbs up emoji

@ljharb
Copy link
Member

@bakkot yes but conceptually they’re the same thing.

@ljharb
Copy link
Member

@mhofman all base classes per the current spec inherit from Object.prototype, so im not sure why that would be a requirement.

@mhofman
Copy link
Member

so im not sure why that would be a requirement.

What would be a requirement? Sorry I lost track.

From what I understand, a use case is to enable bare classes that don't inherit fromObject.prototype.extends void does that in an explicit way. Only a base class can declare it should be bare, and I argue this should be expressed syntactically.

@ljharb
Copy link
Member

A class can extend any expression, and i think that should be preserved, even if the expression evaluates to null.

bathos reacted with thumbs up emoji

@devsnek
Copy link
MemberAuthor

@mhofman

extends null and extends (null) both are currently construction-time errors (unless the prototypes are explicitly overridden later, or the constructor leverages the return override, i.e the few in the wild use cases mentioned above). I agree we should not treat them differently and argue that at this point we should keep the current default behavior of throwing at construction time.

That's fair, though I want to point out that's not currently the consensus of the committee. I don't know exactly how the process goes here but I suspect you'll want to bring this up in plenary.

@mhofman
Copy link
Member

A class can extend any expression, and i think that should be preserved, even if the expression evaluates to null.

I think we agree here.

To be pedantic, a class can extend any expression that evaluates to an object ornull. In particular it's a runtime error if the extend expression evaluates toundefined. That's why I believeclass extends void wouldn't be a breaking change, as it's purely new syntax.

@ljharb
Copy link
Member

@mhofman sure, but that's a technical loophole. Providing a way to make a class with a null prototype thatdoesn't work with bothextends, and an expression, wouldn't really be fixing anything - the problem to fix is that the intuitive thing doesn't work.

@moztcampbell
Copy link

Can someone clarify if the desired behaviour ofclass D extends null {} is for instances of D to have a null prototype or a prototype of Object.create(null)? For example, is(new D) instanceof D true or false?

@bakkot
Copy link
Contributor

bakkot commentedOct 24, 2021
edited
Loading

Can someone clarify if the desired behaviour ofclass D extends null {} is for instances of D to have a null prototype or a prototype of Object.create(null)? For example, is(new D) instanceof D true or false?

The latter. Methods defined on the class should still be accessible from instances, it's just that you won't get methods fromObject.prototype.

ljharb reacted with thumbs up emoji

@devsnek
Copy link
MemberAuthor

and this pr doesn't actually change anything about that, it just fixes the errors during construction.

@jridgewell
Copy link
Member

Is it possible to just special caseFunction.prototype when constructing? Eg, if the super class isFunction.prototype (whichextends null does), we return a normal object without trying to call[[Construct]].

bathos reacted with thumbs up emoji

@jridgewelljridgewell mentioned this pull requestOct 27, 2021
@bathos
Copy link
Contributor

bathos commentedOct 28, 2021
edited
Loading

@jridgewell that’s what I was wondering about too, though I’d mistakenly been thinking “is null” instead of “is Function.prototype” and now I can see why there might be more resistance to that path. Even so I’m in favor of it since it would not break or alter existing extends null usage (I think).

Since that approach would seemingly be a modification to SuperCall steps, it doesn’t appear this would interfere withnew function() {}, where SuperCall is syntactically forbidden and the function is always [[base]], right? Are there cases where theFunction.prototype approach would have consequences apart from “something might have thrown before but now doesn’t” outside of the scenario of interest?

@jridgewell
Copy link
Member

Since that approach would seemingly be a modification to SuperCall steps, it doesn’t appear this would interfere withnew function() {}, where SuperCall is syntactically forbidden and the function is always [[base]], right?

Correct.

Are there cases where theFunction.prototype approach would have consequences apart from “something might have thrown before but now doesn’t” outside of the scenario of interest?

Not that I know of.


Essentially, this entire PR becomes:

SuperCall : super Arguments1. …5. If IsConstructor(_func_) is true, let result be ? Construct(_func_, _argList_, _newTarget_).6. Else if func is %Function.Prototype%, let result be ? OrdinaryObjectCreate(? newTarget.[[GetPrototypeOf]]()).7. Else, throw a *TypeError* exception…

I think.

bathos and ExE-Boss reacted with thumbs up emoji

@bathos
Copy link
Contributor

@jridgewell Thinking about this more I realized the other “default” class — Object, rather than Function — already has a kind of special casing related to derived class construction. The “active function” condition in theObject constructor is what prevents Object from doing its normal thing (which would amount to a return override) when it’s being called fromsuper(). Although different in form, special casing for Function.prototype in SuperCall itself is similar in effect: both are about preventing “default” intrinsics from creating surprising behavior at super() calls.

ljharb and ExE-Boss reacted with thumbs up emoji

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@jmdyckjmdyckjmdyck left review comments

@ExE-BossExE-BossExE-Boss requested changes

At least 1 approving review is required to merge this pull request.

Assignees
No one assigned
Labels
needs consensusThis needs committee consensus before it can be eligible to be merged.normative changeAffects behavior required to correctly evaluate some ECMAScript source text
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

class extends null with implicit constructor still broken
14 participants
@devsnek@ljharb@littledan@bakkot@leobalter@bmeck@ExE-Boss@Jamesernator@bathos@nicolo-ribaudo@jridgewell@moztcampbell@mhofman@jmdyck

[8]ページ先頭

©2009-2025 Movatter.jp