Skip to content

Code Representation

Code representation bridges human-readable source code and executable target instructions. Compilers parse and lower source code through multiple levels of Intermediate Representation (IR) to analyze semantics and apply performance optimizations.


Abstract Syntax Tree (AST)

An Abstract Syntax Tree (AST) is a hierarchical, tree-structured representation of source code produced during syntactic analysis. It strips away purely syntactic tokens (such as semicolons, explicit parentheses, and whitespace) while retaining structural semantics, operator precedence, and parent-child execution scopes.

  • Nodes: Represent structural constructs such as operators (+, *), statements (if, while), or expressions (BinaryOp).
  • Leaves: Represent terminal operands, such as variable names (Name) or literals (Constant).
  • Usage: Used by parsers, linters, semantic analyzers, static security checkers, and source-to-source compilers.

List of Instructions

Lowering an AST into a linear List of Instructions flattens the tree structure into sequential abstract machine instructions. This flat Intermediate Representation (IR) often uses Three-Address Code (3AC) or register-based bytecode instructions where each operation has at most three operands.

  • Characteristics: Simple instruction semantics (e.g., LOAD, ADD, STORE, JUMP_IF_FALSE).
  • Usage: Simplifies register allocation, instruction selection, and target machine code emission. In Numba, Python AST is lowered into Numba IR instructions (such as jump, branch, call, and assignment operations) before being passed to LLVM.

Control Flow Graph (CFG)

A Control Flow Graph (CFG) is a directed graph $(V, E)$ that explicitly models all possible execution paths through a program unit (such as a function or subroutine).

  • Nodes ($V$): Basic Blocks containing sequential instructions.
  • Edges ($E$): Control transfers via conditional branches, unconditional jumps, or function calls.

Basic Blocks

Basic blocks are sequences of instructions with no branches except at the end. They are the building blocks of control flow graphs.

CFG is applied to basic blocks.

Direction vector (Dependence Vector)

A Direction vector is a mathematical representation of the direction of data dependencies between statements in a program. It captures the relationships between statements that read and write shared variables, indicating how data flows through the program. Each vector component corresponds to a loop level, and its value indicates the distance (in iterations) between the source and target statements.

Function compute_dependence_vector(producer_slice, consumer_slice, active_loops):
    If active_loops is empty: return None, None

    Collect all variable names present in producer and consumer slices
    Initialize distance_vector and direction_vector lists
    Is_self_dep = (producer_slice == consumer_slice)

    For each loop_var in active_loops:
        If loop_var is in subscript_vars:
            dist = 0 (if self-dep) or calculate offset difference (if inter-statement)
        Else:
            dist = 1 (indicates loop-carried recurrence)

        Append dist to distance_vector
        Determine direction symbol ('<', '>', or '=') based on dist and append to direction_vector

    Return tuple(distance_vector), tuple(direction_vector)

Example

Consider the following code snippet:

for (int i = 1; i < N; i++) {
    for (int j = 1; j < N; j++) {
        A[i][j] = A[i - 1][j + 1] + 1; // Read from (i-1, j+1), Write to (i, j)
    }
}

The direction vector for the dependency from the read at (i-1, j+1) to the write at (i, j) is (<, >), indicating that the dependency is carried across iterations of both loops.

Data Dependence Graph (DDG)

A Data Dependence Graph (DDG) is a directed graph $(V, E)$ where nodes represent program statements or instructions, and edges represent data dependencies between them. An edge from node $u$ to node $v$ indicates that $v$ depends on the output of $u

The algorithm for constructing DDG is as follows:

Function generate_ddg_with_vectors(node, active_loops=[], state=None):
    If state is None:
        Initialize state with empty DiGraph, last_def dict, last_uses dict, slice_registry, and stmt_counter = 0

    Unwrap node body (Module, list of statements, or single statement)

    For each statement in body:
        If statement is FunctionDef or AsyncFunctionDef:
            Recursively call generate_ddg_with_vectors(stmt.body, active_loops, state)
            Continue

        If statement is For Loop:
            Extract loop_var from loop target
            New_loops = active_loops + [loop_var]
            Recursively call generate_ddg_with_vectors(stmt.body, new_loops, state)
            Continue

        If statement is If or While:
            Recursively call generate_ddg_with_vectors(stmt.body / orelse, active_loops, state)
            Continue

        # Process regular statement
        idx = state.stmt_counter
        Add statement node to DDG graph with its AST and active loop nest

        Run DefUseVisitor on statement to extract defs, uses, and subscripts
        Register subscript slices for this statement index

        # 1. Check True Dependencies (RAW: Read-After-Write)
        For each variable v in statement.uses:
            If v is in state.last_def:
                prev_idx = state.last_def[v]
                Add dependence edge (prev_idx -> idx) of type 'RAW' for variable v
            Else if active_loops and v in statement.defs:
                # Self-dependence recurrence (e.g., C[i][j] += ...)
                Add dependence edge (idx -> idx) of type 'RAW' for variable v

        # 2. Check Output Dependencies (WAW: Write-After-Write)
        For each variable v in statement.defs:
            If v is in state.last_def and prev_idx != idx:
                prev_idx = state.last_def[v]
                Add dependence edge (prev_idx -> idx) of type 'WAW' for variable v
            Else if active_loops and v in statement.uses:
                Add dependence edge (idx -> idx) of type 'WAW' for variable v

        # Update tracking state
        For each variable v in statement.defs:
            state.last_def[v] = idx
        For each variable v in statement.uses:
            Add idx to state.last_uses[v]

        Increment state.stmt_counter

    Return DDG graph