Since my last post, I have been implementing all of the details of properties. They’re almost done now, I just have a bit of code generation to finish.
While I was implementing them, I was thinking about the system for initialisation I described in my last post, and discovered how broken it was.
Escape Analysis
While an object is being initialised, the value of ‘this
‘ is not allowed to escape from the initialiser or the constructor. For example, you can’t do the following:
class Foo { string uninitialised; static ?Foo stored; constructor() throws Exception { stored = this; throw new Exception(); } static uint main([]string args) { try { Foo f = new Foo(); } catch Exception e { if stored != null { stdout::println((cast<Foo> stored).uninitialised); } } return 0; } }
If ‘this
‘ was allowed to escape the constructor, the stdout::println()
would have undefined behaviour, probably causing a crash. So obviously escape analysis is necessary; usually we enforce it by stopping anyone from using ‘this
‘ or calling an instance method before the object is fully initialised, but how do we cope with it for properties?
One thing to note about properties is that some of them have to be initialised in the constructor. For example, properties which do not have a default value for their backing variable must always be initialised, and final properties must always be initialised exactly once.
The problem is how to do this initialisation. If we call the setter, it could allow ‘this
‘ to escape (and we can’t just check that it doesn’t, because it could be overridden by a subclass). We could check setters much more strictly, but this would stop us from ever doing a lot of the things we might like to do in a setter, such as using the old value of the property.
Property Constructors
To solve this problem, Plinth allows properties to have constructors, which can only run once during object initialisation, and never again. This is the much more strictly checked version of the setter: it is not allowed to use ‘this
‘ or depend on anything in the object being initialised, or even depend on anything definitely not yet being initialised (so it can’t either read or write from final variables).
To reduce the amount of unnecessary code written, if a constructor is required but not implemented, it defaults to using the setter’s implementation. (Of course, this doesn’t always work, and in such cases a separate implementation must be provided).
When you are initialising an object, you are allowed to call a property’s constructor by assigning to it for the first time. However, once the constructor has been called, you are not allowed to call its setter until the object is fully initialised. This can lead to interesting situations where we can’t decide between the constructor and the setter:
class Asdf { property string text // (use the default getter) setter(value) { stdout::println("setter"); text = value; } constructor(value) { stdout::println("constructor"); text = value; }; // constructor for Asdf: selfish constructor(boolean b) { if b { this.text = "blah"; // prints "constructor" } this.text = text; // control flow error: impossible to decide between setter and constructor this.text = "Asdf"; // prints "setter" (allowed because the object is fully initialised) } }
Static and Final Properties
Properties don’t only work for instances of objects, and they are allowed to be final.
The definition of ‘final’ in plinth is: ‘must be assigned to exactly once’, with the extra restriction for instance members: ‘must be assigned to before it can be read’. (The same definition is used for both fields and properties).
So what does this mean for the property functions? As you might guess, final properties never have a setter, and must have a constructor. However, depending on what you’re expecting, non-final properties might look slightly weird.
In the case of static non-final properties, they must have a setter, but they cannot ever have a constructor. This is because you could never enforce that the constructor would be called before the getter or setter: a static initialiser from another class might call the setter before you get a chance to call the constructor. Because of this, a constructor would be meaningless.
For non-static, non-final properties, constructors are actually optional. If you declare one, it definitely exists; if you don’t declare one, it *might* not exist, but it’s still possible. A property which has a backing variable which doesn’t have a default value has to have a constructor, for example:
property string s getter { return s; } setter(value) { s = value; };
This property has a constructor which defaults to using the setter’s implementation, even though it wasn’t declared. This allows a subclass to override the constructor to do something different during initialisation if necessary.
The truth table for when getters, setters, constructors exist is as follows:
static | final | getter | setter | constructor |
---|---|---|---|---|
✔ | ✔ | ? | ||
✔ | ✔ | ✔ | ||
✔ | ✔ | ✔ | ||
✔ | ✔ | ✔ | ✔ |
The ‘?’ means that there is a constructor if either a) one is declared, or b) the property is not marked as unbacked
and has a type which does not have a default value.