3.2.3. Transformation phases
3.2.3.1. Parsing
The raw source text of a model is parsed with the functions in the module Perspectives.Parsing.Arc. The output is an Abstract Syntax Tree in terms of the data structures defined in the module Perspectives.Parsing.Arc.AST.
Expression parsing
Query expressions form a substantial subset of the Perspectives Language. They have their own parsers, in Perspectives.Parsing.Arc.Expression. These functions create an AST in terms of the data structures defined in Perspectives.Parsing.Arc.Expression.AST.
Assignments (as in the right hand side of bot rules) are treated by the expression parser, too.
3.2.3.2. Phase Two
The next step – simply called ‘Phase Two’ – is performed by functions in module Perspectives.Parsing.Arc.PhaseTwo. It is only now that a DomeinFile is created. During this phase, the identifiers of the definitions that have been declared, are qualified with namespaces. Contexts and roles are both namespace-giving (embedded definitions are scoped with the name of their embedder). However, references to definitions are left untreated, because a reference may be made in the text before that what it refers to, is fully qualified (the so-called forward reference problem). In this phase we also collect the models that the model under treatment depends on (its ‘referredModels’).
During this phase, we also expand all prefixed names. Prefixed names are shorthand for qualified names, making life easier for modellers. Each prefix must be defined for some (context) scope. Phase Two expands all these names (A full treatment of this subject is given in the text: Expanding prefixed names). Prefixes as such are lost beyond Phase Two (but the namespaces they refer to are saved in referredModels).
3.2.3.3. Phase Three
It is only in the third phase that references are qualified. These are:
-
the object and indirect object of actions
-
the binding of role definitions
-
references to properties (in views)
-
references to views (in actions)
-
the type of value that is returned from a computed role.
Furthermore, Phase Three is responsible for several other processes:
-
expression compilation
-
inverted queries for properties and roles
-
qualification of binding to a Calculated role
-
rule compilation
Expression compilation
Expressions in the source text have, up till Phase Three, been stored as abstract syntax trees. Now that all identifiers are fully qualified, we can look up, for any identifier used in an expression, what its type is. Only now can we compile an expression into a description of a function that can be executed runtime.
Let’s step back. A query expression is used to define a so-called Calculated Role or Calculated Property. When an end user requests the value of a Calculated Role through the Api, a function is executed on a context instance and a set of (Enumerated) role instances is returned. It is these functions whose descriptions are compiled in Phase Three.
A QueryFunctionDescription is a data type (defined in Perspectives.Query.QueryTypes) that gives details of a function’s domain and range and how it should be executed. This can either be a primitive (implemented as a Purescript function) or a composition of QueryFunctionDescriptions. As queries consist for a large part of traversing from contexts to roles and vice versa, we need to be able to look up the type of each role. Hence this compilation can only be done in Phase Three.
The module responsible for this compilation is Perspectives.Query.DescriptionCompiler.
From the description of a query function, a Purescript function is computed with the functions in Perspectives.Query.Compiler. This, however, is just in time compilation: each function is compiled just before it is applied for the first time during a PDR session (the compiled function is cached, so it is compiled only once during each session). In other words, actual query function compilation is a run-time process and does not happen in compilation time (the subject of this text).
Inverted queries for properties and roles
A big responsibility of the PDR is to make information available to those users that are modelled to have a perspective on it. Concretely, if user A adds a role instance to a context instance (as an example), all peers in that context with a perspective on that role should be informed. To this end we calculate, in Phase Three, inverted queries that compute in run time, from a role instance, the peers that should be informed. Whenever a role instance is added (run time), we run the appropriate queries and then add a Delta to a Transaction to be shipped to those peers. In order to prevent misunderstanding:
-
inverted queries are constructed during Phase Three (compilation time)
-
inverted queries are executed during run time.
Qualification of binding to a Calculated role
A role can be defined with a restriction on the type of roles that can be bound to it (that can fill it). This restriction is often in terms of another Enumerated Role, but may be in terms of a Calculated Role. But the type of a Calculated Role is nothing but a composition of types of Enumerated Roles (it is an Abstract Data Type consisting of Sum and Products). So, really, we need to record the type of the Calculated role as the restriction on the binding of such an Enumerated role.
But the type of a Calculated Role is the range of its compiled expression. Hence, only after the expressions of a model are compiled, can we really qualify the binding of an Enumerated Role restricted with a Calculated Role.
This step finds all such roles and sets the restriction on their bindings.
Rule compilation
Rules consist of a condition and assignments (where these assignments may be in the body of a let-expression). In this step we compile the condition (just an expression) and the assignments. As with expressions, an AST representing an assignment is turned into a QueryFunctionDescription.
Strictly speaking we should separate expressions (that just have a value) from assignments (that change state). However, their treatment is sufficiently similar to justify lumping them together in the same modules.