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
    onReceiveT(request : RequestT)  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 RequestT is defined at least as:

type {
    source  AST.Request
    receiver  Object
    name  String
    parts  List⟦type {
        name  String
        parameters  ListObject
    }
    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 onReceiveT(request : RequestT)  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 {
    onRequestT(request : RequestT)  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 onRequestT(request : RequestT)  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
    onInitializedT(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
    onAssignT(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
    onMatchT(pattern : PatternT) against(value : Object)  MatchResultT
}

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
    onMatchT(signature : PatternT) against(value : Object)  MatchResultT
}

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.