-
Notifications
You must be signed in to change notification settings - Fork 1
Description
One goal of custom descriptors is to make the common object-oriented vtable pattern more memory efficient by letting descriptors serve as vtables, obviating separate vtable reference fields in other objects. However, custom descriptors do not address the other large pain point with the vtable pattern: receiver casts.
Methods are currently normal immutable fields on the vtable structs (which may also be descriptors). This means they support depth subtyping: a method field in $sub.vtable may be a subtype of the corresponding method field in $super.vtable. However, function types are contravariant in their parameters, so the parameters of the $sub's method must be supertypes of the parameters of $super's method. This is a problem because one of those parameters is the method receiver. In $sub's method, we want the receiver parameter to be a reference to $sub, but instead it must be a reference to a supertype of $super. This means that all overridden methods must start with a cast of the receiver parameter from the supertype that introduced the method to the specific receiver of the particular override. We could save the cost of this cast if we could prove via the type system that the receiver always had the expected type.
Here is a sketch for a type system extension for eliminating receiver casts. No existential types required!
We introduce a new kind of struct field called a method field. A method field is always immutable and contains a reference to a defined function type. A method field may only appear in a descriptor type and the first parameter of its referenced function type must be a reference to a supertype of the described type.
(rec
(type $foo (sub (descriptor $foo.vtable) (struct)))
(type $foo.vtable (sub (describes $foo) (struct (method $quack (ref null $foo.quack)))))
(type $foo.quack (func (param (ref null $foo))))
)What makes method fields special is that they have different subtyping rules than normal fields. To check whether a method field in a subtype matches the corresponding method field in a supertype, we do not just check that their reference types match like we would for normal fields. Instead, we check that the nullability of the references matches and then check the parameters and results element-wise, checking that all parameters but the first respect contravariance and all the result respect covariance. The first parameter, the receiver, does not matter for this check.
(rec
(type $bar (sub $foo (descriptor $bar.vtable) (struct)))
(type $bar.vtable (sub $foo.vtable (describes $bar) (struct (method $quack (ref null $bar.quack)))))
(type $bar.quack (func (param (ref null $bar))))
)Note here that $bar.vtable is a valid subtype of $foo.vtable even though $bar.quack is not a subtype of $foo.quack. This makes it unsafe to do a struct.get $foo.vtable $quack on an inexact reference to $foo.vtable because that should return a (ref null $foo.quack), but at runtime the $foo.vtable may actually be a $bar.vtable whose $quack field contains a completely unrelated type. To preserve soundness, struct.get on method fields is only valid with exact struct references.
Since we cannot actually read a method field out of a vtable (except in the uninteresting case where we know the exact type of the receiver), we cannot get a reference to the method to pass to call_ref. Instead, we introduce call_method and return_call_method instructions for calling methods. These instructions take the receiver type and method field index as immediates and take the receiver and other method arguments as operands.
call_method x y : [(ref null x) t1*] -> [t2*]
-- C.types[x] ~ (descriptor x') (struct fts)
-- C.types[x'] ~ (struct fts')
-- fts'[y] = method (ref null? ft)
-- ft ~ [(ref null x') t1*] -> [t2*]
call_method and return_call_method execute by getting the descriptor of the receiver, looking up the function reference in the method field in the receiver, and calling the function, passing in the receiver and the rest of the arguments. Since the function is looked up on the receiver's descriptor and we required that all method fields take a supertype of the described type as their receiver parameter, there is no need for a cast to prove that the receiver will have the expected type.
;; Works even if local $foo holds a $bar.
(call_method $foo $quack (local.get $foo))