Ticket #196 (new defect)

Opened 1 year ago

Last modified 1 year ago

(Resolved) Builtins: Does Vector need to be final, or is it enough for some of its methods to be final?

Reported by: lth Assigned to: lth
Type: defect Priority: trivial
Milestone: Component: Spec
Version: 4 Keywords:
Cc: brendan,jeffdyer,edwsmith,graydon

Description

spec/library/Vector.html: right now the spec says that Vector is final, but it may be enough (for optimizations to kick in) for its catchall getters and setters to be final. No sense in restricting the class unduly etc.

Attachments

Change History

Changed 1 year ago by brendan

Overuse of final is a problem in Java's standard library, from what I hear. If we can leave Vector non-final but make its catchalls final and still have all the optimization opportunity we want, great.

/be

Changed 1 year ago by lth

Whether Vector must be final or not depends on the precise semantics of the catchalls and whether it's possible to have fixtures named by uint32-like names.

For Vector the necessary semantics for the catchall getter/setter are that these functions must be triggered precisely when there is not a property directly on the object matching the property name being gotten/set (ie without considering the prototype chain). I don't know if this is true of catchalls today, but it must be.

Vector must in fact be non-dynamic or else intrinsic::set can be used to place a property on the object that would prevent the catchalls from being triggered. This is not desirable, and it prevents subclassing.

The only circumstances under which Vector could be non-final is if catchalls were *always* triggered for non-fixtures and if it were absolutely impossible to create fixtures that are named by uints. (I forget whether we provide syntax for this today.)

I am starting to think that the semantics of catchalls should be:

  • always trigger on non-fixtures, even if the property is defined on the object
  • never consider the prototype chain

Changed 1 year ago by brendan

I agree with the "always trigger on expandos", per Ed's suggestion in the wiki recorded by Steven.

The part about "never consider the prototype chain" doesn't apply to the proposed meta::has -- the proposal says this method must be called only if the prototype chain has been searched and the desired property name not found. Is this wrong?

/be

Changed 1 year ago by lth

Hunch: I think the methods of the protocol need to be in sync. Consider that Vector wants to control tightly all *numeric* properties (not just indexed ones), for reasons of bounds checking and error detection (catching negative indexing, indexing past the end, and also silly things like vec[n/2] when n is odd). Consider further that "in" can be used as a wacky test like "length", eg "37 in v" means length is at least 38 if you know v is Array or Vector. Now if "has" searches the proto chain then if I have stored property "37" on Vector.prototype then the implication is that "length" is at least 38, yet that's not warranted.

I would say that the rule should be simple: if you've installed a catchall, it is triggered *always* if the object that has the catchall does not have a fixture answering to the name being accessed.

Now I see the wiki has some wording about "has" being intended for use to look up properties to resolve lexical references, and that confuses me. What about "in"?

Changed 1 year ago by lth

I just wanted to note that regardless of what we do about the semantics of "has", unless it's possible to block intrinsic::set in some way then Vector must be "final".

We really don't need intrinsic::get and intrinsic::set, I suspect. If it is true that dynamic properties always go through the getter even if they're present on the object, then the getter could as well access a private shadow object for property storage instead of using intrinsic::get on the object itself. The fact that external code can use intrinsic::get (and intrinsic::set!) on the object, and bypass any kind of security, is argument enough for me to want to remove them -- they preclude the implementation of secure object access.

Changed 1 year ago by brendan

The point about "has" and lexical references is that with a reference of the form |x|, you don't know which object on the scope chain (never mind the prototype chain) to call get or set or invoke (or delete) on. You have to

Error: Failed to load processor HasProperty
No macro or processor named 'HasProperty' found

your way up the scope chain until true, then get/set/invoke/delete. This implies a "has" catchall hooked in (to ES3) via

Error: Failed to load processor HasProperty
No macro or processor named 'HasProperty' found

.

The wiki'ed proposal makes such a "has" a "last ditch" handler, so it first must check the prototype chain all the way. Given the recursive spec in ES1-3 for

Error: Failed to load processor HasProperty
No macro or processor named 'HasProperty' found

, the proposal calls "has" in the last-ditch position by passing the initial object |O| along as parameter |Q|.

Why last-ditch semantics? Because (we thought at the time) otherwise your catch-all will be called for toString and other prototype-delegated method names (typically, your invoke catchall, but context decides). This requires the catchall implementor to test and delegate explicitly, raising the cost for catchall accesses a little (Vector's |is Numeric| tests), or possibly quite a bit.

For use-cases brought up in the past, where a class uses catchalls for lazy property construction or reflection that should not override a same-named prototype property, this seemed like the right default. But the wiki'ed proposal rules out overriding prototype properties, and breaks Vector.

So, let's restrict catchalls to run only on the directly referenced object (for "has", only for each object on the scope chain, in order, until found or not). This will help Vector and not hurt any known use-cases. For non-numeric property names, it could hurt but the alternative is worse.

Separately, I don't think we can do without intrinsic::get and intrinsic::set without requiring a private shadow object, but that's perverse for the use-case at hand: should meta function get in class Vector really use a private shadow for non-numeric names?

Another use-case of catchalls is to reflect lazily from an existing peer object in some other "space". In this case too, we should not require a private shadow object.

The question is: why should intrinsic::set be a public method that can subvert whatever integrity properties we think are important? In particular, intrinsic::set should not override ReadOnly? attributes on properties. You can't reference private, protected, and internal namespaces, so you can't pass an intrinsic::Name object with such a namespace and subvert those namespaces.

The only issue I see involves custom getters and setters, which should not be subvertible from outside the class using, e.g., intrinsic::set. This case is an issue, granted. It suggests either putting get and set in a namespace private or internal to the class, and not in the intrinsic namespace -- or else using some mechanism other than a call to a built-in method for these needed APIs. Thoughts?

/be

Changed 1 year ago by brendan

Sorry, should have previewed that comment!

Talking to Lars, it seems we want a local method (akin to nextMethod in the generic functions proposal) for a catchall to forward to its intrinsic counterpart. For meta function set this could be a return value convention: true for "I did it" and false for "please use the intrinsic set". Lars points out that this makes sense only in dynamic classes (such as Array but not Vector).

For meta function get the return value is taken, but again in non-dynamic classes there is no need for intrinsic::get (it's an error to get a non-fixture), and for dynamic classes the answer if not computed by the get catchall must be undefined.

The meta function has case is likewise easy: no need for intrinsic::has. Ditto for meta function invoke and meta function delete.

So a boolean return type for meta function set would suffice. It's minimal (one bit is all the runtime needs!) but not as specific as a nextMethod or intrinsic::set hook would be -- but it can't be abused outside of the catchall to subvert setters.

/be

Changed 1 year ago by brendan

For meta function get the return value is taken, but again in non-dynamic classes there is no need for intrinsic::get (it's an error to get a non-fixture), and for dynamic classes the answer if not computed by the get catchall must be undefined.

The meta function has case is likewise easy: no need for intrinsic::has. Ditto for meta function invoke and meta function delete.

In the above two paragraphs I was writing under the assumption that the prototype chain is ignored. But that again raises to the prototype delegation issue. If you use catchalls and want prototype method name lookup to proceed as usual, you have to do something in the meta function get body when, e.g., the name is not Numeric for Vector's meta::get. A return undefined will hide every prototype-based method and force opening the intrinsic namespace.

Another idea Lars and I both hit on was throwing an exception. It can be fast if the exception reference is to an immutable binding (as with throw StopIteration? for the iteration protocol from iterators and generators). Then meta function set could revert to a function form restricted from containing return statements or having a return type annotation.

Yet another idea is a nextMethod-like local binding in the body of the function. We could even call it default::get or default::set and define only default locally in certain meta functions. Whatever we do it will be ad-hoc, because as Lars pointed out we do not want to leak intrinsic::set or provide anything like it outside of the source extent of the setter (etc. for getter, has'er, invoker, deleter -- ugh, these are awful neologisms! :-P).

Comments of all kinds welcome.

/be

Changed 1 year ago by lth

By resolution of #214, Vector.<T> can be dynamic and non-final, but its catchall methods are final. (Finality may not strictly be required if we come up with a credible protocol that subclasses can use and still play nice. But it is future proof.)

Changed 1 year ago by lth

  • priority changed from major to trivial
  • summary changed from Builtins: Does Vector need to be final, or is it enough for some of its methods to be final? to (Resolved) Builtins: Does Vector need to be final, or is it enough for some of its methods to be final?
Note: See TracTickets for help on using tickets.