Implementation of property behaviors

(Joe Groff) #1

Here are my thoughts on implementing property behaviors. I think we can use an approach that instantiates storage at compile time, in order to avoid introducing type metadata, while still using separate compilation of implementations, by treating the behavior similarly to a protocol. We should be able to get optimization benefits from inlining using our existing and planned future optimization framework.

When a property is declared using a behavior, we emit a vtable referencing the interesting aspects of the property declaration:

- A projection function from Self to the behavior's storage for the property (which can be a pointer projection, since we know the behavior storage is stored),
- The accessors, lowered as methods on the containing Self type;
- If the initializer is bound, then a function that evaluates the initializer expression. If we have the eager/deferred distinction, then an eager initializer is () -> Value, and a deferred initializer is (Self) -> Value.
- If the property's name is bound, then a reference to the global string constant (which can be handed off to StringLiteralConvertible),
- If the behavior can be composed, then the get/materializeForSet/set accessors that project from Self to the base property.

So if you had (using John's syntax proposal):

behavior var [foo] name: Value = /*eager*/ initialValue {
  var storage: [Value?!]
  init() { ... }

  accessor foo(x: Int)
  mutating accessor bar(y: String)

and you instantiated it:

struct X {
  var [foo] x = 99 {
    foo { ... }
    bar { ... }

we'd emit a data structure like:

sil_global [let] @"X.x#foo vtable" : $(
  // Project behavior storage from container, *Self -> *Storage
  project_foo: @convention(thin) (RawPointer) -> RawPointer,
  // Accessors
  foo: @convention(method) (Int, @guaranteed X) -> (),
  bar: @convention(method) (String, @inout X) -> (),
  // Initial value
  initialValue: @convention(thin) () -> Int,
  // Name
  name: (RawPointer, Word)
) {
  %project_foo = function_ref @"X.x#foo.project"
  %foo = function_ref @""
  %bar = function_ref @""
  %initialValue = function_ref @"X.x#foo.initialValue"
  %name = string_literal utf8 "x"
  %tuple = tuple (%project_foo, %foo, %bar, %initialValue, %name)
  return %tuple

(If we want behaviors to be able to resiliently add accessor requirements, we'll need a more sophisticated vtable with runtime support instead of an ad-hoc global constant, more like a protocol witness table.) Effectively, we treat the behavior like a fragile protocol, albeit one that can be conformed to many times by the same set of types.

When emitting the behavior's members, we emit them as generic on Self and the property type. Each member implementation receives the container value 'self' (either inout or in_guaranteed, depending on whether it's mutating) and a reference to the behavior vtable as context (again, very similar to a protocol extension method). References to the initializer get lowered to calls to the initializer function in the vtable. References to behavior storage have to be emitted as projections from 'self', and in mutating contexts, should formally be considered formal accesses derived from 'self' in order to be valid 'inout' accesses. References to the name load the raw global string from the vtable and hand it to the contextual type's init(stringLiteral:) initializer.

Inside the type, we instantiate the behavior's storage, and initialize it as if it had an inline initializer calling the behavior's `init`. So:

struct X {
  var [foo] x = 99 {
    foo { ... }
    bar { ... }

becomes notionally:

struct X {
  var [Int?!] = foo.init(&self, &@"X.x#foo vtable")

I believe we're able to promote loads from immutable globals, specialize, and inline with existing optimizations, which should allow all this to optimize away, without us having to grow new infrastructure to do AST-level serialization and instantiation, or deviating too much from SILGen's current per-declaration code generation model.