1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
|
# Expression 'Tree' Intermediate Representation
The 'expression tree' IR has been developed to support low-level
optimization and advanced code generation. This document describes
this representation, the way it is generated, and the way it is
consumed. You may need this document in order to develop specialised
JIT support for a graph, add support for newly developed VM opcodes,
or to help in debugging.
## Template Syntax
Expression trees are built from the MoarVM bytecode using tree
templates, which are defined in a textual format. This textual format
has been designed for (implementation) simplicity rather than for
convenience. It's not very difficult, but it is very rigid - mistakes
are not tolerated. These templates are defined in a expression list
file. This file is then compiled to a C header file that supplies the
expression tree builder with templates The expression template
compiler is located in 'tools/expr-template-compiler.pl' and can be
invoked as such:
perl -Itools/ tools/expr-template-compiler.pl \
-o output-header-file.h input-file.expr
Note that expression list elements are always translated to (constant)
expressions in a C array and are subject to the limits of such
constant expressions.
A single *list* opens with a '(' and closes with ')'. Between the
parentheses there can be words, numbers, and nested lists. A
syntactically correct (but meaningless)n list is:
(foo 32 (bar baz))
A template definition is a list that starts with the keyword
*template:* followed by the *opcode name* for which the template is
defined, and finally the *template list*. An especially simple
template that yields constants NULL pointer is:
(template: null_s (const 0 ptr_sz))
A template list consists of *node name*, zero or more *child nodes*,
and zero or more *arguments*. The const node above has zero child
nodes and two arguments, first the constant value and second the value
size. A simple example of a nested template list would be inc_i:
(template: inc_i (add $0 (const 1 int_sz)))
**Words** are defined in the usual way (alphanumeric characters
interleaved with underscores), except that they may have *sigils*
attached to them, modifying their meaning. Words without sigils
attached are rendered uppercased and prefixed with
**'MVM\_JIT\_'**. Thus, const is rendered as **MVM\_JIT\_CONST**,
ptr_sz as **MVM\_JIT\_PTR\_SZ**, etc. These names should refer to
constants declared at compile time. A node name must be an
unmodified word.
**Substitutions** are indicated by a '$' prefix. In the inc\_i
template, '$0' is a substition refering to the first operand of the
inc\_i VM opcode. The substitution '$1' refers to the second
operand, if any, '$2' to the third etc. Operand trees are
constructed and linked into the template during expression tree
construction.
It is possible to create your own substitutions using the *let:*
statement:
(template: sp_p6oget_o
(let:
(($val (load (add (^p6obody $1) $2) ptr_sz))
(if (nz $val) $val (vmnull))))
The first subtree of the 'let:' statement is the *definition list*. A
single *definition* consists of the *substitution name* and the
template list that it defines. Definition lists are evaluated in
left-to-right order, meaning that textually later definitions can
refer to earlier definition ames. An important side effect of 'let:'
is that *let: guarantees the evaluation order of defintions*. That is,
the tree node represented by '$val' is computed before its reference
in the 'if' expression following it. (This is not the case for operand
nodes). Finally, note that 'let:' statements declare definitions in a
single expression-wide scope and that redefinitions are not allowed.
**Statement macros** are lists of which the node name start with an
ampersand '&'. These lists are compiled to C-macro
invocations. Arguments to the macro (the other elements in the list)
are reproduced verbatim, and nested lists are NOT supported. For
example:
(&MACRO_NAME foo bar 3)
Is translated to:
MACRO_NAME(foo, bar, 3)
The most common use of this is actually to use the sizeof() and
offsetof() expressions in field access, but it can also be used
to generate a constant pointer or a message string, provided you use
the appropriate C macro hackery.
**Tree macros** are a facility implemented by the template
preprocessor to aid in developing templates. Tree macros are
indicated by a '^' prefix. For instance, the '(^p6obody $1)' list
above is the invocation of a tree macro. A tree macro is substituted
directly into the invoking expression while the template is being
compiled. They differ from substitutions in two respects:
* substitutions are local (only valid within the scope of a let:
expression), while macros are globally defined. Thus, macros can
be used as a building block for templates, and substitutions can
only be used within a template.
* substitutions are *linked* into the resulting tree, thus two
references of the same substitution refer to the same IR tree
nodes. Tree macros are *spliced* into the tree, so two
invocations refer to two different trees. (These trees may be
resolved to be equal when using common subexpression evaluation, but
that's a separate issue).
A tree macro is defined using the 'macro:' keyword, followed by the
macro name (including the '^' prefix), a list of macro arguments, and
the actual macro list. For example, the '^spesh_slot' macro is
implemented as:
(macro: ^spesh_slot (,a)
(idx
(load (addr (frame) (&offsetof MVMFrame effective_spesh_slots)))
,a ptr_sz))
For visual clarity, macro arguments are indicated by a ',' prefix,
like ,a. The arguments given to the macro (words, operands, numbers,
substitution names, or entire trees) are spliced directly into the
macro. In short, *tree macros are a syntax-level facility*, and you
should expect no more of them.
Finally, **comments** are lines starting with the '#' sign. Space or
text before this sign is not allowed.
## Stores
MoarVM opcodes typically imply the storage of a computed value into a
VM-level register. In most cases, this store operation is implicit
into the template, and the expression tree builder inserts these
stores as needed. For some opcodes, this does not work, because (for
example) their VM-level implementation does not directly yield a
value. An example would be the 'atpos' group of opcodes, which
store their result directly into VM local memory:
(template: atpos_o!
(let: (($addr (copy $0)))
(if (^is_type_obj $1)
(store $addr (vmnull) ptr_sz)
(call (load (addr (^repr $1) (&offsetof MVMREPROps pos_funcs.at_pos)) ptr_sz)
(arglist 7
(carg (tc) ptr)
(carg (^stable $1) ptr)
(carg $1 ptr)
(carg (^body $1) ptr)
(carg $2 int)
(carg $addr ptr)
(carg (const ("E MVM_reg_obj_sz) int))
void))))
The $addr value refers to the address of output node. The template is
marked with a '!' postfix to signal that the expression yields no
value and takes care of storing the result itself. Because it yields
no value, it cannot be used as an intermediate result in the
expression. (Because it also emits a function call, it also
invalidates the current register context, but that is another
problem).
# Tree Structure
As noted above, the expression tree consists of *nodes*, which have
*children* and *arguments*. Children must always be references to
other nodes. Arguments on the other hand are intepreted directly as
integers. The file 'src/jit/expr.h' defines all node types supported
by the JIT compiler.
Some node types take a variable number of children, which is indicated
by a negative number in their node definition. For example, the
**ALL** node type may take any number of expressions that yield a
truth value (a **FLAG** value in JIT nomenclature), and yields true
only when all of them do (much like C-style boolean '&&'). The *first
chld* of any variadic node must be a constant number indicating the
number of children. As a rule, variadic nodes do not take arguments,
although that rule is not really strictly enforced. Some other
variadic nodes are **DO**, **ANY** and **ARGLIST**.
## Roots
The compilation order of the expression tree is determined by the tree
*roots*. Roots are simply ordered indices into the tree that
correspond (in principle) to the order of instructions in MoarVM
bytecode, although we reserve the right to reorder them in a
consistent way. (It is by adding roots that the 'let:' statement
ensures an evaluation order).
## Traversal
The Expression Tree IR exposes a mechanism using for preorder, inorder
and postorder traversal. This is achieved by defining a
MVMJitTreeTraverser structure and starting
MVM\_jit\_expr\_tree\_traverse().
struct MVMJitTreeTraverser {
void (*preorder)(MVMThreadContext *tc, MVMJitTreeTraverser *traverser,
MVMJitExprTree *tree, MVMint32 node);
void (*inorder)(MVMThreadContext *tc, MVMJitTreeTraverser *traverser,
MVMJitExprTree *tree, MVMint32 node, MVMint32 child);
void (*postorder)(MVMThreadContext *tc, MVMJitTreeTraverser *traverser,
MVMJitExprTree *tree, MVMint32 node);
void *data;
MVMint32 *visits;
};
The data pointer is supposed to point to a user-supplied data
structure. The visits array is filled with the number of times a
certain node is visited. (Because the tree is really a DAG, a node can
be visited many times). It is suggested you use this to decide whether
to reevaluate your code. In most cases, I find that you don't want
revisiting behaviour.
## Node information
The default tree structure does not supply much information beyond the
structure of the graph. Additional knowledge is encoded in the
tree->info array, which is populated during various passes
through the code. I reserve the right to add and remove fields at
will, but some significant fields are:
* **value** - the MVMJitExprValue structure that holds information
related to the value of this node (i.e. what register it is compiled
too, the size of the value, or the memory address it refers too).
* **tile** - the MVMJitTile* that defines how this value will be
compiled to machine code.
* **op_info** - Static information on this JIT node, e.g. the number
of args and number of children.
* **spesh_ins** - The MVMSpeshIns* of which this value is the root,
which is probably useful information for optimization, because it
can include type and usage information.
## Value types
The JIT IR defines a non-strict and overlapping set of value types,
most importantly REG, MEM, LBL, FLAG, and VOID - the primary tile
result types - and INT, NUM, and PTR which are mostly useful to
indicate C function arguments. It is quite essential that C function
arguments are given their correct class to ensure that they are placed
in the correct registers.
|