Repository files navigation

iris

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

logo

Overview

IRIS is an example of IR that includes simple operations, separated into different dialects - arith, builtin and ctrlflow.

Dialect arith:

  • ADD, SUB, MUL, DIV - basic arithmetical operations;
  • CONST - operation generating a constant SSA value;
  • CAST - operation, casting values between different data types;
  • CMP - comparison operation, which takes predicate (EQ (equal), NEQ (not-equal), A (above), B (below), AE (above or equal), BE (below or equal));
  • AND, OR, XOR, NOT - bitwise operations;
  • SHL, SHR, SAL, SAR - arithmetical and logical bit-shifts

Dialect builtin:

  • PARAM - operation representing function's parameter;
  • COPY - operation that copies one SSA value into the new one.

Dialect ctrlflow:

  • CALL - routine call operation;
  • JUMP - unconditional jump;
  • JUMPC - conditional jump;
  • RETURN - return from the routine operation;
  • PHI - phi-function.

IRIS structure & examples

Here's an example of IRIS IR for a programm that calculates factorial of a number:

factorial:
^bb0 <start> <to bb1> :
a0.ui builtin.param -> (v3,v5,v7)
c1.ui arith.const (1) -> (v5,v8p)
c2.ui arith.const (2) -> (v3)
^bb1 <from bb0> <to T:bb2 / F:bb3> :
v3.b arith.cmp.b (a0 : ui, c2 : ui) -> (n4)
n4 ctrlflow.jmpc (v3 : b)
^bb3 <from bb1> <to bb2> :
v5.ui arith.sub (a0 : ui, c1 : ui) -> (v6)
v6.ui ctrlflow.call @factorial (v5 : ui) -> (v7)
v7.ui arith.mul (a0 : ui, v6 : ui) -> (v8p)
^bb2 <from bb3 bb1> <final> :
v8p.ui ctrlflow.phi (c1 : ui, v7 : ui) -> (n9)
n9 ctrlflow.return (v8p : ui)

Each operation is located in one of the basic blocks, and basic block is located in the region, which represent IR for a whole routine (function).

Function parameters are marked with literal a (a0, a1 etc.). Constant-generated SSA values are marked with literal c (c1, c2 etc.). Other SSA values are marked with literal v (v3, v4 etc.).

Operations that do not produce material result, for example, ctrlflow::jump, are marked with literal n for 'none', as its return value type is DataType::NONE.

Notice that different SSA values can have different literal preceeding its ID, but regardless of the literal every value in the region has unique ID.

Basic block properties:

  • Basic block's ID is unique among the others in the same region;
  • Each basic block has a list of predecessors (<from bb3 bb1>), or it is the starting basic block, in which case it is marked with <start>;
  • A basic block can have either zero (<final>), one (<to bb2>) or two successors (<to T:bb2 / F:bb3>);
  • Basic block cannot have a terminator operation inside of it (it must be the last operation in the basic block);
  • If operation have zero successors, its last operation must be ctrlflow::return (TODO - add check);
  • If operation have one successor, its last operation cannot be ctrlflow::jumpc;
  • If operation have two successors, its last operation must be ctrlflow::jumpc.

Operations common properties:

  • Opcode value, which is unique for every type of operation across all dialects;
  • SSA-value's ID, which is unique across whole region;
  • Return data type, which is DataType::NONE if operation does not produce any result;
  • Optional list of inputs. Some operations can have variadic number of inputs (ctrlflow::call), other have fixed number of inputs (arith::add);
  • List of users, who use this operation's result as their input/inputs.

Dominators analysis

iris::Region class provides API:

  • collectDomInfo() - collect dominators information from the graph;
  • getDFS() and getRPO() - obtain corresponding traversal orders.

iris::doms::DomInfo helper-class provides API:

  • getIdom() and getIdomByID() - get immediate dominator
  • getDominatedBlocks() and getDominatedBlocksByID() - get blocks, for which given one is immediate dominator;
  • getDominatorsChain() and getDominatorsChainByID() - get chain of immediate dominators from the given block to the starting block.

Example graph:

A → B → C → D
↓
F → E → D
↓
G → D

Example output:

========== Dom Tree ===========
[Dominator Tree]
BB#0
BB#1
BB#3
BB#5
BB#4
BB#6
BB#2
================================

Loop analysis

iris::Region class provides API:

  • collectLoopInfo() - collect loops information from the graph.

iris::loops::LoopInfo helper-class provides API:

  • getRootLoop() - get root pseudo-loop, which is parent to top-level loops;
  • getTopLevelLoops() - get top-level loops.

Class iris::loops::Loop provides methods to query:

  • Header basic block;
  • List of latches;
  • Reducibility flag;
  • Exiting edges and blocks contained inside for reducible loops;
  • List of nested loops;
  • Loop's depth;
  • Parent loop (outer).

Root loop is a special pseudo-loop, which:

  • Has no parent loop, header, latches or exiting edges;
  • Has depth 0;
  • Contains all basic blocks, which are are not contained in any other loop in the region.

Example graph:

A → B → C → D → E → F → G → I → K
↓ ↓ ↓ ↓
J → C C E H → B

Example output:

========== Loop Tree ==========
[Root Loop]
Depth: 0 | Reducible: true
Blocks (3): 8 0 10
Nested Loops:
Loop Header: 1
Depth: 1 | Reducible: true
Latches: 7
Blocks (2): 9 6
Exits:
6 -> 8
Nested Loops:
Loop Header: 4
Depth: 2 | Reducible: true
Latches: 5
Blocks (0):
Exits:
5 -> 6
Loop Header: 2
Depth: 2 | Reducible: true
Latches: 3
Blocks (0):
Exits:
3 -> 4
================================

IR Optimizations

IRIS provides a pass-based optimization framework: every optimization is a subclass of iris::opt::Pass, and a iris::opt::PassManager owns a pipeline of passes and runs them sequentially on a iris::Region. An example of an optimization pipeline:

#include<iris.hpp>voidoptimize(iris::Region& region) {
iris::opt::PassManager pm;
pm.addPass(std::make_unique<iris::opt::common::DCEPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithConstFoldPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithPeepHolePass>());
pm.run(region);
}

Available passes:

  • common::DCEPass – removes dead, side-effect-free, non-terminator operations with results that are never used (parameters are kept);
    • Example - examples/DCE;
    • Tests - tests/opt/common/dce.cpp;
  • arith::ArithConstFoldPass – folds arithmetic, bitwise and comparison operations whose operands are compile-time constants into a single arith.const;
    • Example - examples/ConstFold;
    • Tests - tests/opt/arith/constfold.cpp;
  • arith::ArithPeepHolePass – applies local algebraic/bitwise simplifications on small instruction patterns (peepholes) for arith operations;
    • Example - examples/PeepHole.cpp;
    • Tests - tests/opt/arith/peephole.cpp.

Dead Code Elimination (DCEPass)

IR before DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8,v10)
a3.ui builtin.param -> (v10)
c4.ui arith.const (10) -> (v7)
c5.ui arith.const (20) -> (v11)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
v10.ui arith.mul (a2 : ui, a3 : ui) -> (v11)
v11.ui arith.add (v10 : ui, c5 : ui) -> (v12,v12)
v12.ui arith.mul (v11 : ui, v11 : ui)
c13.ui arith.const (5) -> (v15)
c14.ui arith.const (7) -> (v15,v17)
v15.ui arith.add (c13 : ui, c14 : ui) -> (v16)
v16.ui builtin.copy (v15 : ui) -> (v17)
v17.ui arith.sub (v16 : ui, c14 : ui)
n18 ctrlflow.return (v9 : ui)

IR after DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8)
a3.ui builtin.param
c4.ui arith.const (10) -> (v7)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
n18 ctrlflow.return (v9 : ui)

See corresponding example - examples/DCE. See corresponding tests - tests/opt/common/dce.cpp.

Constant folding patterns (ArithConstFoldPass)

  • Addition: add(c1, c2) -> const(c1 + c2) for integer and floating-point types.
  • Subtraction: sub(c1, c2) -> const(c1 - c2) for integer and floating-point types.
  • Multiplication: mul(c1, c2) -> const(c1 * c2) for integer and floating-point types.
  • Division:
    • Integer: div(c1, c2) -> const(c1 / c2) for signed/unsigned integers when c2 != 0 (no folding on division by zero).
    • Floating-point: div(c1, c2) -> const(c1 / c2) using normal IEEE semantics.
  • Bitwise AND: and(c1, c2) -> const(c1 & c2) for integer types.
  • Bitwise OR: or(c1, c2) -> const(c1 | c2) for integer types.
  • Bitwise XOR: xor(c1, c2) -> const(c1 ^ c2) for integer types.
  • Shifts:
    • Signed left: sal(c, s) -> const(c << s) for signed integers.
    • Signed right: sar(c, s) -> const(c >> s) for signed integers.
    • Unsigned left: shl(c, s) -> const(c << s) for unsigned integers.
    • Unsigned right:shr(c, s) -> const(c >> s) for unsigned integers.
  • Bitwise NOT: not(c) -> const(~c) for signed and unsigned integers.
  • Comparisons (result is a boolean constant) for integer, floating-point and boolean operands:
    • cmp.eq(c1, c2) -> const(c1 == c2)
    • cmp.neq(c1, c2) -> const(c1 != c2)
    • cmp.a(c1, c2) -> const(c1 > c2)
    • cmp.b(c1, c2) -> const(c1 < c2)
    • cmp.ae(c1, c2) -> const(c1 >= c2)
    • cmp.be(c1, c2) -> const(c1 <= c2)

IR before ArithConstFoldPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param -> (v21)
c2.si arith.const (10) -> (v10,v17)
c3.si arith.const (3) -> (v10,v11,v17,v19)
c4.ui arith.const (7) -> (v12,v13,v16)
c5.ui arith.const (2) -> (v12,v16)
c6.f arith.const (1.5) -> (v14)
c7.f arith.const (0.5) -> (v14,v15)
c8.b arith.const (true) -> (v18)
c9.b arith.const (false) -> (v18)
v10.si arith.add (c2 : si, c3 : si) -> (v11)
v11.si arith.mul (v10 : si, c3 : si) -> (v20)
v12.ui arith.and (c4 : ui, c5 : ui) -> (v13)
v13.ui arith.xor (v12 : ui, c4 : ui) -> (v21)
v14.f arith.add (c6 : f, c7 : f) -> (v15)
v15.f arith.div (v14 : f, c7 : f) -> (v23)
v16.ui arith.shl (c4 : ui, c5 : ui)
v17.si arith.sar (c2 : si, c3 : si)
v18.b arith.cmp.eq (c8 : b, c9 : b) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (v11 : si, v19 : si) -> (v24)
v21.ui arith.and (v13 : ui, a1 : ui)
v22.si arith.cast (v18 : b) -> (v24)
v23.si arith.cast (v15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

IR after ArithConstFoldPass + DCEPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param
c3.si arith.const (3) -> (v19)
c11.si arith.const (39) -> (v20)
c15.f arith.const (4) -> (v23)
c18.b arith.const (false) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (c11 : si, v19 : si) -> (v24)
v22.si arith.cast (c18 : b) -> (v24)
v23.si arith.cast (c15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

See corresponding example - examples/ConstFold. See corresponding tests - tests/opt/arith/constfold.cpp.

Peephole patterns (ArithPeepHolePass):

  • add:
    • add(x, 0) -> x
    • add(0, x) -> x
  • sub:
    • sub(x, 0) -> x
    • sub(x, x) -> 0
  • mul:
    • mul(x, 1) -> x
    • mul(1, x) -> x
    • mul(x, 0) -> 0
    • mul(0, x) -> 0
  • div:
    • div(x, 1) -> x
  • and:
    • and(x, 0) -> 0
    • and(0, x) -> 0
    • and(x, 11..1) -> x
    • and(11..1, x) -> x
    • and(x, x) -> x
  • or:
    • or(x, 0) -> x
    • or(0, x) -> x
    • or(x, 11..1) -> 11..1
    • or(11..1, x) -> 11..1
    • or(x, x) -> x
  • xor:
    • xor(x, 0) -> x
    • xor(0, x) -> x
    • xor(x, x) -> 0
    • xor(x, 11..1) -> ~x
    • xor(11..1, x) -> ~x
  • Shifts (arithmetical sal/sar, logical shl/shr):
    • sal(x, 0) -> x, sal(0, x) -> 0
    • shl(x, 0) -> x, shl(0, x) -> 0
    • sar(x, 0) -> x, sar(0, x) -> 0
    • shr(x, 0) -> x, shr(0, x) -> 0
  • Double negation:
    • not(not(x)) -> x
  • To expose constant–constant sub-expressions for ArithConstFoldPass, the peephole pass applies rotate patterns to integer add/mul when an inner operation is used only by a single outer operation:
    • add:
      • (x + C1) + C2 -> x + (C1 + C2)
      • (C1 + x) + C2 -> x + (C1 + C2)
      • C2 + (x + C1) -> x + (C1 + C2)
      • C2 + (C1 + x) -> x + (C1 + C2)
    • mul:
      • (C1 * x) * C2 -> x * (C1 * C2)
      • (x * C1) * C2 -> x * (C1 * C2)
      • C2 * (C1 * x) -> x * (C1 * C2)
      • C2 * (x * C1) -> x * (C1 * C2)

IR before ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v10,v11,v12,v14,v15,v16,v18,v34,v35,v36,v37,v50)
a1.si builtin.param -> (v13,v13,v17,v54)
a2.ui builtin.param -> (v19,v20,v21,v22,v24,v25,v26,v27,v29,v30,v32,v33,v38,v39,v40,v41,v42)
a3.ui builtin.param -> (v23,v23,v28,v28,v31,v31)
c4.si arith.const (0) -> (v10,v11,v12,v16,v17,v34,v35,v36,v37)
c5.si arith.const (1) -> (v14,v15,v18)
c6.ui arith.const (0) -> (v19,v20,v24,v25,v29,v30,v38,v39,v40,v41)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615) -> (v21,v22,v26,v27,v32,v33)
v10.si arith.add (a0 : si, c4 : si) -> (v44)
v11.si arith.add (c4 : si, a0 : si)
v12.si arith.sub (a0 : si, c4 : si) -> (v44)
v13.si arith.sub (a1 : si, a1 : si)
v14.si arith.mul (a0 : si, c5 : si) -> (v45)
v15.si arith.mul (c5 : si, a0 : si)
v16.si arith.mul (a0 : si, c4 : si)
v17.si arith.mul (c4 : si, a1 : si)
v18.si arith.div (a0 : si, c5 : si) -> (v45)
v19.ui arith.and (a2 : ui, c6 : ui)
v20.ui arith.and (c6 : ui, a2 : ui)
v21.ui arith.and (a2 : ui, c9 : ui) -> (v46)
v22.ui arith.and (c9 : ui, a2 : ui)
v23.ui arith.and (a3 : ui, a3 : ui)
v24.ui arith.or (a2 : ui, c6 : ui)
v25.ui arith.or (c6 : ui, a2 : ui)
v26.ui arith.or (a2 : ui, c9 : ui) -> (v46)
v27.ui arith.or (c9 : ui, a2 : ui)
v28.ui arith.or (a3 : ui, a3 : ui)
v29.ui arith.xor (a2 : ui, c6 : ui)
v30.ui arith.xor (c6 : ui, a2 : ui)
v31.ui arith.xor (a3 : ui, a3 : ui)
v32.ui arith.xor (a2 : ui, c9 : ui)
v33.ui arith.xor (c9 : ui, a2 : ui)
v34.si arith.sal (a0 : si, c4 : si)
v35.si arith.sal (c4 : si, a0 : si)
v36.si arith.sar (a0 : si, c4 : si)
v37.si arith.sar (c4 : si, a0 : si)
v38.ui arith.shl (a2 : ui, c6 : ui)
v39.ui arith.shl (c6 : ui, a2 : ui)
v40.ui arith.shr (a2 : ui, c6 : ui)
v41.ui arith.shr (c6 : ui, a2 : ui)
v42.ui arith.not (a2 : ui) -> (v43)
v43.ui arith.not (v42 : ui) -> (n56)
v44.si arith.add (v10 : si, v12 : si) -> (v47)
v45.si arith.add (v14 : si, v18 : si) -> (v47)
v46.ui arith.and (v21 : ui, v26 : ui)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v51)
v50.si arith.add (a0 : si, c48 : si) -> (v51)
v51.si arith.add (v50 : si, c49 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v55)
v54.si arith.mul (c52 : si, a1 : si) -> (v55)
v55.si arith.mul (v54 : si, c53 : si)
n56 ctrlflow.return (v43 : ui)

IR after ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v44,v44,v45,v45,v51)
a1.si builtin.param -> (v55)
a2.ui builtin.param -> (v32,v33,n56)
a3.ui builtin.param
c4.si arith.const (0)
c5.si arith.const (1)
c6.ui arith.const (0)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615)
c13.si arith.const (0)
c16.si arith.const (0)
c17.si arith.const (0)
c19.ui arith.const (0)
c20.ui arith.const (0)
c31.ui arith.const (0)
v32.ui arith.not (a2 : ui)
v33.ui arith.not (a2 : ui)
c35.si arith.const (0)
c37.si arith.const (0)
c39.ui arith.const (0)
c41.ui arith.const (0)
v44.si arith.add (a0 : si, a0 : si) -> (v47)
v45.si arith.add (a0 : si, a0 : si) -> (v47)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v50)
v50.si arith.add (c48 : si, c49 : si) -> (v51)
v51.si arith.add (a0 : si, v50 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v54)
v54.si arith.mul (c52 : si, c53 : si) -> (v55)
v55.si arith.mul (a1 : si, v54 : si)
n56 ctrlflow.return (a2 : ui)

See corresponding example - examples/PeepHole. See corresponding tests - tests/opt/arith/peephole.cpp.

Building examples

Requirements:

  • CMake >= 3.21
  • C++ compiler supporting C++23 standard

To build project, use following commads from the project's roor directory:

cmake -B build
cmake --build build

If you want to build specific target, use following command:

cmake --build build --target <target_name>

List of available targets:

  • iris - IR-support static library;
  • factorial - example of IR usage - building IR for a programm calculating factorial manually via iris::IRBuilder;
  • domExample01 - domExample03 - examples of running Dom analysis and building IDom tree;
  • loopExample01 - loopExample06 - examples of running Loop analysis and building Loop tree;
  • DCE - example of optimizing IR with use of DCE;
  • ConstFold - example of optimizing IR with use of Constant Folding;
  • PeepHole - example of optimizing IR with use of PeepHole optimizations.

Static library output files are located in build/lib/, and executables - in build/bin.

Testing

Project includes tests, impemented using GTest.

To build and run tests, run following commands from project's root directory:

cmake -B build
cmake --build build --target iris_tests
cd build
ctest

Tests include:

  • Dominator analysis tests (tests/doms.cpp);
  • Loop analysis tests (tests/loops.cpp).
  • Separate test suites for each dialect and its operations' verifiers and construction pre-conditions (tests/dialects/);
  • Integration tests (tests/integration.cpp);
  • Optimizationpasses & patterns tests (tests/opt/):
    • Dead Code Elimination (common/dce.cpp);
    • Constant Folding for arith operations (arith/constfold.cpp);
    • PeepHole Optimizations for arith operations (arith/peephole.cpp).
  • Tests of verifiers (ex-fails) and API tests for:
    • Generic operation iris::Operation;
    • Basic block iris::BasicBlock;
    • Region iris::Region;
    • IR-builder iris::IRBuilder;
    • Dominator analysis helper class iris::doms::DomInfo;
    • Loop analysis helper class iris::doms::LoopInfo (TODO).

About

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

iris

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

logo

Overview

IRIS is an example of IR that includes simple operations, separated into different dialects - arith, builtin and ctrlflow.

Dialect arith:

  • ADD, SUB, MUL, DIV - basic arithmetical operations;
  • CONST - operation generating a constant SSA value;
  • CAST - operation, casting values between different data types;
  • CMP - comparison operation, which takes predicate (EQ (equal), NEQ (not-equal), A (above), B (below), AE (above or equal), BE (below or equal));
  • AND, OR, XOR, NOT - bitwise operations;
  • SHL, SHR, SAL, SAR - arithmetical and logical bit-shifts

Dialect builtin:

  • PARAM - operation representing function's parameter;
  • COPY - operation that copies one SSA value into the new one.

Dialect ctrlflow:

  • CALL - routine call operation;
  • JUMP - unconditional jump;
  • JUMPC - conditional jump;
  • RETURN - return from the routine operation;
  • PHI - phi-function.

IRIS structure & examples

Here's an example of IRIS IR for a programm that calculates factorial of a number:

factorial:
^bb0 <start> <to bb1> :
a0.ui builtin.param -> (v3,v5,v7)
c1.ui arith.const (1) -> (v5,v8p)
c2.ui arith.const (2) -> (v3)
^bb1 <from bb0> <to T:bb2 / F:bb3> :
v3.b arith.cmp.b (a0 : ui, c2 : ui) -> (n4)
n4 ctrlflow.jmpc (v3 : b)
^bb3 <from bb1> <to bb2> :
v5.ui arith.sub (a0 : ui, c1 : ui) -> (v6)
v6.ui ctrlflow.call @factorial (v5 : ui) -> (v7)
v7.ui arith.mul (a0 : ui, v6 : ui) -> (v8p)
^bb2 <from bb3 bb1> <final> :
v8p.ui ctrlflow.phi (c1 : ui, v7 : ui) -> (n9)
n9 ctrlflow.return (v8p : ui)

Each operation is located in one of the basic blocks, and basic block is located in the region, which represent IR for a whole routine (function).

Function parameters are marked with literal a (a0, a1 etc.). Constant-generated SSA values are marked with literal c (c1, c2 etc.). Other SSA values are marked with literal v (v3, v4 etc.).

Operations that do not produce material result, for example, ctrlflow::jump, are marked with literal n for 'none', as its return value type is DataType::NONE.

Notice that different SSA values can have different literal preceeding its ID, but regardless of the literal every value in the region has unique ID.

Basic block properties:

  • Basic block's ID is unique among the others in the same region;
  • Each basic block has a list of predecessors (<from bb3 bb1>), or it is the starting basic block, in which case it is marked with <start>;
  • A basic block can have either zero (<final>), one (<to bb2>) or two successors (<to T:bb2 / F:bb3>);
  • Basic block cannot have a terminator operation inside of it (it must be the last operation in the basic block);
  • If operation have zero successors, its last operation must be ctrlflow::return (TODO - add check);
  • If operation have one successor, its last operation cannot be ctrlflow::jumpc;
  • If operation have two successors, its last operation must be ctrlflow::jumpc.

Operations common properties:

  • Opcode value, which is unique for every type of operation across all dialects;
  • SSA-value's ID, which is unique across whole region;
  • Return data type, which is DataType::NONE if operation does not produce any result;
  • Optional list of inputs. Some operations can have variadic number of inputs (ctrlflow::call), other have fixed number of inputs (arith::add);
  • List of users, who use this operation's result as their input/inputs.

Dominators analysis

iris::Region class provides API:

  • collectDomInfo() - collect dominators information from the graph;
  • getDFS() and getRPO() - obtain corresponding traversal orders.

iris::doms::DomInfo helper-class provides API:

  • getIdom() and getIdomByID() - get immediate dominator
  • getDominatedBlocks() and getDominatedBlocksByID() - get blocks, for which given one is immediate dominator;
  • getDominatorsChain() and getDominatorsChainByID() - get chain of immediate dominators from the given block to the starting block.

Example graph:

A → B → C → D
↓
F → E → D
↓
G → D

Example output:

========== Dom Tree ===========
[Dominator Tree]
BB#0
BB#1
BB#3
BB#5
BB#4
BB#6
BB#2
================================

Loop analysis

iris::Region class provides API:

  • collectLoopInfo() - collect loops information from the graph.

iris::loops::LoopInfo helper-class provides API:

  • getRootLoop() - get root pseudo-loop, which is parent to top-level loops;
  • getTopLevelLoops() - get top-level loops.

Class iris::loops::Loop provides methods to query:

  • Header basic block;
  • List of latches;
  • Reducibility flag;
  • Exiting edges and blocks contained inside for reducible loops;
  • List of nested loops;
  • Loop's depth;
  • Parent loop (outer).

Root loop is a special pseudo-loop, which:

  • Has no parent loop, header, latches or exiting edges;
  • Has depth 0;
  • Contains all basic blocks, which are are not contained in any other loop in the region.

Example graph:

A → B → C → D → E → F → G → I → K
↓ ↓ ↓ ↓
J → C C E H → B

Example output:

========== Loop Tree ==========
[Root Loop]
Depth: 0 | Reducible: true
Blocks (3): 8 0 10
Nested Loops:
Loop Header: 1
Depth: 1 | Reducible: true
Latches: 7
Blocks (2): 9 6
Exits:
6 -> 8
Nested Loops:
Loop Header: 4
Depth: 2 | Reducible: true
Latches: 5
Blocks (0):
Exits:
5 -> 6
Loop Header: 2
Depth: 2 | Reducible: true
Latches: 3
Blocks (0):
Exits:
3 -> 4
================================

IR Optimizations

IRIS provides a pass-based optimization framework: every optimization is a subclass of iris::opt::Pass, and a iris::opt::PassManager owns a pipeline of passes and runs them sequentially on a iris::Region. An example of an optimization pipeline:

#include<iris.hpp>voidoptimize(iris::Region& region) {
iris::opt::PassManager pm;
pm.addPass(std::make_unique<iris::opt::common::DCEPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithConstFoldPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithPeepHolePass>());
pm.run(region);
}

Available passes:

  • common::DCEPass – removes dead, side-effect-free, non-terminator operations with results that are never used (parameters are kept);
    • Example - examples/DCE;
    • Tests - tests/opt/common/dce.cpp;
  • arith::ArithConstFoldPass – folds arithmetic, bitwise and comparison operations whose operands are compile-time constants into a single arith.const;
    • Example - examples/ConstFold;
    • Tests - tests/opt/arith/constfold.cpp;
  • arith::ArithPeepHolePass – applies local algebraic/bitwise simplifications on small instruction patterns (peepholes) for arith operations;
    • Example - examples/PeepHole.cpp;
    • Tests - tests/opt/arith/peephole.cpp.

Dead Code Elimination (DCEPass)

IR before DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8,v10)
a3.ui builtin.param -> (v10)
c4.ui arith.const (10) -> (v7)
c5.ui arith.const (20) -> (v11)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
v10.ui arith.mul (a2 : ui, a3 : ui) -> (v11)
v11.ui arith.add (v10 : ui, c5 : ui) -> (v12,v12)
v12.ui arith.mul (v11 : ui, v11 : ui)
c13.ui arith.const (5) -> (v15)
c14.ui arith.const (7) -> (v15,v17)
v15.ui arith.add (c13 : ui, c14 : ui) -> (v16)
v16.ui builtin.copy (v15 : ui) -> (v17)
v17.ui arith.sub (v16 : ui, c14 : ui)
n18 ctrlflow.return (v9 : ui)

IR after DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8)
a3.ui builtin.param
c4.ui arith.const (10) -> (v7)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
n18 ctrlflow.return (v9 : ui)

See corresponding example - examples/DCE. See corresponding tests - tests/opt/common/dce.cpp.

Constant folding patterns (ArithConstFoldPass)

  • Addition: add(c1, c2) -> const(c1 + c2) for integer and floating-point types.
  • Subtraction: sub(c1, c2) -> const(c1 - c2) for integer and floating-point types.
  • Multiplication: mul(c1, c2) -> const(c1 * c2) for integer and floating-point types.
  • Division:
    • Integer: div(c1, c2) -> const(c1 / c2) for signed/unsigned integers when c2 != 0 (no folding on division by zero).
    • Floating-point: div(c1, c2) -> const(c1 / c2) using normal IEEE semantics.
  • Bitwise AND: and(c1, c2) -> const(c1 & c2) for integer types.
  • Bitwise OR: or(c1, c2) -> const(c1 | c2) for integer types.
  • Bitwise XOR: xor(c1, c2) -> const(c1 ^ c2) for integer types.
  • Shifts:
    • Signed left: sal(c, s) -> const(c << s) for signed integers.
    • Signed right: sar(c, s) -> const(c >> s) for signed integers.
    • Unsigned left: shl(c, s) -> const(c << s) for unsigned integers.
    • Unsigned right:shr(c, s) -> const(c >> s) for unsigned integers.
  • Bitwise NOT: not(c) -> const(~c) for signed and unsigned integers.
  • Comparisons (result is a boolean constant) for integer, floating-point and boolean operands:
    • cmp.eq(c1, c2) -> const(c1 == c2)
    • cmp.neq(c1, c2) -> const(c1 != c2)
    • cmp.a(c1, c2) -> const(c1 > c2)
    • cmp.b(c1, c2) -> const(c1 < c2)
    • cmp.ae(c1, c2) -> const(c1 >= c2)
    • cmp.be(c1, c2) -> const(c1 <= c2)

IR before ArithConstFoldPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param -> (v21)
c2.si arith.const (10) -> (v10,v17)
c3.si arith.const (3) -> (v10,v11,v17,v19)
c4.ui arith.const (7) -> (v12,v13,v16)
c5.ui arith.const (2) -> (v12,v16)
c6.f arith.const (1.5) -> (v14)
c7.f arith.const (0.5) -> (v14,v15)
c8.b arith.const (true) -> (v18)
c9.b arith.const (false) -> (v18)
v10.si arith.add (c2 : si, c3 : si) -> (v11)
v11.si arith.mul (v10 : si, c3 : si) -> (v20)
v12.ui arith.and (c4 : ui, c5 : ui) -> (v13)
v13.ui arith.xor (v12 : ui, c4 : ui) -> (v21)
v14.f arith.add (c6 : f, c7 : f) -> (v15)
v15.f arith.div (v14 : f, c7 : f) -> (v23)
v16.ui arith.shl (c4 : ui, c5 : ui)
v17.si arith.sar (c2 : si, c3 : si)
v18.b arith.cmp.eq (c8 : b, c9 : b) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (v11 : si, v19 : si) -> (v24)
v21.ui arith.and (v13 : ui, a1 : ui)
v22.si arith.cast (v18 : b) -> (v24)
v23.si arith.cast (v15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

IR after ArithConstFoldPass + DCEPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param
c3.si arith.const (3) -> (v19)
c11.si arith.const (39) -> (v20)
c15.f arith.const (4) -> (v23)
c18.b arith.const (false) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (c11 : si, v19 : si) -> (v24)
v22.si arith.cast (c18 : b) -> (v24)
v23.si arith.cast (c15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

See corresponding example - examples/ConstFold. See corresponding tests - tests/opt/arith/constfold.cpp.

Peephole patterns (ArithPeepHolePass):

  • add:
    • add(x, 0) -> x
    • add(0, x) -> x
  • sub:
    • sub(x, 0) -> x
    • sub(x, x) -> 0
  • mul:
    • mul(x, 1) -> x
    • mul(1, x) -> x
    • mul(x, 0) -> 0
    • mul(0, x) -> 0
  • div:
    • div(x, 1) -> x
  • and:
    • and(x, 0) -> 0
    • and(0, x) -> 0
    • and(x, 11..1) -> x
    • and(11..1, x) -> x
    • and(x, x) -> x
  • or:
    • or(x, 0) -> x
    • or(0, x) -> x
    • or(x, 11..1) -> 11..1
    • or(11..1, x) -> 11..1
    • or(x, x) -> x
  • xor:
    • xor(x, 0) -> x
    • xor(0, x) -> x
    • xor(x, x) -> 0
    • xor(x, 11..1) -> ~x
    • xor(11..1, x) -> ~x
  • Shifts (arithmetical sal/sar, logical shl/shr):
    • sal(x, 0) -> x, sal(0, x) -> 0
    • shl(x, 0) -> x, shl(0, x) -> 0
    • sar(x, 0) -> x, sar(0, x) -> 0
    • shr(x, 0) -> x, shr(0, x) -> 0
  • Double negation:
    • not(not(x)) -> x
  • To expose constant–constant sub-expressions for ArithConstFoldPass, the peephole pass applies rotate patterns to integer add/mul when an inner operation is used only by a single outer operation:
    • add:
      • (x + C1) + C2 -> x + (C1 + C2)
      • (C1 + x) + C2 -> x + (C1 + C2)
      • C2 + (x + C1) -> x + (C1 + C2)
      • C2 + (C1 + x) -> x + (C1 + C2)
    • mul:
      • (C1 * x) * C2 -> x * (C1 * C2)
      • (x * C1) * C2 -> x * (C1 * C2)
      • C2 * (C1 * x) -> x * (C1 * C2)
      • C2 * (x * C1) -> x * (C1 * C2)

IR before ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v10,v11,v12,v14,v15,v16,v18,v34,v35,v36,v37,v50)
a1.si builtin.param -> (v13,v13,v17,v54)
a2.ui builtin.param -> (v19,v20,v21,v22,v24,v25,v26,v27,v29,v30,v32,v33,v38,v39,v40,v41,v42)
a3.ui builtin.param -> (v23,v23,v28,v28,v31,v31)
c4.si arith.const (0) -> (v10,v11,v12,v16,v17,v34,v35,v36,v37)
c5.si arith.const (1) -> (v14,v15,v18)
c6.ui arith.const (0) -> (v19,v20,v24,v25,v29,v30,v38,v39,v40,v41)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615) -> (v21,v22,v26,v27,v32,v33)
v10.si arith.add (a0 : si, c4 : si) -> (v44)
v11.si arith.add (c4 : si, a0 : si)
v12.si arith.sub (a0 : si, c4 : si) -> (v44)
v13.si arith.sub (a1 : si, a1 : si)
v14.si arith.mul (a0 : si, c5 : si) -> (v45)
v15.si arith.mul (c5 : si, a0 : si)
v16.si arith.mul (a0 : si, c4 : si)
v17.si arith.mul (c4 : si, a1 : si)
v18.si arith.div (a0 : si, c5 : si) -> (v45)
v19.ui arith.and (a2 : ui, c6 : ui)
v20.ui arith.and (c6 : ui, a2 : ui)
v21.ui arith.and (a2 : ui, c9 : ui) -> (v46)
v22.ui arith.and (c9 : ui, a2 : ui)
v23.ui arith.and (a3 : ui, a3 : ui)
v24.ui arith.or (a2 : ui, c6 : ui)
v25.ui arith.or (c6 : ui, a2 : ui)
v26.ui arith.or (a2 : ui, c9 : ui) -> (v46)
v27.ui arith.or (c9 : ui, a2 : ui)
v28.ui arith.or (a3 : ui, a3 : ui)
v29.ui arith.xor (a2 : ui, c6 : ui)
v30.ui arith.xor (c6 : ui, a2 : ui)
v31.ui arith.xor (a3 : ui, a3 : ui)
v32.ui arith.xor (a2 : ui, c9 : ui)
v33.ui arith.xor (c9 : ui, a2 : ui)
v34.si arith.sal (a0 : si, c4 : si)
v35.si arith.sal (c4 : si, a0 : si)
v36.si arith.sar (a0 : si, c4 : si)
v37.si arith.sar (c4 : si, a0 : si)
v38.ui arith.shl (a2 : ui, c6 : ui)
v39.ui arith.shl (c6 : ui, a2 : ui)
v40.ui arith.shr (a2 : ui, c6 : ui)
v41.ui arith.shr (c6 : ui, a2 : ui)
v42.ui arith.not (a2 : ui) -> (v43)
v43.ui arith.not (v42 : ui) -> (n56)
v44.si arith.add (v10 : si, v12 : si) -> (v47)
v45.si arith.add (v14 : si, v18 : si) -> (v47)
v46.ui arith.and (v21 : ui, v26 : ui)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v51)
v50.si arith.add (a0 : si, c48 : si) -> (v51)
v51.si arith.add (v50 : si, c49 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v55)
v54.si arith.mul (c52 : si, a1 : si) -> (v55)
v55.si arith.mul (v54 : si, c53 : si)
n56 ctrlflow.return (v43 : ui)

IR after ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v44,v44,v45,v45,v51)
a1.si builtin.param -> (v55)
a2.ui builtin.param -> (v32,v33,n56)
a3.ui builtin.param
c4.si arith.const (0)
c5.si arith.const (1)
c6.ui arith.const (0)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615)
c13.si arith.const (0)
c16.si arith.const (0)
c17.si arith.const (0)
c19.ui arith.const (0)
c20.ui arith.const (0)
c31.ui arith.const (0)
v32.ui arith.not (a2 : ui)
v33.ui arith.not (a2 : ui)
c35.si arith.const (0)
c37.si arith.const (0)
c39.ui arith.const (0)
c41.ui arith.const (0)
v44.si arith.add (a0 : si, a0 : si) -> (v47)
v45.si arith.add (a0 : si, a0 : si) -> (v47)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v50)
v50.si arith.add (c48 : si, c49 : si) -> (v51)
v51.si arith.add (a0 : si, v50 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v54)
v54.si arith.mul (c52 : si, c53 : si) -> (v55)
v55.si arith.mul (a1 : si, v54 : si)
n56 ctrlflow.return (a2 : ui)

See corresponding example - examples/PeepHole. See corresponding tests - tests/opt/arith/peephole.cpp.

Building examples

Requirements:

  • CMake >= 3.21
  • C++ compiler supporting C++23 standard

To build project, use following commads from the project's roor directory:

cmake -B build
cmake --build build

If you want to build specific target, use following command:

cmake --build build --target <target_name>

List of available targets:

  • iris - IR-support static library;
  • factorial - example of IR usage - building IR for a programm calculating factorial manually via iris::IRBuilder;
  • domExample01 - domExample03 - examples of running Dom analysis and building IDom tree;
  • loopExample01 - loopExample06 - examples of running Loop analysis and building Loop tree;
  • DCE - example of optimizing IR with use of DCE;
  • ConstFold - example of optimizing IR with use of Constant Folding;
  • PeepHole - example of optimizing IR with use of PeepHole optimizations.

Static library output files are located in build/lib/, and executables - in build/bin.

Testing

Project includes tests, impemented using GTest.

To build and run tests, run following commands from project's root directory:

cmake -B build
cmake --build build --target iris_tests
cd build
ctest

Tests include:

  • Dominator analysis tests (tests/doms.cpp);
  • Loop analysis tests (tests/loops.cpp).
  • Separate test suites for each dialect and its operations' verifiers and construction pre-conditions (tests/dialects/);
  • Integration tests (tests/integration.cpp);
  • Optimizationpasses & patterns tests (tests/opt/):
    • Dead Code Elimination (common/dce.cpp);
    • Constant Folding for arith operations (arith/constfold.cpp);
    • PeepHole Optimizations for arith operations (arith/peephole.cpp).
  • Tests of verifiers (ex-fails) and API tests for:
    • Generic operation iris::Operation;
    • Basic block iris::BasicBlock;
    • Region iris::Region;
    • IR-builder iris::IRBuilder;
    • Dominator analysis helper class iris::doms::DomInfo;
    • Loop analysis helper class iris::doms::LoopInfo (TODO).

About

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

iris

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

logo

Overview

IRIS is an example of IR that includes simple operations, separated into different dialects - arith, builtin and ctrlflow.

Dialect arith:

  • ADD, SUB, MUL, DIV - basic arithmetical operations;
  • CONST - operation generating a constant SSA value;
  • CAST - operation, casting values between different data types;
  • CMP - comparison operation, which takes predicate (EQ (equal), NEQ (not-equal), A (above), B (below), AE (above or equal), BE (below or equal));
  • AND, OR, XOR, NOT - bitwise operations;
  • SHL, SHR, SAL, SAR - arithmetical and logical bit-shifts

Dialect builtin:

  • PARAM - operation representing function's parameter;
  • COPY - operation that copies one SSA value into the new one.

Dialect ctrlflow:

  • CALL - routine call operation;
  • JUMP - unconditional jump;
  • JUMPC - conditional jump;
  • RETURN - return from the routine operation;
  • PHI - phi-function.

IRIS structure & examples

Here's an example of IRIS IR for a programm that calculates factorial of a number:

factorial:
^bb0 <start> <to bb1> :
a0.ui builtin.param -> (v3,v5,v7)
c1.ui arith.const (1) -> (v5,v8p)
c2.ui arith.const (2) -> (v3)
^bb1 <from bb0> <to T:bb2 / F:bb3> :
v3.b arith.cmp.b (a0 : ui, c2 : ui) -> (n4)
n4 ctrlflow.jmpc (v3 : b)
^bb3 <from bb1> <to bb2> :
v5.ui arith.sub (a0 : ui, c1 : ui) -> (v6)
v6.ui ctrlflow.call @factorial (v5 : ui) -> (v7)
v7.ui arith.mul (a0 : ui, v6 : ui) -> (v8p)
^bb2 <from bb3 bb1> <final> :
v8p.ui ctrlflow.phi (c1 : ui, v7 : ui) -> (n9)
n9 ctrlflow.return (v8p : ui)

Each operation is located in one of the basic blocks, and basic block is located in the region, which represent IR for a whole routine (function).

Function parameters are marked with literal a (a0, a1 etc.). Constant-generated SSA values are marked with literal c (c1, c2 etc.). Other SSA values are marked with literal v (v3, v4 etc.).

Operations that do not produce material result, for example, ctrlflow::jump, are marked with literal n for 'none', as its return value type is DataType::NONE.

Notice that different SSA values can have different literal preceeding its ID, but regardless of the literal every value in the region has unique ID.

Basic block properties:

  • Basic block's ID is unique among the others in the same region;
  • Each basic block has a list of predecessors (<from bb3 bb1>), or it is the starting basic block, in which case it is marked with <start>;
  • A basic block can have either zero (<final>), one (<to bb2>) or two successors (<to T:bb2 / F:bb3>);
  • Basic block cannot have a terminator operation inside of it (it must be the last operation in the basic block);
  • If operation have zero successors, its last operation must be ctrlflow::return (TODO - add check);
  • If operation have one successor, its last operation cannot be ctrlflow::jumpc;
  • If operation have two successors, its last operation must be ctrlflow::jumpc.

Operations common properties:

  • Opcode value, which is unique for every type of operation across all dialects;
  • SSA-value's ID, which is unique across whole region;
  • Return data type, which is DataType::NONE if operation does not produce any result;
  • Optional list of inputs. Some operations can have variadic number of inputs (ctrlflow::call), other have fixed number of inputs (arith::add);
  • List of users, who use this operation's result as their input/inputs.

Dominators analysis

iris::Region class provides API:

  • collectDomInfo() - collect dominators information from the graph;
  • getDFS() and getRPO() - obtain corresponding traversal orders.

iris::doms::DomInfo helper-class provides API:

  • getIdom() and getIdomByID() - get immediate dominator
  • getDominatedBlocks() and getDominatedBlocksByID() - get blocks, for which given one is immediate dominator;
  • getDominatorsChain() and getDominatorsChainByID() - get chain of immediate dominators from the given block to the starting block.

Example graph:

A → B → C → D
↓
F → E → D
↓
G → D

Example output:

========== Dom Tree ===========
[Dominator Tree]
BB#0
BB#1
BB#3
BB#5
BB#4
BB#6
BB#2
================================

Loop analysis

iris::Region class provides API:

  • collectLoopInfo() - collect loops information from the graph.

iris::loops::LoopInfo helper-class provides API:

  • getRootLoop() - get root pseudo-loop, which is parent to top-level loops;
  • getTopLevelLoops() - get top-level loops.

Class iris::loops::Loop provides methods to query:

  • Header basic block;
  • List of latches;
  • Reducibility flag;
  • Exiting edges and blocks contained inside for reducible loops;
  • List of nested loops;
  • Loop's depth;
  • Parent loop (outer).

Root loop is a special pseudo-loop, which:

  • Has no parent loop, header, latches or exiting edges;
  • Has depth 0;
  • Contains all basic blocks, which are are not contained in any other loop in the region.

Example graph:

A → B → C → D → E → F → G → I → K
↓ ↓ ↓ ↓
J → C C E H → B

Example output:

========== Loop Tree ==========
[Root Loop]
Depth: 0 | Reducible: true
Blocks (3): 8 0 10
Nested Loops:
Loop Header: 1
Depth: 1 | Reducible: true
Latches: 7
Blocks (2): 9 6
Exits:
6 -> 8
Nested Loops:
Loop Header: 4
Depth: 2 | Reducible: true
Latches: 5
Blocks (0):
Exits:
5 -> 6
Loop Header: 2
Depth: 2 | Reducible: true
Latches: 3
Blocks (0):
Exits:
3 -> 4
================================

IR Optimizations

IRIS provides a pass-based optimization framework: every optimization is a subclass of iris::opt::Pass, and a iris::opt::PassManager owns a pipeline of passes and runs them sequentially on a iris::Region. An example of an optimization pipeline:

#include<iris.hpp>voidoptimize(iris::Region& region) {
iris::opt::PassManager pm;
pm.addPass(std::make_unique<iris::opt::common::DCEPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithConstFoldPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithPeepHolePass>());
pm.run(region);
}

Available passes:

  • common::DCEPass – removes dead, side-effect-free, non-terminator operations with results that are never used (parameters are kept);
    • Example - examples/DCE;
    • Tests - tests/opt/common/dce.cpp;
  • arith::ArithConstFoldPass – folds arithmetic, bitwise and comparison operations whose operands are compile-time constants into a single arith.const;
    • Example - examples/ConstFold;
    • Tests - tests/opt/arith/constfold.cpp;
  • arith::ArithPeepHolePass – applies local algebraic/bitwise simplifications on small instruction patterns (peepholes) for arith operations;
    • Example - examples/PeepHole.cpp;
    • Tests - tests/opt/arith/peephole.cpp.

Dead Code Elimination (DCEPass)

IR before DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8,v10)
a3.ui builtin.param -> (v10)
c4.ui arith.const (10) -> (v7)
c5.ui arith.const (20) -> (v11)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
v10.ui arith.mul (a2 : ui, a3 : ui) -> (v11)
v11.ui arith.add (v10 : ui, c5 : ui) -> (v12,v12)
v12.ui arith.mul (v11 : ui, v11 : ui)
c13.ui arith.const (5) -> (v15)
c14.ui arith.const (7) -> (v15,v17)
v15.ui arith.add (c13 : ui, c14 : ui) -> (v16)
v16.ui builtin.copy (v15 : ui) -> (v17)
v17.ui arith.sub (v16 : ui, c14 : ui)
n18 ctrlflow.return (v9 : ui)

IR after DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8)
a3.ui builtin.param
c4.ui arith.const (10) -> (v7)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
n18 ctrlflow.return (v9 : ui)

See corresponding example - examples/DCE. See corresponding tests - tests/opt/common/dce.cpp.

Constant folding patterns (ArithConstFoldPass)

  • Addition: add(c1, c2) -> const(c1 + c2) for integer and floating-point types.
  • Subtraction: sub(c1, c2) -> const(c1 - c2) for integer and floating-point types.
  • Multiplication: mul(c1, c2) -> const(c1 * c2) for integer and floating-point types.
  • Division:
    • Integer: div(c1, c2) -> const(c1 / c2) for signed/unsigned integers when c2 != 0 (no folding on division by zero).
    • Floating-point: div(c1, c2) -> const(c1 / c2) using normal IEEE semantics.
  • Bitwise AND: and(c1, c2) -> const(c1 & c2) for integer types.
  • Bitwise OR: or(c1, c2) -> const(c1 | c2) for integer types.
  • Bitwise XOR: xor(c1, c2) -> const(c1 ^ c2) for integer types.
  • Shifts:
    • Signed left: sal(c, s) -> const(c << s) for signed integers.
    • Signed right: sar(c, s) -> const(c >> s) for signed integers.
    • Unsigned left: shl(c, s) -> const(c << s) for unsigned integers.
    • Unsigned right:shr(c, s) -> const(c >> s) for unsigned integers.
  • Bitwise NOT: not(c) -> const(~c) for signed and unsigned integers.
  • Comparisons (result is a boolean constant) for integer, floating-point and boolean operands:
    • cmp.eq(c1, c2) -> const(c1 == c2)
    • cmp.neq(c1, c2) -> const(c1 != c2)
    • cmp.a(c1, c2) -> const(c1 > c2)
    • cmp.b(c1, c2) -> const(c1 < c2)
    • cmp.ae(c1, c2) -> const(c1 >= c2)
    • cmp.be(c1, c2) -> const(c1 <= c2)

IR before ArithConstFoldPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param -> (v21)
c2.si arith.const (10) -> (v10,v17)
c3.si arith.const (3) -> (v10,v11,v17,v19)
c4.ui arith.const (7) -> (v12,v13,v16)
c5.ui arith.const (2) -> (v12,v16)
c6.f arith.const (1.5) -> (v14)
c7.f arith.const (0.5) -> (v14,v15)
c8.b arith.const (true) -> (v18)
c9.b arith.const (false) -> (v18)
v10.si arith.add (c2 : si, c3 : si) -> (v11)
v11.si arith.mul (v10 : si, c3 : si) -> (v20)
v12.ui arith.and (c4 : ui, c5 : ui) -> (v13)
v13.ui arith.xor (v12 : ui, c4 : ui) -> (v21)
v14.f arith.add (c6 : f, c7 : f) -> (v15)
v15.f arith.div (v14 : f, c7 : f) -> (v23)
v16.ui arith.shl (c4 : ui, c5 : ui)
v17.si arith.sar (c2 : si, c3 : si)
v18.b arith.cmp.eq (c8 : b, c9 : b) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (v11 : si, v19 : si) -> (v24)
v21.ui arith.and (v13 : ui, a1 : ui)
v22.si arith.cast (v18 : b) -> (v24)
v23.si arith.cast (v15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

IR after ArithConstFoldPass + DCEPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param
c3.si arith.const (3) -> (v19)
c11.si arith.const (39) -> (v20)
c15.f arith.const (4) -> (v23)
c18.b arith.const (false) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (c11 : si, v19 : si) -> (v24)
v22.si arith.cast (c18 : b) -> (v24)
v23.si arith.cast (c15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

See corresponding example - examples/ConstFold. See corresponding tests - tests/opt/arith/constfold.cpp.

Peephole patterns (ArithPeepHolePass):

  • add:
    • add(x, 0) -> x
    • add(0, x) -> x
  • sub:
    • sub(x, 0) -> x
    • sub(x, x) -> 0
  • mul:
    • mul(x, 1) -> x
    • mul(1, x) -> x
    • mul(x, 0) -> 0
    • mul(0, x) -> 0
  • div:
    • div(x, 1) -> x
  • and:
    • and(x, 0) -> 0
    • and(0, x) -> 0
    • and(x, 11..1) -> x
    • and(11..1, x) -> x
    • and(x, x) -> x
  • or:
    • or(x, 0) -> x
    • or(0, x) -> x
    • or(x, 11..1) -> 11..1
    • or(11..1, x) -> 11..1
    • or(x, x) -> x
  • xor:
    • xor(x, 0) -> x
    • xor(0, x) -> x
    • xor(x, x) -> 0
    • xor(x, 11..1) -> ~x
    • xor(11..1, x) -> ~x
  • Shifts (arithmetical sal/sar, logical shl/shr):
    • sal(x, 0) -> x, sal(0, x) -> 0
    • shl(x, 0) -> x, shl(0, x) -> 0
    • sar(x, 0) -> x, sar(0, x) -> 0
    • shr(x, 0) -> x, shr(0, x) -> 0
  • Double negation:
    • not(not(x)) -> x
  • To expose constant–constant sub-expressions for ArithConstFoldPass, the peephole pass applies rotate patterns to integer add/mul when an inner operation is used only by a single outer operation:
    • add:
      • (x + C1) + C2 -> x + (C1 + C2)
      • (C1 + x) + C2 -> x + (C1 + C2)
      • C2 + (x + C1) -> x + (C1 + C2)
      • C2 + (C1 + x) -> x + (C1 + C2)
    • mul:
      • (C1 * x) * C2 -> x * (C1 * C2)
      • (x * C1) * C2 -> x * (C1 * C2)
      • C2 * (C1 * x) -> x * (C1 * C2)
      • C2 * (x * C1) -> x * (C1 * C2)

IR before ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v10,v11,v12,v14,v15,v16,v18,v34,v35,v36,v37,v50)
a1.si builtin.param -> (v13,v13,v17,v54)
a2.ui builtin.param -> (v19,v20,v21,v22,v24,v25,v26,v27,v29,v30,v32,v33,v38,v39,v40,v41,v42)
a3.ui builtin.param -> (v23,v23,v28,v28,v31,v31)
c4.si arith.const (0) -> (v10,v11,v12,v16,v17,v34,v35,v36,v37)
c5.si arith.const (1) -> (v14,v15,v18)
c6.ui arith.const (0) -> (v19,v20,v24,v25,v29,v30,v38,v39,v40,v41)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615) -> (v21,v22,v26,v27,v32,v33)
v10.si arith.add (a0 : si, c4 : si) -> (v44)
v11.si arith.add (c4 : si, a0 : si)
v12.si arith.sub (a0 : si, c4 : si) -> (v44)
v13.si arith.sub (a1 : si, a1 : si)
v14.si arith.mul (a0 : si, c5 : si) -> (v45)
v15.si arith.mul (c5 : si, a0 : si)
v16.si arith.mul (a0 : si, c4 : si)
v17.si arith.mul (c4 : si, a1 : si)
v18.si arith.div (a0 : si, c5 : si) -> (v45)
v19.ui arith.and (a2 : ui, c6 : ui)
v20.ui arith.and (c6 : ui, a2 : ui)
v21.ui arith.and (a2 : ui, c9 : ui) -> (v46)
v22.ui arith.and (c9 : ui, a2 : ui)
v23.ui arith.and (a3 : ui, a3 : ui)
v24.ui arith.or (a2 : ui, c6 : ui)
v25.ui arith.or (c6 : ui, a2 : ui)
v26.ui arith.or (a2 : ui, c9 : ui) -> (v46)
v27.ui arith.or (c9 : ui, a2 : ui)
v28.ui arith.or (a3 : ui, a3 : ui)
v29.ui arith.xor (a2 : ui, c6 : ui)
v30.ui arith.xor (c6 : ui, a2 : ui)
v31.ui arith.xor (a3 : ui, a3 : ui)
v32.ui arith.xor (a2 : ui, c9 : ui)
v33.ui arith.xor (c9 : ui, a2 : ui)
v34.si arith.sal (a0 : si, c4 : si)
v35.si arith.sal (c4 : si, a0 : si)
v36.si arith.sar (a0 : si, c4 : si)
v37.si arith.sar (c4 : si, a0 : si)
v38.ui arith.shl (a2 : ui, c6 : ui)
v39.ui arith.shl (c6 : ui, a2 : ui)
v40.ui arith.shr (a2 : ui, c6 : ui)
v41.ui arith.shr (c6 : ui, a2 : ui)
v42.ui arith.not (a2 : ui) -> (v43)
v43.ui arith.not (v42 : ui) -> (n56)
v44.si arith.add (v10 : si, v12 : si) -> (v47)
v45.si arith.add (v14 : si, v18 : si) -> (v47)
v46.ui arith.and (v21 : ui, v26 : ui)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v51)
v50.si arith.add (a0 : si, c48 : si) -> (v51)
v51.si arith.add (v50 : si, c49 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v55)
v54.si arith.mul (c52 : si, a1 : si) -> (v55)
v55.si arith.mul (v54 : si, c53 : si)
n56 ctrlflow.return (v43 : ui)

IR after ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v44,v44,v45,v45,v51)
a1.si builtin.param -> (v55)
a2.ui builtin.param -> (v32,v33,n56)
a3.ui builtin.param
c4.si arith.const (0)
c5.si arith.const (1)
c6.ui arith.const (0)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615)
c13.si arith.const (0)
c16.si arith.const (0)
c17.si arith.const (0)
c19.ui arith.const (0)
c20.ui arith.const (0)
c31.ui arith.const (0)
v32.ui arith.not (a2 : ui)
v33.ui arith.not (a2 : ui)
c35.si arith.const (0)
c37.si arith.const (0)
c39.ui arith.const (0)
c41.ui arith.const (0)
v44.si arith.add (a0 : si, a0 : si) -> (v47)
v45.si arith.add (a0 : si, a0 : si) -> (v47)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v50)
v50.si arith.add (c48 : si, c49 : si) -> (v51)
v51.si arith.add (a0 : si, v50 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v54)
v54.si arith.mul (c52 : si, c53 : si) -> (v55)
v55.si arith.mul (a1 : si, v54 : si)
n56 ctrlflow.return (a2 : ui)

See corresponding example - examples/PeepHole. See corresponding tests - tests/opt/arith/peephole.cpp.

Building examples

Requirements:

  • CMake >= 3.21
  • C++ compiler supporting C++23 standard

To build project, use following commads from the project's roor directory:

cmake -B build
cmake --build build

If you want to build specific target, use following command:

cmake --build build --target <target_name>

List of available targets:

  • iris - IR-support static library;
  • factorial - example of IR usage - building IR for a programm calculating factorial manually via iris::IRBuilder;
  • domExample01 - domExample03 - examples of running Dom analysis and building IDom tree;
  • loopExample01 - loopExample06 - examples of running Loop analysis and building Loop tree;
  • DCE - example of optimizing IR with use of DCE;
  • ConstFold - example of optimizing IR with use of Constant Folding;
  • PeepHole - example of optimizing IR with use of PeepHole optimizations.

Static library output files are located in build/lib/, and executables - in build/bin.

Testing

Project includes tests, impemented using GTest.

To build and run tests, run following commands from project's root directory:

cmake -B build
cmake --build build --target iris_tests
cd build
ctest

Tests include:

  • Dominator analysis tests (tests/doms.cpp);
  • Loop analysis tests (tests/loops.cpp).
  • Separate test suites for each dialect and its operations' verifiers and construction pre-conditions (tests/dialects/);
  • Integration tests (tests/integration.cpp);
  • Optimizationpasses & patterns tests (tests/opt/):
    • Dead Code Elimination (common/dce.cpp);
    • Constant Folding for arith operations (arith/constfold.cpp);
    • PeepHole Optimizations for arith operations (arith/peephole.cpp).
  • Tests of verifiers (ex-fails) and API tests for:
    • Generic operation iris::Operation;
    • Basic block iris::BasicBlock;
    • Region iris::Region;
    • IR-builder iris::IRBuilder;
    • Dominator analysis helper class iris::doms::DomInfo;
    • Loop analysis helper class iris::doms::LoopInfo (TODO).

About

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

iris

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

logo

Overview

IRIS is an example of IR that includes simple operations, separated into different dialects - arith, builtin and ctrlflow.

Dialect arith:

  • ADD, SUB, MUL, DIV - basic arithmetical operations;
  • CONST - operation generating a constant SSA value;
  • CAST - operation, casting values between different data types;
  • CMP - comparison operation, which takes predicate (EQ (equal), NEQ (not-equal), A (above), B (below), AE (above or equal), BE (below or equal));
  • AND, OR, XOR, NOT - bitwise operations;
  • SHL, SHR, SAL, SAR - arithmetical and logical bit-shifts

Dialect builtin:

  • PARAM - operation representing function's parameter;
  • COPY - operation that copies one SSA value into the new one.

Dialect ctrlflow:

  • CALL - routine call operation;
  • JUMP - unconditional jump;
  • JUMPC - conditional jump;
  • RETURN - return from the routine operation;
  • PHI - phi-function.

IRIS structure & examples

Here's an example of IRIS IR for a programm that calculates factorial of a number:

factorial:
^bb0 <start> <to bb1> :
a0.ui builtin.param -> (v3,v5,v7)
c1.ui arith.const (1) -> (v5,v8p)
c2.ui arith.const (2) -> (v3)
^bb1 <from bb0> <to T:bb2 / F:bb3> :
v3.b arith.cmp.b (a0 : ui, c2 : ui) -> (n4)
n4 ctrlflow.jmpc (v3 : b)
^bb3 <from bb1> <to bb2> :
v5.ui arith.sub (a0 : ui, c1 : ui) -> (v6)
v6.ui ctrlflow.call @factorial (v5 : ui) -> (v7)
v7.ui arith.mul (a0 : ui, v6 : ui) -> (v8p)
^bb2 <from bb3 bb1> <final> :
v8p.ui ctrlflow.phi (c1 : ui, v7 : ui) -> (n9)
n9 ctrlflow.return (v8p : ui)

Each operation is located in one of the basic blocks, and basic block is located in the region, which represent IR for a whole routine (function).

Function parameters are marked with literal a (a0, a1 etc.). Constant-generated SSA values are marked with literal c (c1, c2 etc.). Other SSA values are marked with literal v (v3, v4 etc.).

Operations that do not produce material result, for example, ctrlflow::jump, are marked with literal n for 'none', as its return value type is DataType::NONE.

Notice that different SSA values can have different literal preceeding its ID, but regardless of the literal every value in the region has unique ID.

Basic block properties:

  • Basic block's ID is unique among the others in the same region;
  • Each basic block has a list of predecessors (<from bb3 bb1>), or it is the starting basic block, in which case it is marked with <start>;
  • A basic block can have either zero (<final>), one (<to bb2>) or two successors (<to T:bb2 / F:bb3>);
  • Basic block cannot have a terminator operation inside of it (it must be the last operation in the basic block);
  • If operation have zero successors, its last operation must be ctrlflow::return (TODO - add check);
  • If operation have one successor, its last operation cannot be ctrlflow::jumpc;
  • If operation have two successors, its last operation must be ctrlflow::jumpc.

Operations common properties:

  • Opcode value, which is unique for every type of operation across all dialects;
  • SSA-value's ID, which is unique across whole region;
  • Return data type, which is DataType::NONE if operation does not produce any result;
  • Optional list of inputs. Some operations can have variadic number of inputs (ctrlflow::call), other have fixed number of inputs (arith::add);
  • List of users, who use this operation's result as their input/inputs.

Dominators analysis

iris::Region class provides API:

  • collectDomInfo() - collect dominators information from the graph;
  • getDFS() and getRPO() - obtain corresponding traversal orders.

iris::doms::DomInfo helper-class provides API:

  • getIdom() and getIdomByID() - get immediate dominator
  • getDominatedBlocks() and getDominatedBlocksByID() - get blocks, for which given one is immediate dominator;
  • getDominatorsChain() and getDominatorsChainByID() - get chain of immediate dominators from the given block to the starting block.

Example graph:

A → B → C → D
↓
F → E → D
↓
G → D

Example output:

========== Dom Tree ===========
[Dominator Tree]
BB#0
BB#1
BB#3
BB#5
BB#4
BB#6
BB#2
================================

Loop analysis

iris::Region class provides API:

  • collectLoopInfo() - collect loops information from the graph.

iris::loops::LoopInfo helper-class provides API:

  • getRootLoop() - get root pseudo-loop, which is parent to top-level loops;
  • getTopLevelLoops() - get top-level loops.

Class iris::loops::Loop provides methods to query:

  • Header basic block;
  • List of latches;
  • Reducibility flag;
  • Exiting edges and blocks contained inside for reducible loops;
  • List of nested loops;
  • Loop's depth;
  • Parent loop (outer).

Root loop is a special pseudo-loop, which:

  • Has no parent loop, header, latches or exiting edges;
  • Has depth 0;
  • Contains all basic blocks, which are are not contained in any other loop in the region.

Example graph:

A → B → C → D → E → F → G → I → K
↓ ↓ ↓ ↓
J → C C E H → B

Example output:

========== Loop Tree ==========
[Root Loop]
Depth: 0 | Reducible: true
Blocks (3): 8 0 10
Nested Loops:
Loop Header: 1
Depth: 1 | Reducible: true
Latches: 7
Blocks (2): 9 6
Exits:
6 -> 8
Nested Loops:
Loop Header: 4
Depth: 2 | Reducible: true
Latches: 5
Blocks (0):
Exits:
5 -> 6
Loop Header: 2
Depth: 2 | Reducible: true
Latches: 3
Blocks (0):
Exits:
3 -> 4
================================

IR Optimizations

IRIS provides a pass-based optimization framework: every optimization is a subclass of iris::opt::Pass, and a iris::opt::PassManager owns a pipeline of passes and runs them sequentially on a iris::Region. An example of an optimization pipeline:

#include<iris.hpp>voidoptimize(iris::Region& region) {
iris::opt::PassManager pm;
pm.addPass(std::make_unique<iris::opt::common::DCEPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithConstFoldPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithPeepHolePass>());
pm.run(region);
}

Available passes:

  • common::DCEPass – removes dead, side-effect-free, non-terminator operations with results that are never used (parameters are kept);
    • Example - examples/DCE;
    • Tests - tests/opt/common/dce.cpp;
  • arith::ArithConstFoldPass – folds arithmetic, bitwise and comparison operations whose operands are compile-time constants into a single arith.const;
    • Example - examples/ConstFold;
    • Tests - tests/opt/arith/constfold.cpp;
  • arith::ArithPeepHolePass – applies local algebraic/bitwise simplifications on small instruction patterns (peepholes) for arith operations;
    • Example - examples/PeepHole.cpp;
    • Tests - tests/opt/arith/peephole.cpp.

Dead Code Elimination (DCEPass)

IR before DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8,v10)
a3.ui builtin.param -> (v10)
c4.ui arith.const (10) -> (v7)
c5.ui arith.const (20) -> (v11)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
v10.ui arith.mul (a2 : ui, a3 : ui) -> (v11)
v11.ui arith.add (v10 : ui, c5 : ui) -> (v12,v12)
v12.ui arith.mul (v11 : ui, v11 : ui)
c13.ui arith.const (5) -> (v15)
c14.ui arith.const (7) -> (v15,v17)
v15.ui arith.add (c13 : ui, c14 : ui) -> (v16)
v16.ui builtin.copy (v15 : ui) -> (v17)
v17.ui arith.sub (v16 : ui, c14 : ui)
n18 ctrlflow.return (v9 : ui)

IR after DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8)
a3.ui builtin.param
c4.ui arith.const (10) -> (v7)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
n18 ctrlflow.return (v9 : ui)

See corresponding example - examples/DCE. See corresponding tests - tests/opt/common/dce.cpp.

Constant folding patterns (ArithConstFoldPass)

  • Addition: add(c1, c2) -> const(c1 + c2) for integer and floating-point types.
  • Subtraction: sub(c1, c2) -> const(c1 - c2) for integer and floating-point types.
  • Multiplication: mul(c1, c2) -> const(c1 * c2) for integer and floating-point types.
  • Division:
    • Integer: div(c1, c2) -> const(c1 / c2) for signed/unsigned integers when c2 != 0 (no folding on division by zero).
    • Floating-point: div(c1, c2) -> const(c1 / c2) using normal IEEE semantics.
  • Bitwise AND: and(c1, c2) -> const(c1 & c2) for integer types.
  • Bitwise OR: or(c1, c2) -> const(c1 | c2) for integer types.
  • Bitwise XOR: xor(c1, c2) -> const(c1 ^ c2) for integer types.
  • Shifts:
    • Signed left: sal(c, s) -> const(c << s) for signed integers.
    • Signed right: sar(c, s) -> const(c >> s) for signed integers.
    • Unsigned left: shl(c, s) -> const(c << s) for unsigned integers.
    • Unsigned right:shr(c, s) -> const(c >> s) for unsigned integers.
  • Bitwise NOT: not(c) -> const(~c) for signed and unsigned integers.
  • Comparisons (result is a boolean constant) for integer, floating-point and boolean operands:
    • cmp.eq(c1, c2) -> const(c1 == c2)
    • cmp.neq(c1, c2) -> const(c1 != c2)
    • cmp.a(c1, c2) -> const(c1 > c2)
    • cmp.b(c1, c2) -> const(c1 < c2)
    • cmp.ae(c1, c2) -> const(c1 >= c2)
    • cmp.be(c1, c2) -> const(c1 <= c2)

IR before ArithConstFoldPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param -> (v21)
c2.si arith.const (10) -> (v10,v17)
c3.si arith.const (3) -> (v10,v11,v17,v19)
c4.ui arith.const (7) -> (v12,v13,v16)
c5.ui arith.const (2) -> (v12,v16)
c6.f arith.const (1.5) -> (v14)
c7.f arith.const (0.5) -> (v14,v15)
c8.b arith.const (true) -> (v18)
c9.b arith.const (false) -> (v18)
v10.si arith.add (c2 : si, c3 : si) -> (v11)
v11.si arith.mul (v10 : si, c3 : si) -> (v20)
v12.ui arith.and (c4 : ui, c5 : ui) -> (v13)
v13.ui arith.xor (v12 : ui, c4 : ui) -> (v21)
v14.f arith.add (c6 : f, c7 : f) -> (v15)
v15.f arith.div (v14 : f, c7 : f) -> (v23)
v16.ui arith.shl (c4 : ui, c5 : ui)
v17.si arith.sar (c2 : si, c3 : si)
v18.b arith.cmp.eq (c8 : b, c9 : b) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (v11 : si, v19 : si) -> (v24)
v21.ui arith.and (v13 : ui, a1 : ui)
v22.si arith.cast (v18 : b) -> (v24)
v23.si arith.cast (v15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

IR after ArithConstFoldPass + DCEPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param
c3.si arith.const (3) -> (v19)
c11.si arith.const (39) -> (v20)
c15.f arith.const (4) -> (v23)
c18.b arith.const (false) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (c11 : si, v19 : si) -> (v24)
v22.si arith.cast (c18 : b) -> (v24)
v23.si arith.cast (c15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

See corresponding example - examples/ConstFold. See corresponding tests - tests/opt/arith/constfold.cpp.

Peephole patterns (ArithPeepHolePass):

  • add:
    • add(x, 0) -> x
    • add(0, x) -> x
  • sub:
    • sub(x, 0) -> x
    • sub(x, x) -> 0
  • mul:
    • mul(x, 1) -> x
    • mul(1, x) -> x
    • mul(x, 0) -> 0
    • mul(0, x) -> 0
  • div:
    • div(x, 1) -> x
  • and:
    • and(x, 0) -> 0
    • and(0, x) -> 0
    • and(x, 11..1) -> x
    • and(11..1, x) -> x
    • and(x, x) -> x
  • or:
    • or(x, 0) -> x
    • or(0, x) -> x
    • or(x, 11..1) -> 11..1
    • or(11..1, x) -> 11..1
    • or(x, x) -> x
  • xor:
    • xor(x, 0) -> x
    • xor(0, x) -> x
    • xor(x, x) -> 0
    • xor(x, 11..1) -> ~x
    • xor(11..1, x) -> ~x
  • Shifts (arithmetical sal/sar, logical shl/shr):
    • sal(x, 0) -> x, sal(0, x) -> 0
    • shl(x, 0) -> x, shl(0, x) -> 0
    • sar(x, 0) -> x, sar(0, x) -> 0
    • shr(x, 0) -> x, shr(0, x) -> 0
  • Double negation:
    • not(not(x)) -> x
  • To expose constant–constant sub-expressions for ArithConstFoldPass, the peephole pass applies rotate patterns to integer add/mul when an inner operation is used only by a single outer operation:
    • add:
      • (x + C1) + C2 -> x + (C1 + C2)
      • (C1 + x) + C2 -> x + (C1 + C2)
      • C2 + (x + C1) -> x + (C1 + C2)
      • C2 + (C1 + x) -> x + (C1 + C2)
    • mul:
      • (C1 * x) * C2 -> x * (C1 * C2)
      • (x * C1) * C2 -> x * (C1 * C2)
      • C2 * (C1 * x) -> x * (C1 * C2)
      • C2 * (x * C1) -> x * (C1 * C2)

IR before ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v10,v11,v12,v14,v15,v16,v18,v34,v35,v36,v37,v50)
a1.si builtin.param -> (v13,v13,v17,v54)
a2.ui builtin.param -> (v19,v20,v21,v22,v24,v25,v26,v27,v29,v30,v32,v33,v38,v39,v40,v41,v42)
a3.ui builtin.param -> (v23,v23,v28,v28,v31,v31)
c4.si arith.const (0) -> (v10,v11,v12,v16,v17,v34,v35,v36,v37)
c5.si arith.const (1) -> (v14,v15,v18)
c6.ui arith.const (0) -> (v19,v20,v24,v25,v29,v30,v38,v39,v40,v41)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615) -> (v21,v22,v26,v27,v32,v33)
v10.si arith.add (a0 : si, c4 : si) -> (v44)
v11.si arith.add (c4 : si, a0 : si)
v12.si arith.sub (a0 : si, c4 : si) -> (v44)
v13.si arith.sub (a1 : si, a1 : si)
v14.si arith.mul (a0 : si, c5 : si) -> (v45)
v15.si arith.mul (c5 : si, a0 : si)
v16.si arith.mul (a0 : si, c4 : si)
v17.si arith.mul (c4 : si, a1 : si)
v18.si arith.div (a0 : si, c5 : si) -> (v45)
v19.ui arith.and (a2 : ui, c6 : ui)
v20.ui arith.and (c6 : ui, a2 : ui)
v21.ui arith.and (a2 : ui, c9 : ui) -> (v46)
v22.ui arith.and (c9 : ui, a2 : ui)
v23.ui arith.and (a3 : ui, a3 : ui)
v24.ui arith.or (a2 : ui, c6 : ui)
v25.ui arith.or (c6 : ui, a2 : ui)
v26.ui arith.or (a2 : ui, c9 : ui) -> (v46)
v27.ui arith.or (c9 : ui, a2 : ui)
v28.ui arith.or (a3 : ui, a3 : ui)
v29.ui arith.xor (a2 : ui, c6 : ui)
v30.ui arith.xor (c6 : ui, a2 : ui)
v31.ui arith.xor (a3 : ui, a3 : ui)
v32.ui arith.xor (a2 : ui, c9 : ui)
v33.ui arith.xor (c9 : ui, a2 : ui)
v34.si arith.sal (a0 : si, c4 : si)
v35.si arith.sal (c4 : si, a0 : si)
v36.si arith.sar (a0 : si, c4 : si)
v37.si arith.sar (c4 : si, a0 : si)
v38.ui arith.shl (a2 : ui, c6 : ui)
v39.ui arith.shl (c6 : ui, a2 : ui)
v40.ui arith.shr (a2 : ui, c6 : ui)
v41.ui arith.shr (c6 : ui, a2 : ui)
v42.ui arith.not (a2 : ui) -> (v43)
v43.ui arith.not (v42 : ui) -> (n56)
v44.si arith.add (v10 : si, v12 : si) -> (v47)
v45.si arith.add (v14 : si, v18 : si) -> (v47)
v46.ui arith.and (v21 : ui, v26 : ui)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v51)
v50.si arith.add (a0 : si, c48 : si) -> (v51)
v51.si arith.add (v50 : si, c49 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v55)
v54.si arith.mul (c52 : si, a1 : si) -> (v55)
v55.si arith.mul (v54 : si, c53 : si)
n56 ctrlflow.return (v43 : ui)

IR after ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v44,v44,v45,v45,v51)
a1.si builtin.param -> (v55)
a2.ui builtin.param -> (v32,v33,n56)
a3.ui builtin.param
c4.si arith.const (0)
c5.si arith.const (1)
c6.ui arith.const (0)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615)
c13.si arith.const (0)
c16.si arith.const (0)
c17.si arith.const (0)
c19.ui arith.const (0)
c20.ui arith.const (0)
c31.ui arith.const (0)
v32.ui arith.not (a2 : ui)
v33.ui arith.not (a2 : ui)
c35.si arith.const (0)
c37.si arith.const (0)
c39.ui arith.const (0)
c41.ui arith.const (0)
v44.si arith.add (a0 : si, a0 : si) -> (v47)
v45.si arith.add (a0 : si, a0 : si) -> (v47)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v50)
v50.si arith.add (c48 : si, c49 : si) -> (v51)
v51.si arith.add (a0 : si, v50 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v54)
v54.si arith.mul (c52 : si, c53 : si) -> (v55)
v55.si arith.mul (a1 : si, v54 : si)
n56 ctrlflow.return (a2 : ui)

See corresponding example - examples/PeepHole. See corresponding tests - tests/opt/arith/peephole.cpp.

Building examples

Requirements:

  • CMake >= 3.21
  • C++ compiler supporting C++23 standard

To build project, use following commads from the project's roor directory:

cmake -B build
cmake --build build

If you want to build specific target, use following command:

cmake --build build --target <target_name>

List of available targets:

  • iris - IR-support static library;
  • factorial - example of IR usage - building IR for a programm calculating factorial manually via iris::IRBuilder;
  • domExample01 - domExample03 - examples of running Dom analysis and building IDom tree;
  • loopExample01 - loopExample06 - examples of running Loop analysis and building Loop tree;
  • DCE - example of optimizing IR with use of DCE;
  • ConstFold - example of optimizing IR with use of Constant Folding;
  • PeepHole - example of optimizing IR with use of PeepHole optimizations.

Static library output files are located in build/lib/, and executables - in build/bin.

Testing

Project includes tests, impemented using GTest.

To build and run tests, run following commands from project's root directory:

cmake -B build
cmake --build build --target iris_tests
cd build
ctest

Tests include:

  • Dominator analysis tests (tests/doms.cpp);
  • Loop analysis tests (tests/loops.cpp).
  • Separate test suites for each dialect and its operations' verifiers and construction pre-conditions (tests/dialects/);
  • Integration tests (tests/integration.cpp);
  • Optimizationpasses & patterns tests (tests/opt/):
    • Dead Code Elimination (common/dce.cpp);
    • Constant Folding for arith operations (arith/constfold.cpp);
    • PeepHole Optimizations for arith operations (arith/peephole.cpp).
  • Tests of verifiers (ex-fails) and API tests for:
    • Generic operation iris::Operation;
    • Basic block iris::BasicBlock;
    • Region iris::Region;
    • IR-builder iris::IRBuilder;
    • Dominator analysis helper class iris::doms::DomInfo;
    • Loop analysis helper class iris::doms::LoopInfo (TODO).

About

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

iris

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

logo

Overview

IRIS is an example of IR that includes simple operations, separated into different dialects - arith, builtin and ctrlflow.

Dialect arith:

  • ADD, SUB, MUL, DIV - basic arithmetical operations;
  • CONST - operation generating a constant SSA value;
  • CAST - operation, casting values between different data types;
  • CMP - comparison operation, which takes predicate (EQ (equal), NEQ (not-equal), A (above), B (below), AE (above or equal), BE (below or equal));
  • AND, OR, XOR, NOT - bitwise operations;
  • SHL, SHR, SAL, SAR - arithmetical and logical bit-shifts

Dialect builtin:

  • PARAM - operation representing function's parameter;
  • COPY - operation that copies one SSA value into the new one.

Dialect ctrlflow:

  • CALL - routine call operation;
  • JUMP - unconditional jump;
  • JUMPC - conditional jump;
  • RETURN - return from the routine operation;
  • PHI - phi-function.

IRIS structure & examples

Here's an example of IRIS IR for a programm that calculates factorial of a number:

factorial:
^bb0 <start> <to bb1> :
a0.ui builtin.param -> (v3,v5,v7)
c1.ui arith.const (1) -> (v5,v8p)
c2.ui arith.const (2) -> (v3)
^bb1 <from bb0> <to T:bb2 / F:bb3> :
v3.b arith.cmp.b (a0 : ui, c2 : ui) -> (n4)
n4 ctrlflow.jmpc (v3 : b)
^bb3 <from bb1> <to bb2> :
v5.ui arith.sub (a0 : ui, c1 : ui) -> (v6)
v6.ui ctrlflow.call @factorial (v5 : ui) -> (v7)
v7.ui arith.mul (a0 : ui, v6 : ui) -> (v8p)
^bb2 <from bb3 bb1> <final> :
v8p.ui ctrlflow.phi (c1 : ui, v7 : ui) -> (n9)
n9 ctrlflow.return (v8p : ui)

Each operation is located in one of the basic blocks, and basic block is located in the region, which represent IR for a whole routine (function).

Function parameters are marked with literal a (a0, a1 etc.). Constant-generated SSA values are marked with literal c (c1, c2 etc.). Other SSA values are marked with literal v (v3, v4 etc.).

Operations that do not produce material result, for example, ctrlflow::jump, are marked with literal n for 'none', as its return value type is DataType::NONE.

Notice that different SSA values can have different literal preceeding its ID, but regardless of the literal every value in the region has unique ID.

Basic block properties:

  • Basic block's ID is unique among the others in the same region;
  • Each basic block has a list of predecessors (<from bb3 bb1>), or it is the starting basic block, in which case it is marked with <start>;
  • A basic block can have either zero (<final>), one (<to bb2>) or two successors (<to T:bb2 / F:bb3>);
  • Basic block cannot have a terminator operation inside of it (it must be the last operation in the basic block);
  • If operation have zero successors, its last operation must be ctrlflow::return (TODO - add check);
  • If operation have one successor, its last operation cannot be ctrlflow::jumpc;
  • If operation have two successors, its last operation must be ctrlflow::jumpc.

Operations common properties:

  • Opcode value, which is unique for every type of operation across all dialects;
  • SSA-value's ID, which is unique across whole region;
  • Return data type, which is DataType::NONE if operation does not produce any result;
  • Optional list of inputs. Some operations can have variadic number of inputs (ctrlflow::call), other have fixed number of inputs (arith::add);
  • List of users, who use this operation's result as their input/inputs.

Dominators analysis

iris::Region class provides API:

  • collectDomInfo() - collect dominators information from the graph;
  • getDFS() and getRPO() - obtain corresponding traversal orders.

iris::doms::DomInfo helper-class provides API:

  • getIdom() and getIdomByID() - get immediate dominator
  • getDominatedBlocks() and getDominatedBlocksByID() - get blocks, for which given one is immediate dominator;
  • getDominatorsChain() and getDominatorsChainByID() - get chain of immediate dominators from the given block to the starting block.

Example graph:

A → B → C → D
↓
F → E → D
↓
G → D

Example output:

========== Dom Tree ===========
[Dominator Tree]
BB#0
BB#1
BB#3
BB#5
BB#4
BB#6
BB#2
================================

Loop analysis

iris::Region class provides API:

  • collectLoopInfo() - collect loops information from the graph.

iris::loops::LoopInfo helper-class provides API:

  • getRootLoop() - get root pseudo-loop, which is parent to top-level loops;
  • getTopLevelLoops() - get top-level loops.

Class iris::loops::Loop provides methods to query:

  • Header basic block;
  • List of latches;
  • Reducibility flag;
  • Exiting edges and blocks contained inside for reducible loops;
  • List of nested loops;
  • Loop's depth;
  • Parent loop (outer).

Root loop is a special pseudo-loop, which:

  • Has no parent loop, header, latches or exiting edges;
  • Has depth 0;
  • Contains all basic blocks, which are are not contained in any other loop in the region.

Example graph:

A → B → C → D → E → F → G → I → K
↓ ↓ ↓ ↓
J → C C E H → B

Example output:

========== Loop Tree ==========
[Root Loop]
Depth: 0 | Reducible: true
Blocks (3): 8 0 10
Nested Loops:
Loop Header: 1
Depth: 1 | Reducible: true
Latches: 7
Blocks (2): 9 6
Exits:
6 -> 8
Nested Loops:
Loop Header: 4
Depth: 2 | Reducible: true
Latches: 5
Blocks (0):
Exits:
5 -> 6
Loop Header: 2
Depth: 2 | Reducible: true
Latches: 3
Blocks (0):
Exits:
3 -> 4
================================

IR Optimizations

IRIS provides a pass-based optimization framework: every optimization is a subclass of iris::opt::Pass, and a iris::opt::PassManager owns a pipeline of passes and runs them sequentially on a iris::Region. An example of an optimization pipeline:

#include<iris.hpp>voidoptimize(iris::Region& region) {
iris::opt::PassManager pm;
pm.addPass(std::make_unique<iris::opt::common::DCEPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithConstFoldPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithPeepHolePass>());
pm.run(region);
}

Available passes:

  • common::DCEPass – removes dead, side-effect-free, non-terminator operations with results that are never used (parameters are kept);
    • Example - examples/DCE;
    • Tests - tests/opt/common/dce.cpp;
  • arith::ArithConstFoldPass – folds arithmetic, bitwise and comparison operations whose operands are compile-time constants into a single arith.const;
    • Example - examples/ConstFold;
    • Tests - tests/opt/arith/constfold.cpp;
  • arith::ArithPeepHolePass – applies local algebraic/bitwise simplifications on small instruction patterns (peepholes) for arith operations;
    • Example - examples/PeepHole.cpp;
    • Tests - tests/opt/arith/peephole.cpp.

Dead Code Elimination (DCEPass)

IR before DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8,v10)
a3.ui builtin.param -> (v10)
c4.ui arith.const (10) -> (v7)
c5.ui arith.const (20) -> (v11)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
v10.ui arith.mul (a2 : ui, a3 : ui) -> (v11)
v11.ui arith.add (v10 : ui, c5 : ui) -> (v12,v12)
v12.ui arith.mul (v11 : ui, v11 : ui)
c13.ui arith.const (5) -> (v15)
c14.ui arith.const (7) -> (v15,v17)
v15.ui arith.add (c13 : ui, c14 : ui) -> (v16)
v16.ui builtin.copy (v15 : ui) -> (v17)
v17.ui arith.sub (v16 : ui, c14 : ui)
n18 ctrlflow.return (v9 : ui)

IR after DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8)
a3.ui builtin.param
c4.ui arith.const (10) -> (v7)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
n18 ctrlflow.return (v9 : ui)

See corresponding example - examples/DCE. See corresponding tests - tests/opt/common/dce.cpp.

Constant folding patterns (ArithConstFoldPass)

  • Addition: add(c1, c2) -> const(c1 + c2) for integer and floating-point types.
  • Subtraction: sub(c1, c2) -> const(c1 - c2) for integer and floating-point types.
  • Multiplication: mul(c1, c2) -> const(c1 * c2) for integer and floating-point types.
  • Division:
    • Integer: div(c1, c2) -> const(c1 / c2) for signed/unsigned integers when c2 != 0 (no folding on division by zero).
    • Floating-point: div(c1, c2) -> const(c1 / c2) using normal IEEE semantics.
  • Bitwise AND: and(c1, c2) -> const(c1 & c2) for integer types.
  • Bitwise OR: or(c1, c2) -> const(c1 | c2) for integer types.
  • Bitwise XOR: xor(c1, c2) -> const(c1 ^ c2) for integer types.
  • Shifts:
    • Signed left: sal(c, s) -> const(c << s) for signed integers.
    • Signed right: sar(c, s) -> const(c >> s) for signed integers.
    • Unsigned left: shl(c, s) -> const(c << s) for unsigned integers.
    • Unsigned right:shr(c, s) -> const(c >> s) for unsigned integers.
  • Bitwise NOT: not(c) -> const(~c) for signed and unsigned integers.
  • Comparisons (result is a boolean constant) for integer, floating-point and boolean operands:
    • cmp.eq(c1, c2) -> const(c1 == c2)
    • cmp.neq(c1, c2) -> const(c1 != c2)
    • cmp.a(c1, c2) -> const(c1 > c2)
    • cmp.b(c1, c2) -> const(c1 < c2)
    • cmp.ae(c1, c2) -> const(c1 >= c2)
    • cmp.be(c1, c2) -> const(c1 <= c2)

IR before ArithConstFoldPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param -> (v21)
c2.si arith.const (10) -> (v10,v17)
c3.si arith.const (3) -> (v10,v11,v17,v19)
c4.ui arith.const (7) -> (v12,v13,v16)
c5.ui arith.const (2) -> (v12,v16)
c6.f arith.const (1.5) -> (v14)
c7.f arith.const (0.5) -> (v14,v15)
c8.b arith.const (true) -> (v18)
c9.b arith.const (false) -> (v18)
v10.si arith.add (c2 : si, c3 : si) -> (v11)
v11.si arith.mul (v10 : si, c3 : si) -> (v20)
v12.ui arith.and (c4 : ui, c5 : ui) -> (v13)
v13.ui arith.xor (v12 : ui, c4 : ui) -> (v21)
v14.f arith.add (c6 : f, c7 : f) -> (v15)
v15.f arith.div (v14 : f, c7 : f) -> (v23)
v16.ui arith.shl (c4 : ui, c5 : ui)
v17.si arith.sar (c2 : si, c3 : si)
v18.b arith.cmp.eq (c8 : b, c9 : b) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (v11 : si, v19 : si) -> (v24)
v21.ui arith.and (v13 : ui, a1 : ui)
v22.si arith.cast (v18 : b) -> (v24)
v23.si arith.cast (v15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

IR after ArithConstFoldPass + DCEPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param
c3.si arith.const (3) -> (v19)
c11.si arith.const (39) -> (v20)
c15.f arith.const (4) -> (v23)
c18.b arith.const (false) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (c11 : si, v19 : si) -> (v24)
v22.si arith.cast (c18 : b) -> (v24)
v23.si arith.cast (c15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

See corresponding example - examples/ConstFold. See corresponding tests - tests/opt/arith/constfold.cpp.

Peephole patterns (ArithPeepHolePass):

  • add:
    • add(x, 0) -> x
    • add(0, x) -> x
  • sub:
    • sub(x, 0) -> x
    • sub(x, x) -> 0
  • mul:
    • mul(x, 1) -> x
    • mul(1, x) -> x
    • mul(x, 0) -> 0
    • mul(0, x) -> 0
  • div:
    • div(x, 1) -> x
  • and:
    • and(x, 0) -> 0
    • and(0, x) -> 0
    • and(x, 11..1) -> x
    • and(11..1, x) -> x
    • and(x, x) -> x
  • or:
    • or(x, 0) -> x
    • or(0, x) -> x
    • or(x, 11..1) -> 11..1
    • or(11..1, x) -> 11..1
    • or(x, x) -> x
  • xor:
    • xor(x, 0) -> x
    • xor(0, x) -> x
    • xor(x, x) -> 0
    • xor(x, 11..1) -> ~x
    • xor(11..1, x) -> ~x
  • Shifts (arithmetical sal/sar, logical shl/shr):
    • sal(x, 0) -> x, sal(0, x) -> 0
    • shl(x, 0) -> x, shl(0, x) -> 0
    • sar(x, 0) -> x, sar(0, x) -> 0
    • shr(x, 0) -> x, shr(0, x) -> 0
  • Double negation:
    • not(not(x)) -> x
  • To expose constant–constant sub-expressions for ArithConstFoldPass, the peephole pass applies rotate patterns to integer add/mul when an inner operation is used only by a single outer operation:
    • add:
      • (x + C1) + C2 -> x + (C1 + C2)
      • (C1 + x) + C2 -> x + (C1 + C2)
      • C2 + (x + C1) -> x + (C1 + C2)
      • C2 + (C1 + x) -> x + (C1 + C2)
    • mul:
      • (C1 * x) * C2 -> x * (C1 * C2)
      • (x * C1) * C2 -> x * (C1 * C2)
      • C2 * (C1 * x) -> x * (C1 * C2)
      • C2 * (x * C1) -> x * (C1 * C2)

IR before ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v10,v11,v12,v14,v15,v16,v18,v34,v35,v36,v37,v50)
a1.si builtin.param -> (v13,v13,v17,v54)
a2.ui builtin.param -> (v19,v20,v21,v22,v24,v25,v26,v27,v29,v30,v32,v33,v38,v39,v40,v41,v42)
a3.ui builtin.param -> (v23,v23,v28,v28,v31,v31)
c4.si arith.const (0) -> (v10,v11,v12,v16,v17,v34,v35,v36,v37)
c5.si arith.const (1) -> (v14,v15,v18)
c6.ui arith.const (0) -> (v19,v20,v24,v25,v29,v30,v38,v39,v40,v41)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615) -> (v21,v22,v26,v27,v32,v33)
v10.si arith.add (a0 : si, c4 : si) -> (v44)
v11.si arith.add (c4 : si, a0 : si)
v12.si arith.sub (a0 : si, c4 : si) -> (v44)
v13.si arith.sub (a1 : si, a1 : si)
v14.si arith.mul (a0 : si, c5 : si) -> (v45)
v15.si arith.mul (c5 : si, a0 : si)
v16.si arith.mul (a0 : si, c4 : si)
v17.si arith.mul (c4 : si, a1 : si)
v18.si arith.div (a0 : si, c5 : si) -> (v45)
v19.ui arith.and (a2 : ui, c6 : ui)
v20.ui arith.and (c6 : ui, a2 : ui)
v21.ui arith.and (a2 : ui, c9 : ui) -> (v46)
v22.ui arith.and (c9 : ui, a2 : ui)
v23.ui arith.and (a3 : ui, a3 : ui)
v24.ui arith.or (a2 : ui, c6 : ui)
v25.ui arith.or (c6 : ui, a2 : ui)
v26.ui arith.or (a2 : ui, c9 : ui) -> (v46)
v27.ui arith.or (c9 : ui, a2 : ui)
v28.ui arith.or (a3 : ui, a3 : ui)
v29.ui arith.xor (a2 : ui, c6 : ui)
v30.ui arith.xor (c6 : ui, a2 : ui)
v31.ui arith.xor (a3 : ui, a3 : ui)
v32.ui arith.xor (a2 : ui, c9 : ui)
v33.ui arith.xor (c9 : ui, a2 : ui)
v34.si arith.sal (a0 : si, c4 : si)
v35.si arith.sal (c4 : si, a0 : si)
v36.si arith.sar (a0 : si, c4 : si)
v37.si arith.sar (c4 : si, a0 : si)
v38.ui arith.shl (a2 : ui, c6 : ui)
v39.ui arith.shl (c6 : ui, a2 : ui)
v40.ui arith.shr (a2 : ui, c6 : ui)
v41.ui arith.shr (c6 : ui, a2 : ui)
v42.ui arith.not (a2 : ui) -> (v43)
v43.ui arith.not (v42 : ui) -> (n56)
v44.si arith.add (v10 : si, v12 : si) -> (v47)
v45.si arith.add (v14 : si, v18 : si) -> (v47)
v46.ui arith.and (v21 : ui, v26 : ui)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v51)
v50.si arith.add (a0 : si, c48 : si) -> (v51)
v51.si arith.add (v50 : si, c49 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v55)
v54.si arith.mul (c52 : si, a1 : si) -> (v55)
v55.si arith.mul (v54 : si, c53 : si)
n56 ctrlflow.return (v43 : ui)

IR after ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v44,v44,v45,v45,v51)
a1.si builtin.param -> (v55)
a2.ui builtin.param -> (v32,v33,n56)
a3.ui builtin.param
c4.si arith.const (0)
c5.si arith.const (1)
c6.ui arith.const (0)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615)
c13.si arith.const (0)
c16.si arith.const (0)
c17.si arith.const (0)
c19.ui arith.const (0)
c20.ui arith.const (0)
c31.ui arith.const (0)
v32.ui arith.not (a2 : ui)
v33.ui arith.not (a2 : ui)
c35.si arith.const (0)
c37.si arith.const (0)
c39.ui arith.const (0)
c41.ui arith.const (0)
v44.si arith.add (a0 : si, a0 : si) -> (v47)
v45.si arith.add (a0 : si, a0 : si) -> (v47)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v50)
v50.si arith.add (c48 : si, c49 : si) -> (v51)
v51.si arith.add (a0 : si, v50 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v54)
v54.si arith.mul (c52 : si, c53 : si) -> (v55)
v55.si arith.mul (a1 : si, v54 : si)
n56 ctrlflow.return (a2 : ui)

See corresponding example - examples/PeepHole. See corresponding tests - tests/opt/arith/peephole.cpp.

Building examples

Requirements:

  • CMake >= 3.21
  • C++ compiler supporting C++23 standard

To build project, use following commads from the project's roor directory:

cmake -B build
cmake --build build

If you want to build specific target, use following command:

cmake --build build --target <target_name>

List of available targets:

  • iris - IR-support static library;
  • factorial - example of IR usage - building IR for a programm calculating factorial manually via iris::IRBuilder;
  • domExample01 - domExample03 - examples of running Dom analysis and building IDom tree;
  • loopExample01 - loopExample06 - examples of running Loop analysis and building Loop tree;
  • DCE - example of optimizing IR with use of DCE;
  • ConstFold - example of optimizing IR with use of Constant Folding;
  • PeepHole - example of optimizing IR with use of PeepHole optimizations.

Static library output files are located in build/lib/, and executables - in build/bin.

Testing

Project includes tests, impemented using GTest.

To build and run tests, run following commands from project's root directory:

cmake -B build
cmake --build build --target iris_tests
cd build
ctest

Tests include:

  • Dominator analysis tests (tests/doms.cpp);
  • Loop analysis tests (tests/loops.cpp).
  • Separate test suites for each dialect and its operations' verifiers and construction pre-conditions (tests/dialects/);
  • Integration tests (tests/integration.cpp);
  • Optimizationpasses & patterns tests (tests/opt/):
    • Dead Code Elimination (common/dce.cpp);
    • Constant Folding for arith operations (arith/constfold.cpp);
    • PeepHole Optimizations for arith operations (arith/peephole.cpp).
  • Tests of verifiers (ex-fails) and API tests for:
    • Generic operation iris::Operation;
    • Basic block iris::BasicBlock;
    • Region iris::Region;
    • IR-builder iris::IRBuilder;
    • Dominator analysis helper class iris::doms::DomInfo;
    • Loop analysis helper class iris::doms::LoopInfo (TODO).

About

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

iris

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

logo

Overview

IRIS is an example of IR that includes simple operations, separated into different dialects - arith, builtin and ctrlflow.

Dialect arith:

  • ADD, SUB, MUL, DIV - basic arithmetical operations;
  • CONST - operation generating a constant SSA value;
  • CAST - operation, casting values between different data types;
  • CMP - comparison operation, which takes predicate (EQ (equal), NEQ (not-equal), A (above), B (below), AE (above or equal), BE (below or equal));
  • AND, OR, XOR, NOT - bitwise operations;
  • SHL, SHR, SAL, SAR - arithmetical and logical bit-shifts

Dialect builtin:

  • PARAM - operation representing function's parameter;
  • COPY - operation that copies one SSA value into the new one.

Dialect ctrlflow:

  • CALL - routine call operation;
  • JUMP - unconditional jump;
  • JUMPC - conditional jump;
  • RETURN - return from the routine operation;
  • PHI - phi-function.

IRIS structure & examples

Here's an example of IRIS IR for a programm that calculates factorial of a number:

factorial:
^bb0 <start> <to bb1> :
a0.ui builtin.param -> (v3,v5,v7)
c1.ui arith.const (1) -> (v5,v8p)
c2.ui arith.const (2) -> (v3)
^bb1 <from bb0> <to T:bb2 / F:bb3> :
v3.b arith.cmp.b (a0 : ui, c2 : ui) -> (n4)
n4 ctrlflow.jmpc (v3 : b)
^bb3 <from bb1> <to bb2> :
v5.ui arith.sub (a0 : ui, c1 : ui) -> (v6)
v6.ui ctrlflow.call @factorial (v5 : ui) -> (v7)
v7.ui arith.mul (a0 : ui, v6 : ui) -> (v8p)
^bb2 <from bb3 bb1> <final> :
v8p.ui ctrlflow.phi (c1 : ui, v7 : ui) -> (n9)
n9 ctrlflow.return (v8p : ui)

Each operation is located in one of the basic blocks, and basic block is located in the region, which represent IR for a whole routine (function).

Function parameters are marked with literal a (a0, a1 etc.). Constant-generated SSA values are marked with literal c (c1, c2 etc.). Other SSA values are marked with literal v (v3, v4 etc.).

Operations that do not produce material result, for example, ctrlflow::jump, are marked with literal n for 'none', as its return value type is DataType::NONE.

Notice that different SSA values can have different literal preceeding its ID, but regardless of the literal every value in the region has unique ID.

Basic block properties:

  • Basic block's ID is unique among the others in the same region;
  • Each basic block has a list of predecessors (<from bb3 bb1>), or it is the starting basic block, in which case it is marked with <start>;
  • A basic block can have either zero (<final>), one (<to bb2>) or two successors (<to T:bb2 / F:bb3>);
  • Basic block cannot have a terminator operation inside of it (it must be the last operation in the basic block);
  • If operation have zero successors, its last operation must be ctrlflow::return (TODO - add check);
  • If operation have one successor, its last operation cannot be ctrlflow::jumpc;
  • If operation have two successors, its last operation must be ctrlflow::jumpc.

Operations common properties:

  • Opcode value, which is unique for every type of operation across all dialects;
  • SSA-value's ID, which is unique across whole region;
  • Return data type, which is DataType::NONE if operation does not produce any result;
  • Optional list of inputs. Some operations can have variadic number of inputs (ctrlflow::call), other have fixed number of inputs (arith::add);
  • List of users, who use this operation's result as their input/inputs.

Dominators analysis

iris::Region class provides API:

  • collectDomInfo() - collect dominators information from the graph;
  • getDFS() and getRPO() - obtain corresponding traversal orders.

iris::doms::DomInfo helper-class provides API:

  • getIdom() and getIdomByID() - get immediate dominator
  • getDominatedBlocks() and getDominatedBlocksByID() - get blocks, for which given one is immediate dominator;
  • getDominatorsChain() and getDominatorsChainByID() - get chain of immediate dominators from the given block to the starting block.

Example graph:

A → B → C → D
↓
F → E → D
↓
G → D

Example output:

========== Dom Tree ===========
[Dominator Tree]
BB#0
BB#1
BB#3
BB#5
BB#4
BB#6
BB#2
================================

Loop analysis

iris::Region class provides API:

  • collectLoopInfo() - collect loops information from the graph.

iris::loops::LoopInfo helper-class provides API:

  • getRootLoop() - get root pseudo-loop, which is parent to top-level loops;
  • getTopLevelLoops() - get top-level loops.

Class iris::loops::Loop provides methods to query:

  • Header basic block;
  • List of latches;
  • Reducibility flag;
  • Exiting edges and blocks contained inside for reducible loops;
  • List of nested loops;
  • Loop's depth;
  • Parent loop (outer).

Root loop is a special pseudo-loop, which:

  • Has no parent loop, header, latches or exiting edges;
  • Has depth 0;
  • Contains all basic blocks, which are are not contained in any other loop in the region.

Example graph:

A → B → C → D → E → F → G → I → K
↓ ↓ ↓ ↓
J → C C E H → B

Example output:

========== Loop Tree ==========
[Root Loop]
Depth: 0 | Reducible: true
Blocks (3): 8 0 10
Nested Loops:
Loop Header: 1
Depth: 1 | Reducible: true
Latches: 7
Blocks (2): 9 6
Exits:
6 -> 8
Nested Loops:
Loop Header: 4
Depth: 2 | Reducible: true
Latches: 5
Blocks (0):
Exits:
5 -> 6
Loop Header: 2
Depth: 2 | Reducible: true
Latches: 3
Blocks (0):
Exits:
3 -> 4
================================

IR Optimizations

IRIS provides a pass-based optimization framework: every optimization is a subclass of iris::opt::Pass, and a iris::opt::PassManager owns a pipeline of passes and runs them sequentially on a iris::Region. An example of an optimization pipeline:

#include<iris.hpp>voidoptimize(iris::Region& region) {
iris::opt::PassManager pm;
pm.addPass(std::make_unique<iris::opt::common::DCEPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithConstFoldPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithPeepHolePass>());
pm.run(region);
}

Available passes:

  • common::DCEPass – removes dead, side-effect-free, non-terminator operations with results that are never used (parameters are kept);
    • Example - examples/DCE;
    • Tests - tests/opt/common/dce.cpp;
  • arith::ArithConstFoldPass – folds arithmetic, bitwise and comparison operations whose operands are compile-time constants into a single arith.const;
    • Example - examples/ConstFold;
    • Tests - tests/opt/arith/constfold.cpp;
  • arith::ArithPeepHolePass – applies local algebraic/bitwise simplifications on small instruction patterns (peepholes) for arith operations;
    • Example - examples/PeepHole.cpp;
    • Tests - tests/opt/arith/peephole.cpp.

Dead Code Elimination (DCEPass)

IR before DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8,v10)
a3.ui builtin.param -> (v10)
c4.ui arith.const (10) -> (v7)
c5.ui arith.const (20) -> (v11)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
v10.ui arith.mul (a2 : ui, a3 : ui) -> (v11)
v11.ui arith.add (v10 : ui, c5 : ui) -> (v12,v12)
v12.ui arith.mul (v11 : ui, v11 : ui)
c13.ui arith.const (5) -> (v15)
c14.ui arith.const (7) -> (v15,v17)
v15.ui arith.add (c13 : ui, c14 : ui) -> (v16)
v16.ui builtin.copy (v15 : ui) -> (v17)
v17.ui arith.sub (v16 : ui, c14 : ui)
n18 ctrlflow.return (v9 : ui)

IR after DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8)
a3.ui builtin.param
c4.ui arith.const (10) -> (v7)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
n18 ctrlflow.return (v9 : ui)

See corresponding example - examples/DCE. See corresponding tests - tests/opt/common/dce.cpp.

Constant folding patterns (ArithConstFoldPass)

  • Addition: add(c1, c2) -> const(c1 + c2) for integer and floating-point types.
  • Subtraction: sub(c1, c2) -> const(c1 - c2) for integer and floating-point types.
  • Multiplication: mul(c1, c2) -> const(c1 * c2) for integer and floating-point types.
  • Division:
    • Integer: div(c1, c2) -> const(c1 / c2) for signed/unsigned integers when c2 != 0 (no folding on division by zero).
    • Floating-point: div(c1, c2) -> const(c1 / c2) using normal IEEE semantics.
  • Bitwise AND: and(c1, c2) -> const(c1 & c2) for integer types.
  • Bitwise OR: or(c1, c2) -> const(c1 | c2) for integer types.
  • Bitwise XOR: xor(c1, c2) -> const(c1 ^ c2) for integer types.
  • Shifts:
    • Signed left: sal(c, s) -> const(c << s) for signed integers.
    • Signed right: sar(c, s) -> const(c >> s) for signed integers.
    • Unsigned left: shl(c, s) -> const(c << s) for unsigned integers.
    • Unsigned right:shr(c, s) -> const(c >> s) for unsigned integers.
  • Bitwise NOT: not(c) -> const(~c) for signed and unsigned integers.
  • Comparisons (result is a boolean constant) for integer, floating-point and boolean operands:
    • cmp.eq(c1, c2) -> const(c1 == c2)
    • cmp.neq(c1, c2) -> const(c1 != c2)
    • cmp.a(c1, c2) -> const(c1 > c2)
    • cmp.b(c1, c2) -> const(c1 < c2)
    • cmp.ae(c1, c2) -> const(c1 >= c2)
    • cmp.be(c1, c2) -> const(c1 <= c2)

IR before ArithConstFoldPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param -> (v21)
c2.si arith.const (10) -> (v10,v17)
c3.si arith.const (3) -> (v10,v11,v17,v19)
c4.ui arith.const (7) -> (v12,v13,v16)
c5.ui arith.const (2) -> (v12,v16)
c6.f arith.const (1.5) -> (v14)
c7.f arith.const (0.5) -> (v14,v15)
c8.b arith.const (true) -> (v18)
c9.b arith.const (false) -> (v18)
v10.si arith.add (c2 : si, c3 : si) -> (v11)
v11.si arith.mul (v10 : si, c3 : si) -> (v20)
v12.ui arith.and (c4 : ui, c5 : ui) -> (v13)
v13.ui arith.xor (v12 : ui, c4 : ui) -> (v21)
v14.f arith.add (c6 : f, c7 : f) -> (v15)
v15.f arith.div (v14 : f, c7 : f) -> (v23)
v16.ui arith.shl (c4 : ui, c5 : ui)
v17.si arith.sar (c2 : si, c3 : si)
v18.b arith.cmp.eq (c8 : b, c9 : b) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (v11 : si, v19 : si) -> (v24)
v21.ui arith.and (v13 : ui, a1 : ui)
v22.si arith.cast (v18 : b) -> (v24)
v23.si arith.cast (v15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

IR after ArithConstFoldPass + DCEPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param
c3.si arith.const (3) -> (v19)
c11.si arith.const (39) -> (v20)
c15.f arith.const (4) -> (v23)
c18.b arith.const (false) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (c11 : si, v19 : si) -> (v24)
v22.si arith.cast (c18 : b) -> (v24)
v23.si arith.cast (c15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

See corresponding example - examples/ConstFold. See corresponding tests - tests/opt/arith/constfold.cpp.

Peephole patterns (ArithPeepHolePass):

  • add:
    • add(x, 0) -> x
    • add(0, x) -> x
  • sub:
    • sub(x, 0) -> x
    • sub(x, x) -> 0
  • mul:
    • mul(x, 1) -> x
    • mul(1, x) -> x
    • mul(x, 0) -> 0
    • mul(0, x) -> 0
  • div:
    • div(x, 1) -> x
  • and:
    • and(x, 0) -> 0
    • and(0, x) -> 0
    • and(x, 11..1) -> x
    • and(11..1, x) -> x
    • and(x, x) -> x
  • or:
    • or(x, 0) -> x
    • or(0, x) -> x
    • or(x, 11..1) -> 11..1
    • or(11..1, x) -> 11..1
    • or(x, x) -> x
  • xor:
    • xor(x, 0) -> x
    • xor(0, x) -> x
    • xor(x, x) -> 0
    • xor(x, 11..1) -> ~x
    • xor(11..1, x) -> ~x
  • Shifts (arithmetical sal/sar, logical shl/shr):
    • sal(x, 0) -> x, sal(0, x) -> 0
    • shl(x, 0) -> x, shl(0, x) -> 0
    • sar(x, 0) -> x, sar(0, x) -> 0
    • shr(x, 0) -> x, shr(0, x) -> 0
  • Double negation:
    • not(not(x)) -> x
  • To expose constant–constant sub-expressions for ArithConstFoldPass, the peephole pass applies rotate patterns to integer add/mul when an inner operation is used only by a single outer operation:
    • add:
      • (x + C1) + C2 -> x + (C1 + C2)
      • (C1 + x) + C2 -> x + (C1 + C2)
      • C2 + (x + C1) -> x + (C1 + C2)
      • C2 + (C1 + x) -> x + (C1 + C2)
    • mul:
      • (C1 * x) * C2 -> x * (C1 * C2)
      • (x * C1) * C2 -> x * (C1 * C2)
      • C2 * (C1 * x) -> x * (C1 * C2)
      • C2 * (x * C1) -> x * (C1 * C2)

IR before ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v10,v11,v12,v14,v15,v16,v18,v34,v35,v36,v37,v50)
a1.si builtin.param -> (v13,v13,v17,v54)
a2.ui builtin.param -> (v19,v20,v21,v22,v24,v25,v26,v27,v29,v30,v32,v33,v38,v39,v40,v41,v42)
a3.ui builtin.param -> (v23,v23,v28,v28,v31,v31)
c4.si arith.const (0) -> (v10,v11,v12,v16,v17,v34,v35,v36,v37)
c5.si arith.const (1) -> (v14,v15,v18)
c6.ui arith.const (0) -> (v19,v20,v24,v25,v29,v30,v38,v39,v40,v41)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615) -> (v21,v22,v26,v27,v32,v33)
v10.si arith.add (a0 : si, c4 : si) -> (v44)
v11.si arith.add (c4 : si, a0 : si)
v12.si arith.sub (a0 : si, c4 : si) -> (v44)
v13.si arith.sub (a1 : si, a1 : si)
v14.si arith.mul (a0 : si, c5 : si) -> (v45)
v15.si arith.mul (c5 : si, a0 : si)
v16.si arith.mul (a0 : si, c4 : si)
v17.si arith.mul (c4 : si, a1 : si)
v18.si arith.div (a0 : si, c5 : si) -> (v45)
v19.ui arith.and (a2 : ui, c6 : ui)
v20.ui arith.and (c6 : ui, a2 : ui)
v21.ui arith.and (a2 : ui, c9 : ui) -> (v46)
v22.ui arith.and (c9 : ui, a2 : ui)
v23.ui arith.and (a3 : ui, a3 : ui)
v24.ui arith.or (a2 : ui, c6 : ui)
v25.ui arith.or (c6 : ui, a2 : ui)
v26.ui arith.or (a2 : ui, c9 : ui) -> (v46)
v27.ui arith.or (c9 : ui, a2 : ui)
v28.ui arith.or (a3 : ui, a3 : ui)
v29.ui arith.xor (a2 : ui, c6 : ui)
v30.ui arith.xor (c6 : ui, a2 : ui)
v31.ui arith.xor (a3 : ui, a3 : ui)
v32.ui arith.xor (a2 : ui, c9 : ui)
v33.ui arith.xor (c9 : ui, a2 : ui)
v34.si arith.sal (a0 : si, c4 : si)
v35.si arith.sal (c4 : si, a0 : si)
v36.si arith.sar (a0 : si, c4 : si)
v37.si arith.sar (c4 : si, a0 : si)
v38.ui arith.shl (a2 : ui, c6 : ui)
v39.ui arith.shl (c6 : ui, a2 : ui)
v40.ui arith.shr (a2 : ui, c6 : ui)
v41.ui arith.shr (c6 : ui, a2 : ui)
v42.ui arith.not (a2 : ui) -> (v43)
v43.ui arith.not (v42 : ui) -> (n56)
v44.si arith.add (v10 : si, v12 : si) -> (v47)
v45.si arith.add (v14 : si, v18 : si) -> (v47)
v46.ui arith.and (v21 : ui, v26 : ui)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v51)
v50.si arith.add (a0 : si, c48 : si) -> (v51)
v51.si arith.add (v50 : si, c49 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v55)
v54.si arith.mul (c52 : si, a1 : si) -> (v55)
v55.si arith.mul (v54 : si, c53 : si)
n56 ctrlflow.return (v43 : ui)

IR after ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v44,v44,v45,v45,v51)
a1.si builtin.param -> (v55)
a2.ui builtin.param -> (v32,v33,n56)
a3.ui builtin.param
c4.si arith.const (0)
c5.si arith.const (1)
c6.ui arith.const (0)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615)
c13.si arith.const (0)
c16.si arith.const (0)
c17.si arith.const (0)
c19.ui arith.const (0)
c20.ui arith.const (0)
c31.ui arith.const (0)
v32.ui arith.not (a2 : ui)
v33.ui arith.not (a2 : ui)
c35.si arith.const (0)
c37.si arith.const (0)
c39.ui arith.const (0)
c41.ui arith.const (0)
v44.si arith.add (a0 : si, a0 : si) -> (v47)
v45.si arith.add (a0 : si, a0 : si) -> (v47)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v50)
v50.si arith.add (c48 : si, c49 : si) -> (v51)
v51.si arith.add (a0 : si, v50 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v54)
v54.si arith.mul (c52 : si, c53 : si) -> (v55)
v55.si arith.mul (a1 : si, v54 : si)
n56 ctrlflow.return (a2 : ui)

See corresponding example - examples/PeepHole. See corresponding tests - tests/opt/arith/peephole.cpp.

Building examples

Requirements:

  • CMake >= 3.21
  • C++ compiler supporting C++23 standard

To build project, use following commads from the project's roor directory:

cmake -B build
cmake --build build

If you want to build specific target, use following command:

cmake --build build --target <target_name>

List of available targets:

  • iris - IR-support static library;
  • factorial - example of IR usage - building IR for a programm calculating factorial manually via iris::IRBuilder;
  • domExample01 - domExample03 - examples of running Dom analysis and building IDom tree;
  • loopExample01 - loopExample06 - examples of running Loop analysis and building Loop tree;
  • DCE - example of optimizing IR with use of DCE;
  • ConstFold - example of optimizing IR with use of Constant Folding;
  • PeepHole - example of optimizing IR with use of PeepHole optimizations.

Static library output files are located in build/lib/, and executables - in build/bin.

Testing

Project includes tests, impemented using GTest.

To build and run tests, run following commands from project's root directory:

cmake -B build
cmake --build build --target iris_tests
cd build
ctest

Tests include:

  • Dominator analysis tests (tests/doms.cpp);
  • Loop analysis tests (tests/loops.cpp).
  • Separate test suites for each dialect and its operations' verifiers and construction pre-conditions (tests/dialects/);
  • Integration tests (tests/integration.cpp);
  • Optimizationpasses & patterns tests (tests/opt/):
    • Dead Code Elimination (common/dce.cpp);
    • Constant Folding for arith operations (arith/constfold.cpp);
    • PeepHole Optimizations for arith operations (arith/peephole.cpp).
  • Tests of verifiers (ex-fails) and API tests for:
    • Generic operation iris::Operation;
    • Basic block iris::BasicBlock;
    • Region iris::Region;
    • IR-builder iris::IRBuilder;
    • Dominator analysis helper class iris::doms::DomInfo;
    • Loop analysis helper class iris::doms::LoopInfo (TODO).

About

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

iris

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

logo

Overview

IRIS is an example of IR that includes simple operations, separated into different dialects - arith, builtin and ctrlflow.

Dialect arith:

  • ADD, SUB, MUL, DIV - basic arithmetical operations;
  • CONST - operation generating a constant SSA value;
  • CAST - operation, casting values between different data types;
  • CMP - comparison operation, which takes predicate (EQ (equal), NEQ (not-equal), A (above), B (below), AE (above or equal), BE (below or equal));
  • AND, OR, XOR, NOT - bitwise operations;
  • SHL, SHR, SAL, SAR - arithmetical and logical bit-shifts

Dialect builtin:

  • PARAM - operation representing function's parameter;
  • COPY - operation that copies one SSA value into the new one.

Dialect ctrlflow:

  • CALL - routine call operation;
  • JUMP - unconditional jump;
  • JUMPC - conditional jump;
  • RETURN - return from the routine operation;
  • PHI - phi-function.

IRIS structure & examples

Here's an example of IRIS IR for a programm that calculates factorial of a number:

factorial:
^bb0 <start> <to bb1> :
a0.ui builtin.param -> (v3,v5,v7)
c1.ui arith.const (1) -> (v5,v8p)
c2.ui arith.const (2) -> (v3)
^bb1 <from bb0> <to T:bb2 / F:bb3> :
v3.b arith.cmp.b (a0 : ui, c2 : ui) -> (n4)
n4 ctrlflow.jmpc (v3 : b)
^bb3 <from bb1> <to bb2> :
v5.ui arith.sub (a0 : ui, c1 : ui) -> (v6)
v6.ui ctrlflow.call @factorial (v5 : ui) -> (v7)
v7.ui arith.mul (a0 : ui, v6 : ui) -> (v8p)
^bb2 <from bb3 bb1> <final> :
v8p.ui ctrlflow.phi (c1 : ui, v7 : ui) -> (n9)
n9 ctrlflow.return (v8p : ui)

Each operation is located in one of the basic blocks, and basic block is located in the region, which represent IR for a whole routine (function).

Function parameters are marked with literal a (a0, a1 etc.). Constant-generated SSA values are marked with literal c (c1, c2 etc.). Other SSA values are marked with literal v (v3, v4 etc.).

Operations that do not produce material result, for example, ctrlflow::jump, are marked with literal n for 'none', as its return value type is DataType::NONE.

Notice that different SSA values can have different literal preceeding its ID, but regardless of the literal every value in the region has unique ID.

Basic block properties:

  • Basic block's ID is unique among the others in the same region;
  • Each basic block has a list of predecessors (<from bb3 bb1>), or it is the starting basic block, in which case it is marked with <start>;
  • A basic block can have either zero (<final>), one (<to bb2>) or two successors (<to T:bb2 / F:bb3>);
  • Basic block cannot have a terminator operation inside of it (it must be the last operation in the basic block);
  • If operation have zero successors, its last operation must be ctrlflow::return (TODO - add check);
  • If operation have one successor, its last operation cannot be ctrlflow::jumpc;
  • If operation have two successors, its last operation must be ctrlflow::jumpc.

Operations common properties:

  • Opcode value, which is unique for every type of operation across all dialects;
  • SSA-value's ID, which is unique across whole region;
  • Return data type, which is DataType::NONE if operation does not produce any result;
  • Optional list of inputs. Some operations can have variadic number of inputs (ctrlflow::call), other have fixed number of inputs (arith::add);
  • List of users, who use this operation's result as their input/inputs.

Dominators analysis

iris::Region class provides API:

  • collectDomInfo() - collect dominators information from the graph;
  • getDFS() and getRPO() - obtain corresponding traversal orders.

iris::doms::DomInfo helper-class provides API:

  • getIdom() and getIdomByID() - get immediate dominator
  • getDominatedBlocks() and getDominatedBlocksByID() - get blocks, for which given one is immediate dominator;
  • getDominatorsChain() and getDominatorsChainByID() - get chain of immediate dominators from the given block to the starting block.

Example graph:

A → B → C → D
↓
F → E → D
↓
G → D

Example output:

========== Dom Tree ===========
[Dominator Tree]
BB#0
BB#1
BB#3
BB#5
BB#4
BB#6
BB#2
================================

Loop analysis

iris::Region class provides API:

  • collectLoopInfo() - collect loops information from the graph.

iris::loops::LoopInfo helper-class provides API:

  • getRootLoop() - get root pseudo-loop, which is parent to top-level loops;
  • getTopLevelLoops() - get top-level loops.

Class iris::loops::Loop provides methods to query:

  • Header basic block;
  • List of latches;
  • Reducibility flag;
  • Exiting edges and blocks contained inside for reducible loops;
  • List of nested loops;
  • Loop's depth;
  • Parent loop (outer).

Root loop is a special pseudo-loop, which:

  • Has no parent loop, header, latches or exiting edges;
  • Has depth 0;
  • Contains all basic blocks, which are are not contained in any other loop in the region.

Example graph:

A → B → C → D → E → F → G → I → K
↓ ↓ ↓ ↓
J → C C E H → B

Example output:

========== Loop Tree ==========
[Root Loop]
Depth: 0 | Reducible: true
Blocks (3): 8 0 10
Nested Loops:
Loop Header: 1
Depth: 1 | Reducible: true
Latches: 7
Blocks (2): 9 6
Exits:
6 -> 8
Nested Loops:
Loop Header: 4
Depth: 2 | Reducible: true
Latches: 5
Blocks (0):
Exits:
5 -> 6
Loop Header: 2
Depth: 2 | Reducible: true
Latches: 3
Blocks (0):
Exits:
3 -> 4
================================

IR Optimizations

IRIS provides a pass-based optimization framework: every optimization is a subclass of iris::opt::Pass, and a iris::opt::PassManager owns a pipeline of passes and runs them sequentially on a iris::Region. An example of an optimization pipeline:

#include<iris.hpp>voidoptimize(iris::Region& region) {
iris::opt::PassManager pm;
pm.addPass(std::make_unique<iris::opt::common::DCEPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithConstFoldPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithPeepHolePass>());
pm.run(region);
}

Available passes:

  • common::DCEPass – removes dead, side-effect-free, non-terminator operations with results that are never used (parameters are kept);
    • Example - examples/DCE;
    • Tests - tests/opt/common/dce.cpp;
  • arith::ArithConstFoldPass – folds arithmetic, bitwise and comparison operations whose operands are compile-time constants into a single arith.const;
    • Example - examples/ConstFold;
    • Tests - tests/opt/arith/constfold.cpp;
  • arith::ArithPeepHolePass – applies local algebraic/bitwise simplifications on small instruction patterns (peepholes) for arith operations;
    • Example - examples/PeepHole.cpp;
    • Tests - tests/opt/arith/peephole.cpp.

Dead Code Elimination (DCEPass)

IR before DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8,v10)
a3.ui builtin.param -> (v10)
c4.ui arith.const (10) -> (v7)
c5.ui arith.const (20) -> (v11)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
v10.ui arith.mul (a2 : ui, a3 : ui) -> (v11)
v11.ui arith.add (v10 : ui, c5 : ui) -> (v12,v12)
v12.ui arith.mul (v11 : ui, v11 : ui)
c13.ui arith.const (5) -> (v15)
c14.ui arith.const (7) -> (v15,v17)
v15.ui arith.add (c13 : ui, c14 : ui) -> (v16)
v16.ui builtin.copy (v15 : ui) -> (v17)
v17.ui arith.sub (v16 : ui, c14 : ui)
n18 ctrlflow.return (v9 : ui)

IR after DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8)
a3.ui builtin.param
c4.ui arith.const (10) -> (v7)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
n18 ctrlflow.return (v9 : ui)

See corresponding example - examples/DCE. See corresponding tests - tests/opt/common/dce.cpp.

Constant folding patterns (ArithConstFoldPass)

  • Addition: add(c1, c2) -> const(c1 + c2) for integer and floating-point types.
  • Subtraction: sub(c1, c2) -> const(c1 - c2) for integer and floating-point types.
  • Multiplication: mul(c1, c2) -> const(c1 * c2) for integer and floating-point types.
  • Division:
    • Integer: div(c1, c2) -> const(c1 / c2) for signed/unsigned integers when c2 != 0 (no folding on division by zero).
    • Floating-point: div(c1, c2) -> const(c1 / c2) using normal IEEE semantics.
  • Bitwise AND: and(c1, c2) -> const(c1 & c2) for integer types.
  • Bitwise OR: or(c1, c2) -> const(c1 | c2) for integer types.
  • Bitwise XOR: xor(c1, c2) -> const(c1 ^ c2) for integer types.
  • Shifts:
    • Signed left: sal(c, s) -> const(c << s) for signed integers.
    • Signed right: sar(c, s) -> const(c >> s) for signed integers.
    • Unsigned left: shl(c, s) -> const(c << s) for unsigned integers.
    • Unsigned right:shr(c, s) -> const(c >> s) for unsigned integers.
  • Bitwise NOT: not(c) -> const(~c) for signed and unsigned integers.
  • Comparisons (result is a boolean constant) for integer, floating-point and boolean operands:
    • cmp.eq(c1, c2) -> const(c1 == c2)
    • cmp.neq(c1, c2) -> const(c1 != c2)
    • cmp.a(c1, c2) -> const(c1 > c2)
    • cmp.b(c1, c2) -> const(c1 < c2)
    • cmp.ae(c1, c2) -> const(c1 >= c2)
    • cmp.be(c1, c2) -> const(c1 <= c2)

IR before ArithConstFoldPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param -> (v21)
c2.si arith.const (10) -> (v10,v17)
c3.si arith.const (3) -> (v10,v11,v17,v19)
c4.ui arith.const (7) -> (v12,v13,v16)
c5.ui arith.const (2) -> (v12,v16)
c6.f arith.const (1.5) -> (v14)
c7.f arith.const (0.5) -> (v14,v15)
c8.b arith.const (true) -> (v18)
c9.b arith.const (false) -> (v18)
v10.si arith.add (c2 : si, c3 : si) -> (v11)
v11.si arith.mul (v10 : si, c3 : si) -> (v20)
v12.ui arith.and (c4 : ui, c5 : ui) -> (v13)
v13.ui arith.xor (v12 : ui, c4 : ui) -> (v21)
v14.f arith.add (c6 : f, c7 : f) -> (v15)
v15.f arith.div (v14 : f, c7 : f) -> (v23)
v16.ui arith.shl (c4 : ui, c5 : ui)
v17.si arith.sar (c2 : si, c3 : si)
v18.b arith.cmp.eq (c8 : b, c9 : b) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (v11 : si, v19 : si) -> (v24)
v21.ui arith.and (v13 : ui, a1 : ui)
v22.si arith.cast (v18 : b) -> (v24)
v23.si arith.cast (v15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

IR after ArithConstFoldPass + DCEPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param
c3.si arith.const (3) -> (v19)
c11.si arith.const (39) -> (v20)
c15.f arith.const (4) -> (v23)
c18.b arith.const (false) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (c11 : si, v19 : si) -> (v24)
v22.si arith.cast (c18 : b) -> (v24)
v23.si arith.cast (c15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

See corresponding example - examples/ConstFold. See corresponding tests - tests/opt/arith/constfold.cpp.

Peephole patterns (ArithPeepHolePass):

  • add:
    • add(x, 0) -> x
    • add(0, x) -> x
  • sub:
    • sub(x, 0) -> x
    • sub(x, x) -> 0
  • mul:
    • mul(x, 1) -> x
    • mul(1, x) -> x
    • mul(x, 0) -> 0
    • mul(0, x) -> 0
  • div:
    • div(x, 1) -> x
  • and:
    • and(x, 0) -> 0
    • and(0, x) -> 0
    • and(x, 11..1) -> x
    • and(11..1, x) -> x
    • and(x, x) -> x
  • or:
    • or(x, 0) -> x
    • or(0, x) -> x
    • or(x, 11..1) -> 11..1
    • or(11..1, x) -> 11..1
    • or(x, x) -> x
  • xor:
    • xor(x, 0) -> x
    • xor(0, x) -> x
    • xor(x, x) -> 0
    • xor(x, 11..1) -> ~x
    • xor(11..1, x) -> ~x
  • Shifts (arithmetical sal/sar, logical shl/shr):
    • sal(x, 0) -> x, sal(0, x) -> 0
    • shl(x, 0) -> x, shl(0, x) -> 0
    • sar(x, 0) -> x, sar(0, x) -> 0
    • shr(x, 0) -> x, shr(0, x) -> 0
  • Double negation:
    • not(not(x)) -> x
  • To expose constant–constant sub-expressions for ArithConstFoldPass, the peephole pass applies rotate patterns to integer add/mul when an inner operation is used only by a single outer operation:
    • add:
      • (x + C1) + C2 -> x + (C1 + C2)
      • (C1 + x) + C2 -> x + (C1 + C2)
      • C2 + (x + C1) -> x + (C1 + C2)
      • C2 + (C1 + x) -> x + (C1 + C2)
    • mul:
      • (C1 * x) * C2 -> x * (C1 * C2)
      • (x * C1) * C2 -> x * (C1 * C2)
      • C2 * (C1 * x) -> x * (C1 * C2)
      • C2 * (x * C1) -> x * (C1 * C2)

IR before ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v10,v11,v12,v14,v15,v16,v18,v34,v35,v36,v37,v50)
a1.si builtin.param -> (v13,v13,v17,v54)
a2.ui builtin.param -> (v19,v20,v21,v22,v24,v25,v26,v27,v29,v30,v32,v33,v38,v39,v40,v41,v42)
a3.ui builtin.param -> (v23,v23,v28,v28,v31,v31)
c4.si arith.const (0) -> (v10,v11,v12,v16,v17,v34,v35,v36,v37)
c5.si arith.const (1) -> (v14,v15,v18)
c6.ui arith.const (0) -> (v19,v20,v24,v25,v29,v30,v38,v39,v40,v41)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615) -> (v21,v22,v26,v27,v32,v33)
v10.si arith.add (a0 : si, c4 : si) -> (v44)
v11.si arith.add (c4 : si, a0 : si)
v12.si arith.sub (a0 : si, c4 : si) -> (v44)
v13.si arith.sub (a1 : si, a1 : si)
v14.si arith.mul (a0 : si, c5 : si) -> (v45)
v15.si arith.mul (c5 : si, a0 : si)
v16.si arith.mul (a0 : si, c4 : si)
v17.si arith.mul (c4 : si, a1 : si)
v18.si arith.div (a0 : si, c5 : si) -> (v45)
v19.ui arith.and (a2 : ui, c6 : ui)
v20.ui arith.and (c6 : ui, a2 : ui)
v21.ui arith.and (a2 : ui, c9 : ui) -> (v46)
v22.ui arith.and (c9 : ui, a2 : ui)
v23.ui arith.and (a3 : ui, a3 : ui)
v24.ui arith.or (a2 : ui, c6 : ui)
v25.ui arith.or (c6 : ui, a2 : ui)
v26.ui arith.or (a2 : ui, c9 : ui) -> (v46)
v27.ui arith.or (c9 : ui, a2 : ui)
v28.ui arith.or (a3 : ui, a3 : ui)
v29.ui arith.xor (a2 : ui, c6 : ui)
v30.ui arith.xor (c6 : ui, a2 : ui)
v31.ui arith.xor (a3 : ui, a3 : ui)
v32.ui arith.xor (a2 : ui, c9 : ui)
v33.ui arith.xor (c9 : ui, a2 : ui)
v34.si arith.sal (a0 : si, c4 : si)
v35.si arith.sal (c4 : si, a0 : si)
v36.si arith.sar (a0 : si, c4 : si)
v37.si arith.sar (c4 : si, a0 : si)
v38.ui arith.shl (a2 : ui, c6 : ui)
v39.ui arith.shl (c6 : ui, a2 : ui)
v40.ui arith.shr (a2 : ui, c6 : ui)
v41.ui arith.shr (c6 : ui, a2 : ui)
v42.ui arith.not (a2 : ui) -> (v43)
v43.ui arith.not (v42 : ui) -> (n56)
v44.si arith.add (v10 : si, v12 : si) -> (v47)
v45.si arith.add (v14 : si, v18 : si) -> (v47)
v46.ui arith.and (v21 : ui, v26 : ui)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v51)
v50.si arith.add (a0 : si, c48 : si) -> (v51)
v51.si arith.add (v50 : si, c49 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v55)
v54.si arith.mul (c52 : si, a1 : si) -> (v55)
v55.si arith.mul (v54 : si, c53 : si)
n56 ctrlflow.return (v43 : ui)

IR after ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v44,v44,v45,v45,v51)
a1.si builtin.param -> (v55)
a2.ui builtin.param -> (v32,v33,n56)
a3.ui builtin.param
c4.si arith.const (0)
c5.si arith.const (1)
c6.ui arith.const (0)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615)
c13.si arith.const (0)
c16.si arith.const (0)
c17.si arith.const (0)
c19.ui arith.const (0)
c20.ui arith.const (0)
c31.ui arith.const (0)
v32.ui arith.not (a2 : ui)
v33.ui arith.not (a2 : ui)
c35.si arith.const (0)
c37.si arith.const (0)
c39.ui arith.const (0)
c41.ui arith.const (0)
v44.si arith.add (a0 : si, a0 : si) -> (v47)
v45.si arith.add (a0 : si, a0 : si) -> (v47)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v50)
v50.si arith.add (c48 : si, c49 : si) -> (v51)
v51.si arith.add (a0 : si, v50 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v54)
v54.si arith.mul (c52 : si, c53 : si) -> (v55)
v55.si arith.mul (a1 : si, v54 : si)
n56 ctrlflow.return (a2 : ui)

See corresponding example - examples/PeepHole. See corresponding tests - tests/opt/arith/peephole.cpp.

Building examples

Requirements:

  • CMake >= 3.21
  • C++ compiler supporting C++23 standard

To build project, use following commads from the project's roor directory:

cmake -B build
cmake --build build

If you want to build specific target, use following command:

cmake --build build --target <target_name>

List of available targets:

  • iris - IR-support static library;
  • factorial - example of IR usage - building IR for a programm calculating factorial manually via iris::IRBuilder;
  • domExample01 - domExample03 - examples of running Dom analysis and building IDom tree;
  • loopExample01 - loopExample06 - examples of running Loop analysis and building Loop tree;
  • DCE - example of optimizing IR with use of DCE;
  • ConstFold - example of optimizing IR with use of Constant Folding;
  • PeepHole - example of optimizing IR with use of PeepHole optimizations.

Static library output files are located in build/lib/, and executables - in build/bin.

Testing

Project includes tests, impemented using GTest.

To build and run tests, run following commands from project's root directory:

cmake -B build
cmake --build build --target iris_tests
cd build
ctest

Tests include:

  • Dominator analysis tests (tests/doms.cpp);
  • Loop analysis tests (tests/loops.cpp).
  • Separate test suites for each dialect and its operations' verifiers and construction pre-conditions (tests/dialects/);
  • Integration tests (tests/integration.cpp);
  • Optimizationpasses & patterns tests (tests/opt/):
    • Dead Code Elimination (common/dce.cpp);
    • Constant Folding for arith operations (arith/constfold.cpp);
    • PeepHole Optimizations for arith operations (arith/peephole.cpp).
  • Tests of verifiers (ex-fails) and API tests for:
    • Generic operation iris::Operation;
    • Basic block iris::BasicBlock;
    • Region iris::Region;
    • IR-builder iris::IRBuilder;
    • Dominator analysis helper class iris::doms::DomInfo;
    • Loop analysis helper class iris::doms::LoopInfo (TODO).

About

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

iris

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

logo

Overview

IRIS is an example of IR that includes simple operations, separated into different dialects - arith, builtin and ctrlflow.

Dialect arith:

  • ADD, SUB, MUL, DIV - basic arithmetical operations;
  • CONST - operation generating a constant SSA value;
  • CAST - operation, casting values between different data types;
  • CMP - comparison operation, which takes predicate (EQ (equal), NEQ (not-equal), A (above), B (below), AE (above or equal), BE (below or equal));
  • AND, OR, XOR, NOT - bitwise operations;
  • SHL, SHR, SAL, SAR - arithmetical and logical bit-shifts

Dialect builtin:

  • PARAM - operation representing function's parameter;
  • COPY - operation that copies one SSA value into the new one.

Dialect ctrlflow:

  • CALL - routine call operation;
  • JUMP - unconditional jump;
  • JUMPC - conditional jump;
  • RETURN - return from the routine operation;
  • PHI - phi-function.

IRIS structure & examples

Here's an example of IRIS IR for a programm that calculates factorial of a number:

factorial:
^bb0 <start> <to bb1> :
a0.ui builtin.param -> (v3,v5,v7)
c1.ui arith.const (1) -> (v5,v8p)
c2.ui arith.const (2) -> (v3)
^bb1 <from bb0> <to T:bb2 / F:bb3> :
v3.b arith.cmp.b (a0 : ui, c2 : ui) -> (n4)
n4 ctrlflow.jmpc (v3 : b)
^bb3 <from bb1> <to bb2> :
v5.ui arith.sub (a0 : ui, c1 : ui) -> (v6)
v6.ui ctrlflow.call @factorial (v5 : ui) -> (v7)
v7.ui arith.mul (a0 : ui, v6 : ui) -> (v8p)
^bb2 <from bb3 bb1> <final> :
v8p.ui ctrlflow.phi (c1 : ui, v7 : ui) -> (n9)
n9 ctrlflow.return (v8p : ui)

Each operation is located in one of the basic blocks, and basic block is located in the region, which represent IR for a whole routine (function).

Function parameters are marked with literal a (a0, a1 etc.). Constant-generated SSA values are marked with literal c (c1, c2 etc.). Other SSA values are marked with literal v (v3, v4 etc.).

Operations that do not produce material result, for example, ctrlflow::jump, are marked with literal n for 'none', as its return value type is DataType::NONE.

Notice that different SSA values can have different literal preceeding its ID, but regardless of the literal every value in the region has unique ID.

Basic block properties:

  • Basic block's ID is unique among the others in the same region;
  • Each basic block has a list of predecessors (<from bb3 bb1>), or it is the starting basic block, in which case it is marked with <start>;
  • A basic block can have either zero (<final>), one (<to bb2>) or two successors (<to T:bb2 / F:bb3>);
  • Basic block cannot have a terminator operation inside of it (it must be the last operation in the basic block);
  • If operation have zero successors, its last operation must be ctrlflow::return (TODO - add check);
  • If operation have one successor, its last operation cannot be ctrlflow::jumpc;
  • If operation have two successors, its last operation must be ctrlflow::jumpc.

Operations common properties:

  • Opcode value, which is unique for every type of operation across all dialects;
  • SSA-value's ID, which is unique across whole region;
  • Return data type, which is DataType::NONE if operation does not produce any result;
  • Optional list of inputs. Some operations can have variadic number of inputs (ctrlflow::call), other have fixed number of inputs (arith::add);
  • List of users, who use this operation's result as their input/inputs.

Dominators analysis

iris::Region class provides API:

  • collectDomInfo() - collect dominators information from the graph;
  • getDFS() and getRPO() - obtain corresponding traversal orders.

iris::doms::DomInfo helper-class provides API:

  • getIdom() and getIdomByID() - get immediate dominator
  • getDominatedBlocks() and getDominatedBlocksByID() - get blocks, for which given one is immediate dominator;
  • getDominatorsChain() and getDominatorsChainByID() - get chain of immediate dominators from the given block to the starting block.

Example graph:

A → B → C → D
↓
F → E → D
↓
G → D

Example output:

========== Dom Tree ===========
[Dominator Tree]
BB#0
BB#1
BB#3
BB#5
BB#4
BB#6
BB#2
================================

Loop analysis

iris::Region class provides API:

  • collectLoopInfo() - collect loops information from the graph.

iris::loops::LoopInfo helper-class provides API:

  • getRootLoop() - get root pseudo-loop, which is parent to top-level loops;
  • getTopLevelLoops() - get top-level loops.

Class iris::loops::Loop provides methods to query:

  • Header basic block;
  • List of latches;
  • Reducibility flag;
  • Exiting edges and blocks contained inside for reducible loops;
  • List of nested loops;
  • Loop's depth;
  • Parent loop (outer).

Root loop is a special pseudo-loop, which:

  • Has no parent loop, header, latches or exiting edges;
  • Has depth 0;
  • Contains all basic blocks, which are are not contained in any other loop in the region.

Example graph:

A → B → C → D → E → F → G → I → K
↓ ↓ ↓ ↓
J → C C E H → B

Example output:

========== Loop Tree ==========
[Root Loop]
Depth: 0 | Reducible: true
Blocks (3): 8 0 10
Nested Loops:
Loop Header: 1
Depth: 1 | Reducible: true
Latches: 7
Blocks (2): 9 6
Exits:
6 -> 8
Nested Loops:
Loop Header: 4
Depth: 2 | Reducible: true
Latches: 5
Blocks (0):
Exits:
5 -> 6
Loop Header: 2
Depth: 2 | Reducible: true
Latches: 3
Blocks (0):
Exits:
3 -> 4
================================

IR Optimizations

IRIS provides a pass-based optimization framework: every optimization is a subclass of iris::opt::Pass, and a iris::opt::PassManager owns a pipeline of passes and runs them sequentially on a iris::Region. An example of an optimization pipeline:

#include<iris.hpp>voidoptimize(iris::Region& region) {
iris::opt::PassManager pm;
pm.addPass(std::make_unique<iris::opt::common::DCEPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithConstFoldPass>());
pm.addPass(std::make_unique<iris::opt::arith::ArithPeepHolePass>());
pm.run(region);
}

Available passes:

  • common::DCEPass – removes dead, side-effect-free, non-terminator operations with results that are never used (parameters are kept);
    • Example - examples/DCE;
    • Tests - tests/opt/common/dce.cpp;
  • arith::ArithConstFoldPass – folds arithmetic, bitwise and comparison operations whose operands are compile-time constants into a single arith.const;
    • Example - examples/ConstFold;
    • Tests - tests/opt/arith/constfold.cpp;
  • arith::ArithPeepHolePass – applies local algebraic/bitwise simplifications on small instruction patterns (peepholes) for arith operations;
    • Example - examples/PeepHole.cpp;
    • Tests - tests/opt/arith/peephole.cpp.

Dead Code Elimination (DCEPass)

IR before DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8,v10)
a3.ui builtin.param -> (v10)
c4.ui arith.const (10) -> (v7)
c5.ui arith.const (20) -> (v11)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
v10.ui arith.mul (a2 : ui, a3 : ui) -> (v11)
v11.ui arith.add (v10 : ui, c5 : ui) -> (v12,v12)
v12.ui arith.mul (v11 : ui, v11 : ui)
c13.ui arith.const (5) -> (v15)
c14.ui arith.const (7) -> (v15,v17)
v15.ui arith.add (c13 : ui, c14 : ui) -> (v16)
v16.ui builtin.copy (v15 : ui) -> (v17)
v17.ui arith.sub (v16 : ui, c14 : ui)
n18 ctrlflow.return (v9 : ui)

IR after DCEPass:

dce:
^bb0 <start> <final> :
a0.ui builtin.param -> (v6)
a1.ui builtin.param -> (v6)
a2.ui builtin.param -> (v8)
a3.ui builtin.param
c4.ui arith.const (10) -> (v7)
v6.ui arith.add (a0 : ui, a1 : ui) -> (v7)
v7.ui arith.mul (v6 : ui, c4 : ui) -> (v8)
v8.ui arith.sub (v7 : ui, a2 : ui) -> (v9)
v9.ui builtin.copy (v8 : ui) -> (n18)
n18 ctrlflow.return (v9 : ui)

See corresponding example - examples/DCE. See corresponding tests - tests/opt/common/dce.cpp.

Constant folding patterns (ArithConstFoldPass)

  • Addition: add(c1, c2) -> const(c1 + c2) for integer and floating-point types.
  • Subtraction: sub(c1, c2) -> const(c1 - c2) for integer and floating-point types.
  • Multiplication: mul(c1, c2) -> const(c1 * c2) for integer and floating-point types.
  • Division:
    • Integer: div(c1, c2) -> const(c1 / c2) for signed/unsigned integers when c2 != 0 (no folding on division by zero).
    • Floating-point: div(c1, c2) -> const(c1 / c2) using normal IEEE semantics.
  • Bitwise AND: and(c1, c2) -> const(c1 & c2) for integer types.
  • Bitwise OR: or(c1, c2) -> const(c1 | c2) for integer types.
  • Bitwise XOR: xor(c1, c2) -> const(c1 ^ c2) for integer types.
  • Shifts:
    • Signed left: sal(c, s) -> const(c << s) for signed integers.
    • Signed right: sar(c, s) -> const(c >> s) for signed integers.
    • Unsigned left: shl(c, s) -> const(c << s) for unsigned integers.
    • Unsigned right:shr(c, s) -> const(c >> s) for unsigned integers.
  • Bitwise NOT: not(c) -> const(~c) for signed and unsigned integers.
  • Comparisons (result is a boolean constant) for integer, floating-point and boolean operands:
    • cmp.eq(c1, c2) -> const(c1 == c2)
    • cmp.neq(c1, c2) -> const(c1 != c2)
    • cmp.a(c1, c2) -> const(c1 > c2)
    • cmp.b(c1, c2) -> const(c1 < c2)
    • cmp.ae(c1, c2) -> const(c1 >= c2)
    • cmp.be(c1, c2) -> const(c1 <= c2)

IR before ArithConstFoldPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param -> (v21)
c2.si arith.const (10) -> (v10,v17)
c3.si arith.const (3) -> (v10,v11,v17,v19)
c4.ui arith.const (7) -> (v12,v13,v16)
c5.ui arith.const (2) -> (v12,v16)
c6.f arith.const (1.5) -> (v14)
c7.f arith.const (0.5) -> (v14,v15)
c8.b arith.const (true) -> (v18)
c9.b arith.const (false) -> (v18)
v10.si arith.add (c2 : si, c3 : si) -> (v11)
v11.si arith.mul (v10 : si, c3 : si) -> (v20)
v12.ui arith.and (c4 : ui, c5 : ui) -> (v13)
v13.ui arith.xor (v12 : ui, c4 : ui) -> (v21)
v14.f arith.add (c6 : f, c7 : f) -> (v15)
v15.f arith.div (v14 : f, c7 : f) -> (v23)
v16.ui arith.shl (c4 : ui, c5 : ui)
v17.si arith.sar (c2 : si, c3 : si)
v18.b arith.cmp.eq (c8 : b, c9 : b) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (v11 : si, v19 : si) -> (v24)
v21.ui arith.and (v13 : ui, a1 : ui)
v22.si arith.cast (v18 : b) -> (v24)
v23.si arith.cast (v15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

IR after ArithConstFoldPass + DCEPass:

constfold:
^bb0 <start> <final> :
a0.si builtin.param -> (v19)
a1.ui builtin.param
c3.si arith.const (3) -> (v19)
c11.si arith.const (39) -> (v20)
c15.f arith.const (4) -> (v23)
c18.b arith.const (false) -> (v22)
v19.si arith.add (a0 : si, c3 : si) -> (v20)
v20.si arith.add (c11 : si, v19 : si) -> (v24)
v22.si arith.cast (c18 : b) -> (v24)
v23.si arith.cast (c15 : f) -> (v25)
v24.si arith.add (v20 : si, v22 : si) -> (v25)
v25.si arith.add (v24 : si, v23 : si) -> (n26)
n26 ctrlflow.return (v25 : si)

See corresponding example - examples/ConstFold. See corresponding tests - tests/opt/arith/constfold.cpp.

Peephole patterns (ArithPeepHolePass):

  • add:
    • add(x, 0) -> x
    • add(0, x) -> x
  • sub:
    • sub(x, 0) -> x
    • sub(x, x) -> 0
  • mul:
    • mul(x, 1) -> x
    • mul(1, x) -> x
    • mul(x, 0) -> 0
    • mul(0, x) -> 0
  • div:
    • div(x, 1) -> x
  • and:
    • and(x, 0) -> 0
    • and(0, x) -> 0
    • and(x, 11..1) -> x
    • and(11..1, x) -> x
    • and(x, x) -> x
  • or:
    • or(x, 0) -> x
    • or(0, x) -> x
    • or(x, 11..1) -> 11..1
    • or(11..1, x) -> 11..1
    • or(x, x) -> x
  • xor:
    • xor(x, 0) -> x
    • xor(0, x) -> x
    • xor(x, x) -> 0
    • xor(x, 11..1) -> ~x
    • xor(11..1, x) -> ~x
  • Shifts (arithmetical sal/sar, logical shl/shr):
    • sal(x, 0) -> x, sal(0, x) -> 0
    • shl(x, 0) -> x, shl(0, x) -> 0
    • sar(x, 0) -> x, sar(0, x) -> 0
    • shr(x, 0) -> x, shr(0, x) -> 0
  • Double negation:
    • not(not(x)) -> x
  • To expose constant–constant sub-expressions for ArithConstFoldPass, the peephole pass applies rotate patterns to integer add/mul when an inner operation is used only by a single outer operation:
    • add:
      • (x + C1) + C2 -> x + (C1 + C2)
      • (C1 + x) + C2 -> x + (C1 + C2)
      • C2 + (x + C1) -> x + (C1 + C2)
      • C2 + (C1 + x) -> x + (C1 + C2)
    • mul:
      • (C1 * x) * C2 -> x * (C1 * C2)
      • (x * C1) * C2 -> x * (C1 * C2)
      • C2 * (C1 * x) -> x * (C1 * C2)
      • C2 * (x * C1) -> x * (C1 * C2)

IR before ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v10,v11,v12,v14,v15,v16,v18,v34,v35,v36,v37,v50)
a1.si builtin.param -> (v13,v13,v17,v54)
a2.ui builtin.param -> (v19,v20,v21,v22,v24,v25,v26,v27,v29,v30,v32,v33,v38,v39,v40,v41,v42)
a3.ui builtin.param -> (v23,v23,v28,v28,v31,v31)
c4.si arith.const (0) -> (v10,v11,v12,v16,v17,v34,v35,v36,v37)
c5.si arith.const (1) -> (v14,v15,v18)
c6.ui arith.const (0) -> (v19,v20,v24,v25,v29,v30,v38,v39,v40,v41)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615) -> (v21,v22,v26,v27,v32,v33)
v10.si arith.add (a0 : si, c4 : si) -> (v44)
v11.si arith.add (c4 : si, a0 : si)
v12.si arith.sub (a0 : si, c4 : si) -> (v44)
v13.si arith.sub (a1 : si, a1 : si)
v14.si arith.mul (a0 : si, c5 : si) -> (v45)
v15.si arith.mul (c5 : si, a0 : si)
v16.si arith.mul (a0 : si, c4 : si)
v17.si arith.mul (c4 : si, a1 : si)
v18.si arith.div (a0 : si, c5 : si) -> (v45)
v19.ui arith.and (a2 : ui, c6 : ui)
v20.ui arith.and (c6 : ui, a2 : ui)
v21.ui arith.and (a2 : ui, c9 : ui) -> (v46)
v22.ui arith.and (c9 : ui, a2 : ui)
v23.ui arith.and (a3 : ui, a3 : ui)
v24.ui arith.or (a2 : ui, c6 : ui)
v25.ui arith.or (c6 : ui, a2 : ui)
v26.ui arith.or (a2 : ui, c9 : ui) -> (v46)
v27.ui arith.or (c9 : ui, a2 : ui)
v28.ui arith.or (a3 : ui, a3 : ui)
v29.ui arith.xor (a2 : ui, c6 : ui)
v30.ui arith.xor (c6 : ui, a2 : ui)
v31.ui arith.xor (a3 : ui, a3 : ui)
v32.ui arith.xor (a2 : ui, c9 : ui)
v33.ui arith.xor (c9 : ui, a2 : ui)
v34.si arith.sal (a0 : si, c4 : si)
v35.si arith.sal (c4 : si, a0 : si)
v36.si arith.sar (a0 : si, c4 : si)
v37.si arith.sar (c4 : si, a0 : si)
v38.ui arith.shl (a2 : ui, c6 : ui)
v39.ui arith.shl (c6 : ui, a2 : ui)
v40.ui arith.shr (a2 : ui, c6 : ui)
v41.ui arith.shr (c6 : ui, a2 : ui)
v42.ui arith.not (a2 : ui) -> (v43)
v43.ui arith.not (v42 : ui) -> (n56)
v44.si arith.add (v10 : si, v12 : si) -> (v47)
v45.si arith.add (v14 : si, v18 : si) -> (v47)
v46.ui arith.and (v21 : ui, v26 : ui)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v51)
v50.si arith.add (a0 : si, c48 : si) -> (v51)
v51.si arith.add (v50 : si, c49 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v55)
v54.si arith.mul (c52 : si, a1 : si) -> (v55)
v55.si arith.mul (v54 : si, c53 : si)
n56 ctrlflow.return (v43 : ui)

IR after ArithPeepHolePass:

peephole:
^bb0 <start> <final> :
a0.si builtin.param -> (v44,v44,v45,v45,v51)
a1.si builtin.param -> (v55)
a2.ui builtin.param -> (v32,v33,n56)
a3.ui builtin.param
c4.si arith.const (0)
c5.si arith.const (1)
c6.ui arith.const (0)
c7.ui arith.const (1)
c8.si arith.const (-1)
c9.ui arith.const (18446744073709551615)
c13.si arith.const (0)
c16.si arith.const (0)
c17.si arith.const (0)
c19.ui arith.const (0)
c20.ui arith.const (0)
c31.ui arith.const (0)
v32.ui arith.not (a2 : ui)
v33.ui arith.not (a2 : ui)
c35.si arith.const (0)
c37.si arith.const (0)
c39.ui arith.const (0)
c41.ui arith.const (0)
v44.si arith.add (a0 : si, a0 : si) -> (v47)
v45.si arith.add (a0 : si, a0 : si) -> (v47)
v47.si arith.add (v44 : si, v45 : si)
c48.si arith.const (10) -> (v50)
c49.si arith.const (5) -> (v50)
v50.si arith.add (c48 : si, c49 : si) -> (v51)
v51.si arith.add (a0 : si, v50 : si)
c52.si arith.const (2) -> (v54)
c53.si arith.const (3) -> (v54)
v54.si arith.mul (c52 : si, c53 : si) -> (v55)
v55.si arith.mul (a1 : si, v54 : si)
n56 ctrlflow.return (a2 : ui)

See corresponding example - examples/PeepHole. See corresponding tests - tests/opt/arith/peephole.cpp.

Building examples

Requirements:

  • CMake >= 3.21
  • C++ compiler supporting C++23 standard

To build project, use following commads from the project's roor directory:

cmake -B build
cmake --build build

If you want to build specific target, use following command:

cmake --build build --target <target_name>

List of available targets:

  • iris - IR-support static library;
  • factorial - example of IR usage - building IR for a programm calculating factorial manually via iris::IRBuilder;
  • domExample01 - domExample03 - examples of running Dom analysis and building IDom tree;
  • loopExample01 - loopExample06 - examples of running Loop analysis and building Loop tree;
  • DCE - example of optimizing IR with use of DCE;
  • ConstFold - example of optimizing IR with use of Constant Folding;
  • PeepHole - example of optimizing IR with use of PeepHole optimizations.

Static library output files are located in build/lib/, and executables - in build/bin.

Testing

Project includes tests, impemented using GTest.

To build and run tests, run following commands from project's root directory:

cmake -B build
cmake --build build --target iris_tests
cd build
ctest

Tests include:

  • Dominator analysis tests (tests/doms.cpp);
  • Loop analysis tests (tests/loops.cpp).
  • Separate test suites for each dialect and its operations' verifiers and construction pre-conditions (tests/dialects/);
  • Integration tests (tests/integration.cpp);
  • Optimizationpasses & patterns tests (tests/opt/):
    • Dead Code Elimination (common/dce.cpp);
    • Constant Folding for arith operations (arith/constfold.cpp);
    • PeepHole Optimizations for arith operations (arith/peephole.cpp).
  • Tests of verifiers (ex-fails) and API tests for:
    • Generic operation iris::Operation;
    • Basic block iris::BasicBlock;
    • Region iris::Region;
    • IR-builder iris::IRBuilder;
    • Dominator analysis helper class iris::doms::DomInfo;
    • Loop analysis helper class iris::doms::LoopInfo (TODO).

About

Intermediate Representation Internal Structure - SSA-like IR developed as an educational project

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages