-
Notifications
You must be signed in to change notification settings - Fork 225
Description
In response to #306, this issue proposes the introduction of static classes. This is a class modifier static
and a compile-time constraint on each class C
which has that modifier, ensuring that certain instances of C
can be eliminated altogether, without changing the observable behavior of the program.
The computations associated with the creation of an instance o
of C
and running one or more instance methods with o
as the receiver will be desugared by the compiler into invocations of statically resolved functions (e.g., top-level functions), thus lowering the pressure on the memory management system and enabling various other optimizations.
As seen from a developer's point of view, we can mark a class C
as static
in order to indicate the intention that compilers should be able to apply that optimization, and the class C
will then have a compile-time error unless it satisfies the requirements for allowing the optimization.
As an alternative, the optimization could be applied to each class which satisfies the constraints, silently. However, developers would then have more trouble detecting when a seemingly benign change to a class suddenly prevents that class from admitting this optimization. So we consider the explicit approach better: Developers will announce their intention to define a class whose instances can be eliminated in specific situations by including the modifier static
, and future violations of the associated constraint will then be flagged explicitly as a compile-time error.
A couple of examples of classes where this concept could be useful are given in #306, but the main motivation for having the concept are the following:
-
It should be possible to provide a guarantee that an invocation of a static extension method (cf. Static Extension Methods #41) corresponds to an invocation of a top-level function, and in particular that no objects are allocated as part of the method invocation itself.
-
It should be possible to provide a guarantee that no wrapper object is created when an object is accessed under a specific extension type (cf. Static Extension Types #42), except in the case where that object is "leaked", e.g., by being passed as an argument to a function invocation whose type is a superinterface of that extension type.
In other words, the optimization which is the core point of having this proposal is crucial for extension methods and extension types, but it may be helpful in a lot of other situations in which case it is useful to be able to specify this property separately.
Syntax
The grammar is adjusted as follows in order to support this feature:
<classDeclaration> ::= // Modified
'abstract'? 'static'? 'class' <typeApplication>
(<superclass> <mixins>?)? <interfaces>?
'{' (<metadata> <classMemberDefinition>)* '}'
| 'abstract'? 'static'? 'class' <mixinApplicationClass>
<mixinDeclaration> ::= // Modified
'static'? 'mixin' <typeIdentifier> <typeParameters>?
('on' <typeNotVoidNotFunctionList>)? <interfaces>?
'{' (<metadata> <mixinMemberDefinition>)* '}'
The only difference is that it is possible to add static
as a modifier of the class or mixin.
Static Analysis
The static classes are the following: The class Object
is a static class. When the declaration of a class C
has the modifier static
, C
is a static class. A mixin application S with M
is a static class if S
is a static class and M
is a static mixin or class.
It is a compile-time error if a class C
is static and its superclass is not static.
The classes implemented by a static class or mixin may or may not be static, no special constraints are imposed for that.
It is a compile-time error if a static class has one or more mutable instance variables.
Consider an expression e
. We say that e
occurs in a non-leaking position if it occurs in one of the following ways:
e ('||' <logicalAndExpression>)+
e ('&&' <equalityExpression>)+
e <equalityOperator> <relationalExpression>
e <typeTest>
e <typeCast>
e <relationalOperator> <bitwiseOrExpression>
e ('|' <bitwiseXorExpression>)+
e ('^' <bitwiseAndExpression>)+
e ('&' <shiftExpression>)+
e (<shiftOperator> <additiveExpression>)+
e (<additiveOperator> <multiplicativeExpression>)+
e (<multiplicativeOperator> <unaryExpression>)+
<prefixOperator> e
e <selector>+
e <assignableSelectorPart>+ <assignmentOperator> <expression>
e <cascadeSection>+
, when it occurs as an<expressionStatement>
It is a compile-time error if the reserved word this
occurs in an expression in the body of a static class, unless it occurs in a non-leaking position.
This allows for this
to occur in all the syntactic contexts where the semantics is a method invocation on this
, plus possibly some additional steps that are language defined and known to not store a reference to this
anywhere (including in variables of any kind, or as a parameter in a function invocation). With the cascade it is crucial that the value of the expression as a whole is discarded, which is ensured by the requirement that the cascade must be an expression statement.
In short, the implementation of a static class can call methods on this
, but it cannot leak this
.
Note that a constructor in a static class may have an initializing formal, e.g., C(this.x)
, but the occurrence of this
in such a formal parameter is not an expression, so there are no special constraints on that. Similarly, an element in the initializer list of a constructor in a static class may use this
as in this.x = 42
in static classes, just like other classes.
Dynamic Semantics
No special rules apply for the dynamic semantics of instances of a static class.
However, consider an instance creation expression e
which invokes a generative constructor to create an instance o
of a class C
. In the case where C
is static and e
occurs in a non-leaking position it is guaranteed that no reference to o
is ever stored, except that the stack frame for each instance method invocation of C
will store a binding of this
—but the code in the implementation of that instance method has the same constraints, so they will also not leak the value of this
.
Because of this guarantee, it is permissible for a compiler to generate code whereby the values of the instance variables of o
(which must be final because C
is static) can be copied freely and stored in activation records on the run-time stack. This means that there is no need to allocate an actual object in the heap, it can effectively be transformed into separate variables, one for each instance variable of C
,
which may be passed as actual arguments to static functions and copied freely to as many stack frames as needed, and stored as local variables, one for each instance variable of o
.
Discussion
Consider the following situation:
static class C {
final int x;
final int y;
C(this.x, int a): y = a + a;
void foo() => print("C($x, $y)");
void bar(int z) => print("Sum: ${x + y + z}");
int baz() { foo(); return x; }
}
main() {
// Allocate an instance of `C`.
var c = C();
// Create an instance in a non-leaking position.
C(3, 4)..foo()..bar(5);
var i = C(5, 6).baz();
}
The class needs to exist as usual if it is used for any purpose which does not admit elimination of the instance. So the initialization of c
causes allocation of an instance of C
, and it has state and methods just like it would have had if C
were not a static class.
However, the cascade C(3, 4)..foo()..bar(5)
can be optimized because it is a non-leaking occurrence of an instance creation that invokes a generative constructor. Similarly for the declaration and initialization of i
. This could be achieved by a desugaring translation along the following lines:
// Desugared code.
class C { // No changes here.
final int x;
final int y;
C(this.x, int a): y = a + a;
void foo() => print("C($x, $y)");
void bar(int z) => print("Sum: ${x + y + z}");
int baz() { foo(); return x; }
}
void desugared_C_foo(final int x, final int y) => print("C($x, $y)");
void desugared_C_bar(final int x, final int y, int z) => print("Sum: ${x + y + z}");
int desugared_C_baz(final int x, final int y) {
desugared_C_foo(x, y);
return x;
}
main() {
// Normal ("leaking") usages are unchanged.
var c = C();
// The non-leaking cascade usage desugars to an "inlined" representation
// of the `C` instance, followed by static function calls.
final int v1 = 3;
final int v2 = 4;
final int v3 = v2 + v2;
desugared_C_foo(v1, v3);
desugared_C_bar(v1, v3, 5);
// The non-leaking expression usage desugars similarly.
var i = let v4 = 5, v5 = 6 in desugared_C_baz(v4, v5) end;
}
It should be noted that this optimization is only applicable in the situation where the exact type of the instance-which-will-be-eliminated is known, and in particular it can be determined statically which implementation each instance method has, and which getters correspond to a native storage read operation (there is no mutable state, so if there are any setters then they are just regular instance methods, with the usual constraints).
The desugared code may look like there is not much gained by this optimization, but it should be noted that invocations of instance methods from other instance methods would otherwise be hard to optimize (when baz
calls foo
it is not known that there is no overriding implementation of foo
, but in the desugared code it is known that the exact type of the "receiver which isn't there" is C
, so we can call foo
statically, and might inline it). It should also be noted that the parameters of the desugared methods may be allocated in registers whereas the instance variables of an instance of C
would have to be stored in the heap.