8.6.3. Standard variables: the lexical concepts in QueryFunctionDescriptions
Expressions are parsed to Step and Step is compiled to a QueryFunctionDescription. Runtime, a QueryFunctionDescription is compiled to an executable function. We’ve seen above how the lexical concepts current context, current subject and current object are represented in parse tree elements. How do these representations relate to standard variables?
8.6.3.1. The expression compiler
First, a quick recap on QueryFunctionDescription. These represent functions, with a domain and range and some information on functionality and being mandatory, along with a representation of the actual function to compute. The expression compiler (module Perspectives.Query.ExpressionCompiler) uses these representations to reason about the correctness of the function expressions entered by the modeller (it checks, among others, whether properties that are used do in fact exist on the roles they are supposed to be on, whether both sides of (in)equalities have the same type, etcetera).
8.6.3.2. Inserting standard variables into expressions
Standard variables are just like the variables that the modeller himself can introduce, in a letE or letA construct, but he need not bind them himself. Consequently, expressions will only have references to those variables but never their bindings. This would cause the expression compiler to throw an error. To prevent these errors, we add bindings automatically for the standard variables to the parse tree of expressions. This allows the expression compiler to reason about their use as usual.
These bindings are added to existing letE and letA constructs, or an expression or statement is wrapped in a letE to hold the new bindings. We don’t actually always add all standard variables to expressions: the code analyses the expression to see what standard variables appear.
By modifying the expression itself, we can evaluate it in the standard way in any location in the code of the core. We guarantee that each expression is self-contained: the core code does not have to add standard variables on computing the value of an expression.
This is very convenient, as calculated roles and properties can be thought of as named functions that are, indeed, called by name from other queries. We can therefore freely compose such functions without the need to add bindings to the runtime environment.
8.6.3.3. Treatment of statements
Statements are treated differently. We do always add standard variables to statements (single statements or sequences of them, or a letA construct)(This happens in module Perspectives.Parsing.Arc.PhaseThree). This is because when we execute an automatic action or a notification, the core code itself needs the value of these variables. The origin
is available anyway, because it is the argument that the compiled expression function is applied to. But in order to distribute delta’s for the changes incurred by an automatic action, we need to know the current actor. And to compute the current actor, we need to have the current context – because the actor is computed relative to the current context. So, before actually executing statements, we have the value of all three standard variables available.
It would therefore be a waste of resources to recompute them with the statements while it introduces almost no overhead to add the already computed values to the runtime environment that holds values for our variables.
We handle this in runtime as follows, in the execution machine (modules Perspectives.RoleStateCompiler and Perspectives.ContextStateCompiler):
-
we let the execution machine push a runtime environment (the data construct that we store variable bindings in);
-
we then add the values of the standard variables to that environment.
But what about the bindings added to the statements? Well, in the very last step that creates an executable function (in module Perspectives.Query.UnsafeCompiler), we remove the bindings for the standard variables. The executable functions will never compute their values, nor add them to the runtime environment. A variable reference will just pick up the value added by the runtime execution machine.
You may have noted that an extra environment is pushed by the compiled statement (we just remove the variable bindings). This is unnecessary, but has no effect: variable lookup scours the entire environment stack, so the empty environment is just passed (adding very little overhead).
8.6.3.4. Expressions in statements
We just add bindings to entire statement groups, not to each and every individual expression in them. This is because each expression is applied to the same origin
and because there are no syntactical constructs within statements that change the current context, subject or object. Neither can statement groups be nested inside other statement groups (do, action and notify do not contain lexical constructs that introduce scopes). Hence a single set of bindings suffices.
8.6.3.5. Simple treatment of compile time only bindings
As the values of standard variables are never computed with the assignments that they occur in, we do not actually have to construct a full function to compute it. Just having a QueryFunctionDescription with the right domain, range, functionality and being mandatory information, will do: it is all the expression compiler needs to do its checks. For that reason we introduce a QueryFunction that can be considered a no-op. It is actually this QueryFunction that makes the unsafe compiler remove a binding.
Consider the computation of the current context from the origin
, for a do. Now when we have context state, it is trivial: just the identity function. But when we have role state, we take the current context value from the RoleIdentification and put that into this parse tree construct:
Simple $ TypeTimeOnlyContext pos <currentcontext>
(where <currentcontext> is the name of the context type that is the current context) The expressioncompiler will turn it into:
SQD currentDomain (QF.TypeTimeOnlyContextF <currentcontext>) (CDOM (ST $ <currentcontext>)) True True
This can be used to reason with: whenever currentcontext
occurs in the statement, the compile knows its runtime type.
8.6.3.6. Overview: standard variable computing in runtime
What variable should be computed when? With both context- and role situations, actions, automatic actions and notifications, we have a lot of cases. We do not always have to compute a value:
-
origin
never needs to be computed runtime; -
currentcontext
must be computed for roles; -
currentactor
(ornotifieduser
) must always be computed.