Code Generation
The compiler library contains the functionality required to emit code in the intermediate
language, compile it into machine code for the
current platform, and to later execute the code (usually by binding it to a Function
). The
functionality related to the intermediate language is covered in detail in the language
reference. This section covers the types in more
detail.
In addition to the code generation facilities for the intermediate language, the library also contains a few utilities that can be used to make code generation more convenient.
Operands
The value core.asm.Operand
represents an operand for an instruction in the intermediate
language. The different types of operands are described in detail
here.
The Operand
class has a number of cast constructors that allow implicit conversion from registers
(core.asm.Reg
), variables (core.asm.Var
), labels
(core.asm.Label
), references
(core.asm.Ref
), and source positions (core.lang.SrcPos
). There are also a
number of functions that create operands from various types of literals.
The Operand
type has the usual operators for comparison, and toS
for creating a string
representation. The empty operand (created by the default constructor) can also be checked for using
the empty
and any
functions as usual. It also has the following operands for accessing the
contents of the operand:
-
core.asm.OpType type()
Get the type of the operand.
-
core.asm.Size size()
Get the size of the operand.
-
core.Bool readable()
Is this operand readable?
-
core.Bool writable()
Is this operand writable?
The depending on the value returned from type
, it is possible to extract the contents of the
operand by calling the appropriate members below. These functions check the type internally and
throws an error (InvalidValue
) if they are used incorrectly.
-
core.Word constant()
Get the constant in the operand. Valid if
type
returnsconstant
. If the value stored is aSize
or anOffset
, then the value obtained by callingcurrent
is returned. -
core.asm.Reg reg()
Get the content register. Valid if
type
returnsregister
orrelative
. -
core.asm.Var var()
Get the variable accessed. Valid if
type
returnsvariable
.Note: The size of the returned variable is equal to the size we wish to read. This may differ from the size of the original variable!
-
core.asm.Offset offset()
Get the offset used. Possible to call any time, but only makes sense for
relative
,variable
, andrelativeLabel
. -
core.asm.CondFlag condFlag()
Get the condition flag. Valid if
type
returnscondFlag
. -
core.asm.Block block()
Get the block. Valid if
type
returnsblock
. -
core.asm.Label label()
Get the label. Valid if
type
returnslabel
. -
core.asm.Ref ref()
Get the reference. Valid if
type
returnsreference
. -
core.lang.SrcPos srcPos()
Get the source position. Valid if
type
returnssrcPos
. -
core.Maybe<core.Object> obj()
Get the referenced object. Valid if
type
returnsobjReference
. Returns anObject
. -
core.Maybe<core.TObject> tObj()
Get the referenced object. Valid if
type
returnsobjReference
. Returns aTObject
.
Instruction
The core.asm.Instr
class represents a single instruction in the intermediate
representation. The instruction format is described in detail
here. In short, each instruction has
two operands. The first one may be written to, and the second is only ever read from. The Instr
class is created by one of many free functions with the same name as the op-codes described in the
language reference. These functions perform basic checking of operand sizes and properties, and
throw an error if they are used incorrectly.
The class itself has only a few members as follows:
-
core.asm.OpCode op()
Get the op-code.
-
core.asm.Operand src()
Get the source operand.
-
core.asm.Operand dest()
Get the destination operand.
-
core.asm.DestMode mode()
Get the mode of the destination operand. This determines how the instruction uses the destination (e.g. read/write, read, or write).
-
core.asm.Size size()
Get the maximum size of the operands.
-
core.asm.Instr alterSrc(core.asm.Operand src)
Create an instruction with a different source operand. Intended to be used by backends, so performs no sanity checking.
-
core.asm.Instr alterDest(core.asm.Operand dest)
Create an instruction with a different destination operand. Intended to be used by backends, so performs no sanity checking.
-
core.asm.Instr alter(core.asm.Operand dest, core.asm.Operand src)
Create an instruction with a different set of operands. Intended to be used by backends, so performs no sanity checking.
For function class, the derived class core.asm.TypeInstr
is used. This adds the members
type
and member
. The type
member contains a type description of the parameter or return type.
The member
member is only used for function call instructions and determines whether the called
function is a member function or not.
Arena
An core.asm.Arena
describes the platform-specific bits of code generation. The function
core.asm.Arena core.asm.arena()
can be used to create an Arena
for the current platform.
Creating an Arena
for another platform allows generating code for another platform. This is,
however, not very useful currently, as it is not possible to save generated code to disk. It does,
however, open up for future cross-platform builds.
Listing
The core.asm.Listing
object represents a sequence of instructions, alongside information
about local variables, labels, and blocks. The concepts behind the listing is described in more
detail here. In short, the listing can be
seen as representing a namespace for labels, blocks, and variables. As such, it is able to
generate new names for these concepts that are only usable in that particular listing. These names
are represented as instances of the types core.asm.Label
, core.asm.Block
,
and core.asm.Var
respectively. These created names can then be used as operands to
instructions that are then added to the listing. Labels can also be added directly to the listing to
allow other instructions to reference other instructions easily (e.g. for jump instructions).
Creation
A listing stores the expected return type of the function it will be bound to. As such, it can be created in any of the following ways:
-
init()
Create a listing for a function that returns
void
. -
init(core.Bool member, core.asm.TypeDesc result)
Create a listing for a function that may be a member function (based on
member
), and returns the typeresult
.
There are also two overloads that accept an arena
. This is to allow the listing to format
backend-specific things properly. It does not otherwise affect how the Listing
works.
Adding and Inspecting Instructions
Instructions and labels are added using the <<
operator. The Listing
also provides the usual
empty
, any
, count
, and []
members to allow iterating through the instructions in the
listing. Labels that appear before a particular instruction are retrieved using the labels
function.
Labels
Labels are created using the function core.asm.Label label()
. It returns a new, unique
label that is valid in the listing. As mentioned above, labels can be used as operands to
instructions, and they can be inserted in between instructions. Each label is assumed to be added to
the listing exactly once.
Variables and Blocks
Variables in a Listing
are arranged in a tree of blocks, similarly to many block-scoped languages.
The Listing
provides the following members to create and manipulate blocks:
-
core.asm.Block root()
Get the root block.
-
core.asm.Block createBlock(core.asm.Block parent)
Create a block inside
parent
. Note that it does not matter ifparent
is the block or any part inside that block. -
core.asm.Block parent(core.asm.Block b)
Get the parent part to a variable or a block.
-
core.Bool isParent(core.asm.Block parent, core.asm.Block q)
See if the part
q
is an indirect parent toparent
. -
core.Array<core.asm.Block> allBlocks()
Get all blocks.
-
void addCatch(core.asm.Block block, core.asm.Listing.CatchInfo add)
Add a catch handler to a block. Multiple handlers may be added to a single block. If so, they are evaluated in the order they were added.
-
void addCatch(core.asm.Block block, core.lang.Type type, core.asm.Label resume)
Convenience function to create a
CatchInfo
value. -
core.Maybe<core.Array<core.asm.Listing.CatchInfo>> catchInfo(core.asm.Block block)
Get all catch clauses for a block.
Variables can then be added to blocks using any of the createVar
overloads. Parameters are treated
specially, and are created by the createParam
overloads. Variables and parameters can then be
inspected and manipulated using any of the following functions:
-
core.Bool accessible(core.asm.Var v, core.asm.Block p)
See if the variable
v
is accessible in the partp
. This is almost equivalent to checking if any parent blocks ofp
contains the variable. -
core.Bool isParam(core.asm.Var v)
Is this a parameter?
-
core.Maybe<core.asm.TypeDesc> paramDesc(core.asm.Var v)
Get parameter info about a variable.
-
core.Maybe<core.asm.Listing.VarInfo> varInfo(core.asm.Var v)
Get debug info about a variable.
-
void varInfo(core.asm.Var v, core.Maybe<core.asm.Listing.VarInfo> info)
Set debug info.
-
core.asm.Block parent(core.asm.Var b)
Get the parent part to a variable or a block.
-
core.asm.Var prev(core.asm.Var v)
Get the variable stored just before
v
in this stack frame. Within a single block, it just returns the variable added beforev
. Ifv
is the first variable in that block, the last variable of the previous block is returned. This will give all variables visible at the same time as the start variable of the iteration. Parameters are returned lastly. -
core.Array<core.asm.Var> allVars()
Get all variables. Always in order, so allVars()i.key() == i
-
core.Array<core.asm.Var> allVars(core.asm.Block b)
Get all variables in a block.
-
core.Array<core.asm.Var> allParams()
Get all parameters.
-
void moveParam(core.asm.Var param, core.Nat to)
Move a parameter to a specific location.
-
void moveFirst(core.asm.Var var)
Move a variable to the beginning of a block. This impacts memory layout and destruction order.
ExprResult
The value core.lang.ExprResult
is a utility class that can be convenient to use for code
generation in various languages. It is, for example, used in Basic Storm.
The ExprResult
is intended to represent a result from evaluating an expression. It encapsulates
the idea that an expression may either result in some value (represented as a
core.lang.Value
), or it may instead stop execution before it evaluates to a value (e.g.
returning from the function or throwing an exception). As such, an ExprResult
thus has a separate
representation of will never return, which is distinct from returns, but produces no value.
It provides the following members:
-
init(core.lang.ExprResult& other)
Copy constructor.
-
init()
Return void.
-
init(core.lang.Value result)
Return a value.
-
core.lang.Value type()
Result type. If this is the no-return value, then
void
is returned here. -
core.Bool value()
Does the expression return a value (ie. not
void
)? -
core.Bool empty()
Does the expression return void?
-
core.Bool nothing()
Does the expression not return at all.
-
core.lang.ExprResult asRef(core.Bool v)
Convert the result to a by-reference value.
The free function noReturn
creates the special value that indicates that the expression will not
return.
CodeGen
The utility class core.lang.CodeGen
encapsulates a core.asm.Listing
(stored
in the member l
) that is being used to output instructions to. It also contains additional state
that is typically needed during code generation. This state is a core.lang.RunOn
that
indicates the current thread, and a core.asm.Block
that represents the current block.
This means that instead of passing three variables to code generation functions, one can instead use
a single CodeGen
object.
In addition to bundling three concepts together, it provides a few convenience functions:
-
core.lang.CodeGen child(core.asm.Block block)
Create a child code gen where another block is the topmost one.
-
core.lang.CodeGen child()
Create a child code gen with a new child block.
-
core.asm.Var createParam(core.lang.Value type)
Add a parameter with the specified type.
-
core.lang.VarInfo createVar(core.lang.Value type)
Add a variable in the current block.
-
core.lang.VarInfo createVar(core.lang.Value type, core.asm.Block in)
Add a variable in the specified block.
The createVar
functions returns a core.lang.VarInfo
object. This object stores a
core.asm.Var
alongside a value that records if this variable needs to be activated after
it has been constructed. It is typically not necessary to inspect this value manually, it is enough
to call the void created(core.lang.CodeGen gen)
member as soon as the variable
has been initialized.
There are also a number of free functions that accomplish some common tasks:
-
void allocObject(core.lang.CodeGen s, core.lang.Function ctor, core.Array<core.asm.Operand> params, core.asm.Var to)
Allocate an object on the heap. Store it in variable
to
. -
core.asm.Var allocObject(core.lang.CodeGen s, core.lang.Function ctor, core.Array<core.asm.Operand> params)
Allocate an object on the heap. Store it in variable
to
. -
core.asm.Var allocObject(core.lang.CodeGen s, core.lang.Type type)
Create an object on the heap using the default constructor.
-
core.Array<core.asm.Operand> spillRegisters(core.lang.CodeGen s, core.Array<core.asm.Operand> params)
Make sure all parameters in a parameter list reside in memory, moving any values stored in registers into memory as necessary. Returns either the original parameter list or a modified version if at least one parameter was flushed to memory.
-
core.asm.Var createFnCall(core.lang.CodeGen to, core.Array<core.lang.Value> formals, core.Array<core.asm.Operand> actuals, core.Bool copy)
Allocate and fill a
FnCall
object, copying arguments depending oncopy
.
CodeResult
The utility class core.lang.CodeResult
is a class that can be used by language
implementations to communicate the location of data during code generation. To illustrate the usage,
imagine you wish to generate code for a particular AST node. The node procudes a result of type T
.
You wish to use the result eventually, but it is not important where it is located. This wish can be
expressed as a CodeResult
and passed to the code generation function of the AST node. The AST node
inspects the wish, and updates the CodeResult
with the location of the result. The caller can then
inspect the location and use it as desired.
The benefit of this approach is that it is in most cases not important where the result from an AST node is located. So if the AST node represents a local variable, the AST node may just inform the caller that the result is located in a variable already, and thus avoids a copy if possible.
A CodeResult
can represent three different types of wishes, as indicated by the constructors:
-
init()
The caller is not interested in the result. Only evaluate the AST node for side-effects is applicable, and skip creating the result if suitable.
-
init(core.lang.Value type, core.asm.Var var)
The caller asks for a result of type
type
, and it needs to be stored in the provided variable. This means that the AST node is not able to propose another location in order to avoid copies. -
init(core.lang.Value type, core.asm.Block block)
The caller asks for a result of type
type
. The variable needs to be visible inblock
, but apart from that it does not matter which variable is used. As such, this allows the AST node to propose an already existing variable to avoid copying.
The AST node then inspects the CodeResult
instance to find out which type was asked for, and may
propose a location using the following functions:
-
core.lang.Value type()
What type was requested? If
void
then no result is required. -
core.Bool needed()
Does the caller need a result at all?
-
core.Bool suggest(core.lang.CodeGen s, core.asm.Var v)
Suggest a location for the result. Returns true if the suggestion is acceptable, otherwise, it is necessary to use the variable returned by
location
. If the suggestion is used, we assume it is already created (i.e.created
will be a no-op). -
core.Bool suggest(core.lang.CodeGen s, core.asm.Operand v)
Suggest a location for the result. Returns true if the suggestion is acceptable, otherwise, it is necessary to use the variable returned by
location
. If the suggestion is used, we assume it is already created (i.e.created
will be a no-op). -
core.asm.Var location(core.lang.CodeGen s)
Get a location to store the variable in. Asserts if the result is not needed.
-
void created(core.lang.CodeGen s)
Call when the variable was initialized to set up any cleanup required. If you are entirely sure that no cleanup is required for the contained variable, the call to
created
may be omitted. -
core.asm.Var safeLocation(core.lang.CodeGen s, core.lang.Value type)
Get the location of the result even if the result is not needed. Can be used in cases where it is not suitable to avoid creating a result even if it is not needed.
-
core.lang.CodeResult split(core.lang.CodeGen s)
Create a copy of this result that is usable when multiple pieces of code may produce the result represented by this object. Essentially, this means that cleanup will not be set up until
created
is called on this object. Thus, make sure to callcreated
here!Call
split
once for each branch that is expected to create a new value, so that each producer gets its own object. Otherwise, issues will arise when they callcreated
multiple times.
Finally, the caller may call location
to retrieve the created value.