C2's high level IR is normally called Ideal and it's really kind of a
mid level IR. There aren't that many high level nodes in it and most
of the nodes correspond to single machine instructions on most
machines. The highest level are nodes like Allocate/AllocateArray and
Lock/Unlock which embody fast and slow path idioms. They get expanded
at the very end of compilation into a larger set of nodes that
represent the operations at a lower level.
All instructions in ideal are subclasses of Node. Nodes have a
collection of Node inputs and as much as possible everything is done
by using node inputs instead of having special fields on the
subclasses of Node. Nodes maintain def-use edges so that you can
always visit all the users of any node relatively cheaply. This is
one of the reasons for the uniform treatment of inputs, since it makes
maintaining that information straightforward.
There's a lot of sharing of nodes in the ideal graph because of the
uniform treatment of nodes and the use global value numbering. For
instance there's only one copy of every constant in the graph which is
good for code but it makes understanding the graph somewhat difficult
because globally the graph can get pretty tangled.
It has the normal set of binary operators you'd expect and these are
specialized into int, long, float, double and pointer using a capital
letter at the end to identify they type, so AddI is integer add and
CmpP is the pointer compare.
Loads distinguish between the smaller signed and unsigned integral
types like char and byte, so you have instructions like LoadC to load
a character and LoadS to load a short. Stores are only differentiated
by the size of the store and not the sign. Memory operations all have
a special input called the Memory input which identifies the slice of
memory that the load or store is operating on. All memory operations
are connected to each other in a thing called the memory graph.
There's an incoming memory state at the entry point of the compile and
loads and stores are connected up to that. The memory state is
divided into a bunch of slices which correspond to aliases classes.
Operations within the same slice are ordered relative to each other
but slices aren't ordered relative to other slices except when the
slices merge into some form of memory barrier, like a call. The
slices have to merge up at any operation where the compiler can't see
the effect of a node, like a call to some other Java code, or where
explicitly ordering is needed, like volatile memory operations. These
operations produce a new memory state that all following loads and
stores consume. The memory graph captures all the memory dependence
in the ideal and insures that load and stores are issued in the
correct order.
There are no explicit basic blocks in ideal at all. It's representation is what's known
as a program dependence graph in the compiler literature. A node can
only be evaluated once all of it's inputs have been evaluated so any constraints on order of execution
have to be completely captured by the inputs to the node. There are special
nodes that are said to produce control and every node has a special
input, input 0, which can be set to point at one of these control
nodes. Nodes that produce control are obvious ones like If and Call
but there are also Region nodes which represent the merge of multiple
control flow paths plus Halt and Return that terminate control flow.
PhiNodes have a Region as their control
input and inputs for each control flow path merging at the region.
Many nodes don't require a control node at all because they can safely be executed
once their data inputs have been evaluated. All the binary operators except division don't require
a control input. Division is special in Java since it might produce a divide by zero exception.
By setting input 0 to point at a particular control flow node you're
constraining that node to be evaluated only after that control flow
node is evaluated. Note that control dependence expresses only the
earliest point that a node can be evaluated. It doesn't say anything
about the latest point in the program that node can be evaluated.
Data and memory dependence tend to constrain the latest evaluation
point. The node can actually be evaluated anywhere between the control point and the
latest point allowed by the nodes uses.
There are also special nodes called projections (ProjNode) which are
used to deal with nodes which produce multiple values. If nodes
represent their control flow split by having two control projections,
IfTrue and IfFalse, with nodes being dependent on the appropriate
control projection instead of being dependent on the If.
Call nodes are among the most complex nodes in the graph. In addition
to having a control input they are 4 other special inputs, along with
their arguments and any values which are needed for deoptimization.
The only other interesting special input is the incoming memory state.
This is similar to the memory input to a load or store but it
generally gathers all the live memory slices into one MergeMem object
to represent the fact all memory operation preceeding the call should
complete before the call is executed. Calls produce multiple values
using ProjNodes. In particular they produce a control projection for
nodes which are control dependent on the call, a memory projection to
represent the state of memory after the call is complete and a
projection for the return value of the call if there is one.
For calls which might be safepoints, there are also extra inputs which
capture all the values which are live as far as deopt is concerned. A
separate data structure called the JVMState maps from Java's notion of
frames, locals and expression stack to inputs to the node.
There is also a special node call top which is use to fill inputs that
we don't care about or which are dead.