Grace Annotations
1 Annotation semantics
Annotations are objects which perform syntax checking and intercession on language
constructs. They are provided as a list of expressions following the
is keyword. The location of the list depends on the syntax
of the construct they are annotating. Annotations have no special static
semantics, so while a dialect is free to reason about those it can statically
determine the value of, there is no guarantee that it will be able to
do so for all of the annotations in a module. As such, we provide only
a dynamic semantics here, and all static semantics are determined on
a dialect-by-dialect basis.
1.1 On Objects
The syntax for annotations on objects is as follows:
object is annotations { ... }
annotations is a comma separated list of expressions.
When this expression is encountered by the interpreter, each expression
in the annotation list is evaluated, in the order listed, before the
object is evaluated. The resulting object of each expression is asserted
to be of type ObjectAnnotation. Failure to satisfy the
type results in the raising of an exception. The assertions are performed
as soon as the annotation object has been evaluated, so if an annotation
fails the assertion then none of the annotations which follow it in
the annotation list will be evaluated.
The type ObjectAnnotation is defined as:
type {
annotateObject → ObjectHandler
}
The type ObjectHandler is defined as:
type {
audit(source : AST.ObjectConstructor) in(scope : Scope) → Done
onConstructed(value : Object) → Done
onInitialized(value : Object) → Done
onReceive⟦T⟧(request : Request⟦T⟧) → T
}
The audit() in() method is passed the AST node of the
source of the object. This permits annotations to perform runtime checking
in the same manner as dialects. Auditing annotations have access to
the values of variables referenced inside the object with the scope parameter, which is a lookup function from identifier name to local
variable value. The names are not strings, and must be retrieved from
the source object, thereby preventing
audit() in() from looking up variables which are not referenced
in the audited object itself. A helper method values in
scope walks all unique variables referenced in source.
One of the crucial properties of an ObjectHandler is that
it has been given complete permission over the object it is providing
a handle to. As a result it can add aspect-style intercession usually
reserved for a proxy directly into the object itself. There may be
other intercession operations the handle should export (such as onReflected()),
but these are the proposed ones for now.
The type Request⟦T⟧ is defined at least as:
type {
source → AST.Request
receiver → Object
name → String
parts → List⟦type {
name → String
parameters → List⟦Object⟧
}⟧
continue → T
}
Access to the source of a request is crucial for implementing security guarantees
for annotations such as confidential, as we
will we see soon. The
continue method completes the request to the receiver
and returns the result of the method which was called. This permits
implementation of 'around' advice.
An annotation that performs all of the actions available to it might look like this:
def example = object {
method annotateObject → ObjectHandler {
object {
inherits objectHandler.abstract
method audit(source : AST.ObjectConstructor) in(scope : Scope) → Done {
check(source) in(scope)
}
method onConstructed(value : Object) → Done {
remember(value)
}
method onInitialized(value : Object) → Done {
interactWith(value)
}
method onReceive⟦T⟧(request : Request⟦T⟧) → T {
log(request)
request.continue
}
}
}
}
method superObject {
object {
// Constructed
...
}
}
def myObject = object is example {
inherits superObject
...
// Initialized
}
// Received
myObject.run
Note that the 'constructed' event will occur once all methods have been added to the object, but before any initialisation code has been run. An alternative option is that the annotation could receive the identity of the object which is being constructed, but it is a runtime error to request methods on it or perform any other operation which inspects the structure of the object. The purpose of the 'constructed' event is for systems like branding, where storing the identity of the object is all that is performed, but it is necessary to do so before initialisation code is run in case that code relies on the object satisfying the brand type. It is not expected that annotations will want to request methods on objects until after initialisation is complete, so the semantics could be simplified by removing this event (arguably the most complicated one, too), and avoids confusion between the meanings of 'construction' and 'initialisation'. The 'constructed' event is strictly more powerful, and so has been included for now.
1.2 On Methods
The syntax for annotations on methods is as follows:
method run(...) → ... is annotations {}
Again, annotations is a comma separated list of expressions.
The semantics for the execution of these annotations is more complicated
that for objects, because they are annotating declarations rather than
expressions, and so must run as early as possible in order to ensure
the properties they enforce are immediate.
All annotations immediately inside the body of an object expression are run as
soon as the execution of the body begins, including before any inherits
clause. The annotation list of each declaration is run in the order
of the declarations themselves, so a method which appears syntactically
before another will have its annotation list executed first. The resulting
object of each expression is asserted to be of type MethodAnnotation,
which is also checked as soon as the annotation is evaluated and raises
an exception on failure.
The type MethodAnnotation is defined as:
type {
annotateMethod → MethodHandler
}
The type MethodHandler is defined as:
RequestHandler & type {
audit(source : AST.Method) in(scope : Scope) → Done
}
Where RequestHandler is the type:
type {
onRequest⟦T⟧(request : Request⟦T⟧) → T
}
As with ObjectHandler, the audit() in() method
can be used to audit the source of the method like a dialect, and the
'request' event allows a level of intercession only available because
the method itself has provided permission. Again, there may be other
events worth considering here: refelection, overriding and so on. It
may also be necessary to retrieve the surrounding object of the method,
but that would be inherently unsafe to perform before the object is
at least constructed.
The implementation of the confidential annotation might
look like this:
def confidential = object {
def annotateMethod = object {
inherits methodHandler.abstract
method onRequest⟦T⟧(request : Request⟦T⟧) → T {
def receiver = request.source.receiver
if (!ImplicitReceiver.match(receiver) && !Self.match(receiver)) then {
ConfidentialAccess.raiseForRequestTo(request.name) in(request.receiver)
}
request.continue
}
}
}
1.3 On Fields
Annotations on fields are more complicated than methods because there are more significant events that need to be included in the handle. As such, there needs to be distinguishing types for fields, including between defs and vars.
The types DefAnnotation and VarAnnotation are defined, respectively, as:
type {
annotateDef → DefHandler
}
type {
annotateVar → VarHandler
}
The type DefHandler is defined as:
RequestHandler & type {
audit(source : AST.Def) in(scope : Scope) → Done
onInitialized⟦T⟧(value : T) → Done
}
The 'initialized' event is triggered once the body of the def is completed evaluation and the field is assigned its value (which also means the value satisfied the guard on the field). The value is passed to the response.
And the type VarHandler is defined as:
RequestHandler & type {
audit(source : AST.Var) in(scope : Scope) → Done
onAssign⟦T⟧(value : T) → T
}
A var is never initialised, only assigned to. Note that the onAssign() method can change the value which is assigned to the var.
Alternatively, the intercession API for a var can be seen as a pairing of methods:
type {
audit(source : AST.Var) in(scope : Scope) → Done
getter → RequestHandler
setter → RequestHandler
}
This format is more powerful, because the setter handler is passed the full request
to the setter method, instead of just the value, allowing it to perform
'around' advice rather than just 'before', which is mostly what onAssign() is capable of. There's no reason that onAssign() couldn't
also take a request object as well, so really the only difference is
whether a VarHandler should be a subtype of RequestHandler or not.
1.4 On Types and Signatures
The syntax for annotations on objects is as follows:
type is annotations { ... }
Once again, these annotations follow a standard pattern. The type
TypeAnnotation is defined as:
type {
annotateType → TypeHandler
}
The type TypeHandler is defined as:
type {
audit(source : AST.Type) in(scope : Scope) → Done
onMatch⟦T⟧(pattern : Pattern⟦T⟧) against(value : Object) → MatchResult⟦T⟧
}
In this sense a type annotation is essentially an annotation on the type's
match() method, where the request can be continued by
requesting match() on the type value itself.
Of more interest is annotations on signatures. The syntax is as follows:
run(...) → ... is annotations
The type SignatureAnnotation is defined as:
type {
annotateSignature → SignatureHandler
}
The type SignatureHandler is very similar to TypeHandler:
type {
audit(source : AST.Signature) in(scope : Scope) → Done
onMatch⟦T⟧(signature : Pattern⟦T⟧) against(value : Object) → MatchResult⟦T⟧
}
The representation of each signature is a type with just that signature in it:
or alternatively, a type is just the intersection of all of its signatures.
This means the signature can look at the entire object which it is
matched against, though the standard implementation of its match() method is just a reflective examination of the given object to see
if it contains a satisfactory method.
The distinction between annotations on methods and annotations on signatures is
important. Methods annotations will usually modify the behaviour of
the method in some way, whereas signature annotations will modify the
behaviour of the type test. Most annotations on methods will probably
want to also implement an
annotateSignature method that checks that the corresponding
method has also been annotated, but it is not necessary, and there
can be annotations which apply to signatures which do not apply to
methods. For instance, there may be an annotation inheritable that checks that the corresponding method can be inherited, but no
annotation is required on the actual method itself.