Repository: QuTech-Delft/OpenQL (language: python)

Issue:
Support for parameterized instructions
In https://github.com/QuTech-Delft/OpenQL/issues/419, support for parameterized gate **decompositions** was tracked. It was concluded that such support already exists and works, but is only meaningful if the parameter can somehow be consumed, either by supporting parameterized gates at the backend level, or by allowing the following type of decompositions to work (with a much longer if-chain in practice):
```
// Parameterized gate decompositions (test)
        "_rx": {
           "prototype": ["X:qubit", "L:int"],
           "duration": @MW_DURATION@,
           "decomposition": {
               "into": [  // FIXME: makes no real sense, and currently fails in backend
                    "if (op(1) < 45) {",
                    "   rx45 op(0)",
                    "} else {",
                    "   rx90 op(0)",
                    "}"
                ]
           }
```

The latter approach currently fails because the backend doesn't support constant expressions (e.g. `23 < 45`), and that is the case because it was earlier decided that such support would be better offloaded to a separate pass, instead of polluting the backend (see https://github.com/QuTech-Delft/OpenQL/issues/424). Such a pass wouldn't only support paramerized instructions, but also adds generic support for constant expressions in other contexts.
Constant propagation & dead code removal
To cater for #448, this PR provides two new passes: constant propagation and dead code removal. In addition to support for parameterized instructions, this indirectly also adds support for constant expressions to the CC.
Since these are separate passes, they may also be useful in other contexts than the CC backend.


Discussion / hints:

Fix patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fea751b59..c44ee8a9d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -310,6 +310,9 @@ add_library(ql
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ql/pass/dec/structure/structure.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ql/pass/opt/clifford/detail/clifford.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ql/pass/opt/clifford/optimize.cc"
+    "${CMAKE_CURRENT_SOURCE_DIR}/src/ql/pass/opt/const_prop/detail/propagate.cc"
+    "${CMAKE_CURRENT_SOURCE_DIR}/src/ql/pass/opt/const_prop/const_prop.cc"
+    "${CMAKE_CURRENT_SOURCE_DIR}/src/ql/pass/opt/dead_code_elim/dead_code_elim.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ql/pass/sch/schedule/detail/scheduler.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ql/pass/sch/schedule/schedule.cc"
     "${CMAKE_CURRENT_SOURCE_DIR}/src/ql/pass/sch/list_schedule/list_schedule.cc"
diff --git a/include/ql/pass/opt/const_prop/const_prop.h b/include/ql/pass/opt/const_prop/const_prop.h
new file mode 100644
index 000000000..3a18e8d29
--- /dev/null
+++ b/include/ql/pass/opt/const_prop/const_prop.h
@@ -0,0 +1,62 @@
+/** \file
+ * Constant propagation pass.
+ */
+
+#pragma once
+
+#include "ql/pmgr/pass_types/specializations.h"
+
+namespace ql {
+namespace pass {
+namespace opt {
+namespace const_prop {
+
+/**
+ * Constant propagation pass.
+ */
+class ConstantPropagationPass : public pmgr::pass_types::Transformation {
+public:
+
+    /**
+     * Constructs a constant propagation pass.
+     */
+    ConstantPropagationPass(
+        const utils::Ptr<const pmgr::Factory> &pass_factory,
+        const utils::Str &instance_name,
+        const utils::Str &type_name
+    );
+
+    /**
+     * Returns a user-friendly type name for this pass.
+     */
+    utils::Str get_friendly_type() const override;
+
+    /**
+     * Runs the constant propagation pass.
+     */
+    utils::Int run(
+        const ir::Ref &ir,
+        const pmgr::pass_types::Context &context
+    ) const override;
+
+protected:
+
+    /**
+     * Dumps docs for constant propagation pass.
+     */
+    void dump_docs(
+        std::ostream &os,
+        const utils::Str &line_prefix
+    ) const override;
+
+};
+
+/**
+ * Shorthand for referring to the pass using namespace notation.
+ */
+using Pass = ConstantPropagationPass;
+
+} // namespace const_prop
+} // namespace opt
+} // namespace pass
+} // namespace ql
diff --git a/include/ql/pass/opt/dead_code_elim/dead_code_elim.h b/include/ql/pass/opt/dead_code_elim/dead_code_elim.h
new file mode 100644
index 000000000..5d2cf1f3b
--- /dev/null
+++ b/include/ql/pass/opt/dead_code_elim/dead_code_elim.h
@@ -0,0 +1,71 @@
+/** \file
+ * Dead code elimination pass.
+ */
+
+#pragma once
+
+#include "ql/pmgr/pass_types/specializations.h"
+
+namespace ql {
+namespace pass {
+namespace opt {
+namespace dead_code_elim {
+
+/**
+ * Dead code elimination pass.
+ */
+class DeadCodeEliminationPass : public pmgr::pass_types::Transformation {
+public:
+
+    /**
+     * Constructs a dead code elimination pass.
+     */
+    DeadCodeEliminationPass(
+        const utils::Ptr<const pmgr::Factory> &pass_factory,
+        const utils::Str &instance_name,
+        const utils::Str &type_name
+    );
+
+    /**
+     * Returns a user-friendly type name for this pass.
+     */
+    utils::Str get_friendly_type() const override;
+
+    /**
+     * Runs the dead code elimination pass.
+     */
+    utils::Int run(
+        const ir::Ref &ir,
+        const pmgr::pass_types::Context &context
+    ) const override;
+
+protected:
+
+    /**
+     * Dumps docs for dead code elimination pass.
+     */
+    void dump_docs(
+        std::ostream &os,
+        const utils::Str &line_prefix
+    ) const override;
+
+private:
+    /**
+     * Runs the dead code elimination pass on the given block.
+     */
+    static void run_on_block(
+        const ir::BlockBaseRef &block,
+        utils::UInt level = 0
+    );
+
+};
+
+/**
+ * Shorthand for referring to the pass using namespace notation.
+ */
+using Pass = DeadCodeEliminationPass;
+
+} // namespace dead_code_elim
+} // namespace opt
+} // namespace pass
+} // namespace ql
diff --git a/src/ql/ir/old_to_new.cc b/src/ql/ir/old_to_new.cc
index 4eef53c74..a0f0bf45f 100644
--- a/src/ql/ir/old_to_new.cc
+++ b/src/ql/ir/old_to_new.cc
@@ -937,7 +937,7 @@ Ref convert_old_to_new(const compat::PlatformRef &old) {
     utils::Str architecture = ir->platform->architecture->family->get_namespace_name();
 
     if(architecture == "cc") {
-        QL_WOUT("adding hardcoded CC functions");
+        QL_IOUT("adding hardcoded CC functions");
         fn = add_function_type(ir, utils::make<FunctionType>("rnd_seed"));
         fn->operand_types.emplace(prim::OperandMode::READ, int_type);   // seed
         fn->return_type = int_type;
diff --git a/src/ql/pass/opt/const_prop/const_prop.cc b/src/ql/pass/opt/const_prop/const_prop.cc
new file mode 100644
index 000000000..53791a913
--- /dev/null
+++ b/src/ql/pass/opt/const_prop/const_prop.cc
@@ -0,0 +1,63 @@
+/** \file
+ * Constant propagation pass.
+ */
+
+#include "ql/pass/opt/const_prop/const_prop.h"
+#include "detail/propagate.h"
+
+#include "ql/pmgr/pass_types/base.h"
+
+namespace ql {
+namespace pass {
+namespace opt {
+namespace const_prop {
+
+/**
+ * Constructs a constant propagation pass.
+ */
+ConstantPropagationPass::ConstantPropagationPass(
+    const utils::Ptr<const pmgr::Factory> &pass_factory,
+    const utils::Str &instance_name,
+    const utils::Str &type_name
+) : pmgr::pass_types::Transformation(pass_factory, instance_name, type_name) {
+}
+
+/**
+ * Returns a user-friendly type name for this pass.
+ */
+utils::Str ConstantPropagationPass::get_friendly_type() const {
+    return "Constant propagator";
+}
+
+/**
+ * Runs the constant propagation pass.
+ */
+utils::Int ConstantPropagationPass::run(
+    const ir::Ref &ir,
+    const pmgr::pass_types::Context &context
+) const {
+    // perform constant propagation
+    if (!ir->program.empty()) {
+        for (auto &block : ir->program->blocks) {
+            detail::propagate(ir, *block);
+        }
+    }
+    return 0;
+}
+
+/**
+ * Dumps docs for constant propagation pass.
+ */
+void ConstantPropagationPass::dump_docs(
+    std::ostream &os,
+    const utils::Str &line_prefix
+) const {
+    utils::dump_str(os, line_prefix, R"(
+    This pass replaces constant expressions by their result.
+    )");
+}
+
+} // namespace const_prop
+} // namespace opt
+} // namespace pass
+} // namespace ql
diff --git a/src/ql/pass/opt/const_prop/detail/platform_functions.h b/src/ql/pass/opt/const_prop/detail/platform_functions.h
new file mode 100644
index 000000000..02a7e3395
--- /dev/null
+++ b/src/ql/pass/opt/const_prop/detail/platform_functions.h
@@ -0,0 +1,54 @@
+#pragma once
+
+/*
+ * Define platform functions using the venerable X macro mechanism
+ *
+ * We only need to implement libqasm functions which are overridden by OpenQL, and - since libqasm supports
+ * polymorphism - only for the parameter types overridden (i.e. bit and int).
+ * Also see the comment in arch::cc::pass::gen::vq1asm::detail::codegen.h::handle_set_instruction() on how platform
+ * functions come into existence.
+ *
+ * Return types (R_*):
+ * - R_B: bit
+ * - R_I: integer
+ * Parameter types (P_*):
+ * - P_B: bit literal
+ * - P_I: integer literal
+ *
+ * Based on:
+ * - deps/libqasm/src/cqasm/func-gen/funcgen.cpp
+ * - <build>/deps/libqasm/src/cqasm/cqasm-v1-functions-gen.cpp
+ * - src/ql/arch/cc/pass/gen/vq1asm/detail/functions.cc
+ *
+ * Note: to have better guarantees on consistency, we might also use this file in ir/old_to_new.cc to create the
+ * platform functions, and extend it to be usable for ir/operator_info.cc
+ */
+
+//name		        ret-type    par0    par1    operation   func
+#define PLATFORM_FUNCTION_LIST \
+/* Basic scalar arithmetic operators. */ \
+X2("operator+",     R_I,        P_I,    P_I,    a + b,      op_add) \
+X2("operator-",     R_I,        P_I,    P_I,    a - b,      op_sub) \
+X2("operator*",     R_I,        P_I,    P_I,    a * b,      op_mul) \
+/* Relational operators. */              \
+X2("operator==",    R_B,        P_I,    P_I,    a == b,     op_eq) \
+X2("operator!=",    R_B,        P_I,    P_I,    a != b,     op_ne) \
+X2("operator>=",    R_B,        P_I,    P_I,    a >= b,     op_ge) \
+X2("operator>",     R_B,        P_I,    P_I,    a > b,      op_gt) \
+X2("operator<=",    R_B,        P_I,    P_I,    a <= b,     op_le) \
+X2("operator<",     R_B,        P_I,    P_I,    a < b,      op_lt) \
+/* Bitwise operators. */                 \
+X2("operator&",     R_I,        P_I,    P_I,    a & b,      op_band) \
+X2("operator|",     R_I,        P_I,    P_I,    a | b,      op_bor) \
+X2("operator^",     R_I,        P_I,    P_I,    a ^ b,      op_bxor) \
+/* Logical operators. */                 \
+X2("operator==",    R_B,        P_B,    P_B,    a == b,     op_eq) \
+X2("operator!=",    R_B,        P_B,    P_B,    a != b,     op_ne) \
+X2("operator&&",    R_B,        P_B,    P_B,    a && b,     op_land) \
+X2("operator||",    R_B,        P_B,    P_B,    a || b,     op_lor) \
+X2("operator^^",    R_B,        P_B,    P_B,    !a ^ !b,    op_lxor) \
+/* Unary functions */\
+X1("operator-",     R_I,        P_I,            -a,         op_neg) \
+X1("operator~",     R_I,        P_I,            ~a,         op_binv) \
+X1("operator!",     R_I,        P_B,            !a,         op_linv) \
+X1("int",           R_I,        P_B,            (int)a,     op_int)
diff --git a/src/ql/pass/opt/const_prop/detail/propagate.cc b/src/ql/pass/opt/const_prop/detail/propagate.cc
new file mode 100644
index 000000000..54f9978d6
--- /dev/null
+++ b/src/ql/pass/opt/const_prop/detail/propagate.cc
@@ -0,0 +1,224 @@
+/** \file
+ * Recursively perform constant propagation on an IR node
+ */
+
+#define DEBUG(x) QL_DOUT(x)
+
+#include "propagate.h"
+#include "platform_functions.h"
+
+#include "ql/ir/describe.h"
+#include "ql/ir/ops.h"
+
+namespace ql {
+namespace pass {
+namespace opt {
+namespace const_prop {
+namespace detail {
+
+// type definitions for platform functions we implement here
+using FncArgs = const utils::Any<ir::Expression>;
+using FncRet = utils::One<ir::Expression>;
+
+
+// create the functions from PLATFORM_FUNCTION_LIST
+// the identifier consists of the concatenation of func, "_" and the (non macro expanded) parameters, e.g.
+// "ap_add_P_IP_I". Doesn't look great, but you never see it anyway.
+#define P_I as_int_literal()
+#define P_B as_bit_literal()
+#define R_I(ir, x) make_int_lit(ir, x)   // NB: performs checking against IR types (thus disallowing integer overflow)
+#define R_B(ir, x) make_bit_lit(ir, x)   // NB: performs checking against IR types
+
+#define X2(name, ret_type, par0, par1, operation, func)                     \
+static FncRet func ## _ ## par0 ## par1(const ir::Ref &ir, FncArgs &args) { \
+    auto a = args[0]->par0->value;                                          \
+    auto b = args[1]->par1->value;                                          \
+    return ret_type(ir, operation);                                         \
+}
+#define X1(name, ret_type, par0, operation, func)                           \
+static FncRet func ## _ ## par0 (const ir::Ref &ir, FncArgs &args) {        \
+    auto a = args[0]->par0->value;                                          \
+    return ret_type(ir, operation);                                         \
+}
+
+PLATFORM_FUNCTION_LIST
+
+#undef X1
+#undef X2
+#undef R_B
+#undef R_I
+#undef P_B
+#undef P_I
+
+
+class ConstantPropagator : public ir::RecursiveVisitor {
+public:
+
+    /**
+     * Constructs a ConstantPropagator.
+     */
+    explicit ConstantPropagator(const ir::Ref &ir) : ir(ir) {
+        register_functions();
+    };
+
+private:  // RecursiveVisitor overrides
+
+    /**
+     * Fallback function.
+     */
+    void visit_node(ir::Node &node) override {
+        // Note that in our case it is not an error to come here, since we don't intend to handle all
+        // node types
+        DEBUG("visiting node '"  << ir::describe(node) << "'");
+    }
+
+    /**
+     * Visitor function for `SetInstruction` nodes.
+     */
+
+    void visit_set_instruction(ir::SetInstruction &set_instruction) override {
+        handle_expression(set_instruction.rhs);
+    }
+
+    /**
+     * Visitor function for `DynamicLoop` nodes.
+     */
+
+    void visit_dynamic_loop(ir::DynamicLoop &dynamic_loop) override {
+        handle_expression(dynamic_loop.condition);
+
+        // descend loop body
+        dynamic_loop.body->visit(*this);
+    }
+
+    /**
+     * Visitor function for `IfElseBranch` nodes.
+     */
+    void visit_if_else_branch(ir::IfElseBranch &if_else_branch) override {
+        handle_expression(if_else_branch.condition);
+
+        // descend branch body
+        if_else_branch.body->visit(*this);
+    }
+
+    /**
+     * Visitor function for `FunctionCall` nodes.
+     */
+    void visit_function_call(ir::FunctionCall &function_call) override {
+        DEBUG("recursing into operands of function call '" << ir::describe(function_call) << "'");
+        for (auto &operand : function_call.operands) {
+            handle_expression(operand);
+        }
+    }
+
+    /**
+     * Visitor function for `FunctionCall` nodes.
+     */
+    void visit_expression(ir::Expression &expression) override {
+        if(auto function_call = expression.as_function_call()) {
+            DEBUG("recursing into function call '" << ir::describe(*function_call) << "'");
+            visit_function_call(*function_call);   // descend
+        }
+    }
+
+private:    // types
+
+    // function pointer
+    typedef FncRet (*tOpFunc)(const ir::Ref &ir, const FncArgs &args);
+
+    // map function key (see register_functions()) to function pointer
+    using FuncMap = std::map<utils::Str, tOpFunc>;
+
+private:    // vars
+    const ir::Ref &ir;
+    FuncMap func_map;
+
+private:    // functions
+
+    /*
+     * Handle an expression node, i.e. replace eligible functions calls with a literal expression.
+     *
+     * Note that we cannot directly use visit_expression(), because that has a 'ir::Expression &' as parameter.
+     * It is therefore not possible to change a function_call into a (say) int_literal. Here the expression is
+     * wrapped in a 'utils::One', allowing polymorphic access. This does force us however to visit all relevant node
+     * types that contain an expression (although we can skip e.g. custom_instruction.operands).
+     */
+    void handle_expression(utils::One<ir::Expression> &expression) {
+        DEBUG("descending '" << ir::describe(expression));
+        visit_expression(*expression);    // descend
+        DEBUG("done descending '" << ir::describe(expression));
+
+        if (auto function_call = expression->as_function_call()) {
+            QL_IOUT("function call '" << ir::describe(*function_call) << "'");
+
+            // generate key, consistent with register_functions()
+            utils::Str key = function_call->function_type->name + "_";
+            for (auto &operand : function_call->operands) {
+                if (operand->as_int_literal()) {
+                    key += "i";
+                } else if (operand->as_bit_literal()) {
+                    key += "b";
+                } else {
+                    DEBUG("not touching operand '" << ir::describe(operand) << "'");
+                    return;     // we don't handle functions with other operand types, but leave them untouched
+                }
+            }
+
+            // NB: we don't perform type promotions like libqasm's cQASM resolver, see FunctionTable::call
+
+            // lookup key
+            auto it = func_map.find(key);
+            if (it == func_map.end()) {
+                DEBUG("ignoring non-eligible function '" << key << "'");
+
+            } else {
+                // call the function
+                FncRet ret = (*it->second)(ir, function_call->operands);
+
+                // replace node
+                QL_IOUT("replacing '" << ir::describe(*function_call) << "' by '" << ir::describe(*ret) << "'");
+                expression = ret;
+            }
+        }
+    }
+
+    /**
+     * Register the functions we handle.
+     */
+    void register_functions() {
+        // Initialize func_map with the functions from PLATFORM_FUNCTION_LIST
+        // The key consists of the concatenation of name, "_" and the stringified profile, e.g "operator+_ii"
+
+        #define str(x) #x
+        #define xstr(x) str(x)
+        #define P_I i
+        #define P_B b
+        #define X2(name, ret_type, par0, par1, operation, func) { name "_" xstr(par0) xstr(par1), &(func ## _ ## par0 ## par1) },
+        #define X1(name, ret_type, par0,       operation, func) { name "_" xstr(par0)           , &(func ## _ ## par0        ) },
+        func_map = {
+            PLATFORM_FUNCTION_LIST
+        };
+        #undef X1
+        #undef X2
+        #undef P_B
+        #undef P_I
+        #undef xstr
+        #undef str
+    };
+
+};
+
+/**
+ * Recursively perform constant propagation on an IR node.
+ */
+void propagate(const ir::Ref &ir, ir::Node &node) {
+    ConstantPropagator visitor(ir);
+
+    node.visit(visitor);
+}
+
+} // namespace detail
+} // namespace const_prop
+} // namespace opt
+} // namespace pass
+} // namespace ql
diff --git a/src/ql/pass/opt/const_prop/detail/propagate.h b/src/ql/pass/opt/const_prop/detail/propagate.h
new file mode 100644
index 000000000..fce91b6f5
--- /dev/null
+++ b/src/ql/pass/opt/const_prop/detail/propagate.h
@@ -0,0 +1,27 @@
+/** \file
+ * Recursively perform constant propagation on an IR node
+ */
+
+#pragma once
+
+#include "ql/ir/ir.h"
+
+namespace ql {
+namespace pass {
+namespace opt {
+namespace const_prop {
+namespace detail {
+
+/**
+ * Recursively perform constant propagation on an IR node.
+ */
+void propagate(
+    const ir::Ref &ir,
+    ir::Node &node
+);
+
+} // namespace detail
+} // namespace const_prop
+} // namespace opt
+} // namespace pass
+} // namespace ql
diff --git a/src/ql/pass/opt/dead_code_elim/dead_code_elim.cc b/src/ql/pass/opt/dead_code_elim/dead_code_elim.cc
new file mode 100644
index 000000000..914369979
--- /dev/null
+++ b/src/ql/pass/opt/dead_code_elim/dead_code_elim.cc
@@ -0,0 +1,178 @@
+/** \file
+ * Dead code elimination pass.
+ */
+
+#include "ql/pass/opt/dead_code_elim/dead_code_elim.h"
+
+#include "ql/pmgr/pass_types/base.h"
+#include "ql/ir/describe.h"
+
+#define DEBUG(x) QL_DOUT(x)
+
+namespace ql {
+namespace pass {
+namespace opt {
+namespace dead_code_elim {
+
+/**
+ * Constructs a dead code elimination pass.
+ */
+DeadCodeEliminationPass::DeadCodeEliminationPass(
+    const utils::Ptr<const pmgr::Factory> &pass_factory,
+    const utils::Str &instance_name,
+    const utils::Str &type_name
+) : pmgr::pass_types::Transformation(pass_factory, instance_name, type_name) {
+}
+
+/**
+ * Returns a user-friendly type name for this pass.
+ */
+utils::Str DeadCodeEliminationPass::get_friendly_type() const {
+    return "Dead code eliminator";
+}
+
+/**
+ * Runs the dead code elimination pass on the given block.
+ */
+void DeadCodeEliminationPass::run_on_block(
+    const ir::BlockBaseRef &block,
+    utils::UInt level
+) {
+    utils::Str block_name = block->as_block() ? block->as_block()->name : QL_SS2S("(anon[" << level << "]");
+    DEBUG(block_name << ": running dead code elimination");
+    for (utils::UInt stmt_idx = 0; stmt_idx < block->statements.size(); stmt_idx++) {    // NB: we need index for add() below
+        const auto &statement = block->statements[stmt_idx];
+
+        // handle if_else.
+        //
+        // Note that this is especially useful for 'parameterized gate decomposition', e.g. (noting that a real
+        // example would feature a longer if-tree):
+        //         "_rx": {
+        //           "prototype": ["X:qubit", "L:int"],
+        //           "duration": 20,
+        //           "decomposition": {
+        //               "into": [
+        //                     "if (op(1) < 45) {",
+        //                    "   rx45 op(0)",
+        //                    "} else {",
+        //                    "   rx90 op(0)",
+        //                    "}"
+        if (auto if_else = statement->as_if_else()) {
+            utils::UInt if_else_idx = stmt_idx;
+            bool remove_if_else = false;
+
+            // remove unreachable branches, and maybe the complete if_else statement
+            for (utils::UInt branch_idx = 0; branch_idx < if_else->branches.size(); ) {    // NB: we need index for remove() below
+                auto &branch = if_else->branches[branch_idx];
+                if (auto condition = branch->condition->as_bit_literal()) {
+                    if (condition->value) {   // condition 'true'
+                        // descend body
+                        run_on_block(branch->body, level+1);
+
+                        // delete subsequent if_else branches and if_else->otherwise, since these are unreachable
+                        DEBUG(block_name << ": found 'if_else(true)': removing unreachable if_else-branches and if_else->otherwise");
+                        for (utils::UInt i = branch_idx + 1; i < if_else->branches.size(); i++) {
+                            if_else->branches.remove(i);
+                        }
+                        if_else->otherwise.reset();     // removes Maybe item
+
+                        // if we're the sole if_else-branch (remaining), turn body (sub_block) into statements
+                        if(branch_idx == 0) {
+                            DEBUG(block_name << ": turn body of sole 'if(true)' branch into statements");
+                            for (auto &st : branch->body->statements) {
+                                block->statements.add(st, ++stmt_idx);  // NB: increment affects both this add and statement loop
+                            }
+
+                            // mark complete if_else statement for removal
+                            remove_if_else = true;
+                        }
+
+                        // NB: done looping, since we erased everything that followed
+                        break;
+
+                    } else {    // condition 'false'
+                        // NB: no need to descend body, since we'll discard it
+                        DEBUG(block_name << ": removing dead if-branch " << branch_idx);
+                        if_else->branches.remove(branch_idx);
+                        continue;   // loop, using same value for branch_idx
+
+                        // NB: may remove all if_else branches (which we repair later)
+                    }
+
+                // condition is not a bit_literal
+                } else {
+                    // descend body
+                    run_on_block(branch->body, level+1);
+                }
+                branch_idx++;
+            }
+
+            // descend otherwise
+            if (!if_else->otherwise.empty()) {
+                run_on_block(if_else->otherwise, level+1);
+            }
+
+            // if we no longer have branches, but do have otherwise, promote its body into statements within this block
+            if (if_else->branches.empty()) {
+                if (!if_else->otherwise.empty()) {
+                    DEBUG(block_name << ": turn body of final 'if_else->otherwise' into statements");
+
+                    // move if_else->otherwise statements
+                    for (auto &st : if_else->otherwise->statements) {
+                        block->statements.add(st, ++stmt_idx);  // NB: increment affects both this add and statement loop
+                    }
+                }
+
+                remove_if_else = true;
+            }
+
+            // if needed, remove the complete if_else statement altogether
+            if (remove_if_else) {
+                DEBUG(block_name << ": removing if_else (if_else_idx=" << if_else_idx << ", block->statements.size()=" << block->statements.size() << ")");
+                block->statements.remove(if_else_idx);
+            }
+
+        // handle loop
+        } else if (auto loop = statement->as_loop()) {
+            // descend loop body
+            run_on_block(loop->body, level+1);
+
+            // NB: we currently have no real use for optimizing static loops, note that we cannot fully
+            // remove loop anyway if break or continue exists
+        }
+    }
+    DEBUG(block_name << ": done running dead code elimination");
+}
+
+/**
+ * Runs the dead code elimination pass.
+ */
+utils::Int DeadCodeEliminationPass::run(
+    const ir::Ref &ir,
+    const pmgr::pass_types::Context &context
+) const {
+    // perform dead code elimination
+    if (!ir->program.empty()) {
+        for (const auto &block : ir->program->blocks) {
+            run_on_block(block);
+        }
+    }
+    return 0;
+}
+
+/**
+ * Dumps docs for dead code elimination pass.
+ */
+void DeadCodeEliminationPass::dump_docs(
+    std::ostream &os,
+    const utils::Str &line_prefix
+) const {
+    utils::dump_str(os, line_prefix, R"(
+    This pass removes dead code, currently only unreachable if-branches.
+    )");
+}
+
+} // namespace dead_code_elim
+} // namespace opt
+} // namespace pass
+} // namespace ql
diff --git a/src/ql/pmgr/factory.cc b/src/ql/pmgr/factory.cc
index a73c1c815..13041ac91 100644
--- a/src/ql/pmgr/factory.cc
+++ b/src/ql/pmgr/factory.cc
@@ -21,6 +21,8 @@
 #include "ql/pass/dec/specialize/specialize.h"
 #include "ql/pass/dec/structure/structure.h"
 #include "ql/pass/opt/clifford/optimize.h"
+#include "ql/pass/opt/const_prop/const_prop.h"
+#include "ql/pass/opt/dead_code_elim/dead_code_elim.h"
 #include "ql/pass/sch/schedule/schedule.h"
 #include "ql/pass/sch/list_schedule/list_schedule.h"
 //#include "ql/pass/map/qubits/place_mip/place_mip.h" // Broken: need half-decent IR for gates and virtual vs real qubit operands first.
@@ -50,6 +52,8 @@ Factory::Factory() {
     register_pass<::ql::pass::dec::specialize::Pass>("dec.Specialize");
     register_pass<::ql::pass::dec::structure::Pass>("dec.Structure");
     register_pass<::ql::pass::opt::clifford::optimize::Pass>("opt.clifford.Optimize");
+    register_pass<::ql::pass::opt::const_prop::Pass>("opt.ConstProp");
+    register_pass<::ql::pass::opt::dead_code_elim::Pass>("opt.DeadCodeElim");
     register_pass<::ql::pass::sch::schedule::Pass>("sch.Schedule");
     register_pass<::ql::pass::sch::list_schedule::Pass>("sch.ListSchedule");
     //register_pass<::ql::pass::map::qubits::place_mip::Pass>("map.qubits.PlaceMIP"); // Broken: need half-decent IR for gates and virtual vs real qubit operands first.


Test patch:
diff --git a/tests/cc/test_cc_cqasm.py b/tests/cc/test_cc_cqasm.py
index 0e1523d6f..df4a1a501 100644
--- a/tests/cc/test_cc_cqasm.py
+++ b/tests/cc/test_cc_cqasm.py
@@ -31,42 +31,45 @@ def run_test_case(self, name):
             # ql.set_option('log_level', 'LOG_DEBUG')
             # ql.set_option('log_level', 'LOG_WARNING')
 
-            if 0:
-                # use compatibility
-                # fails in new-to-old IR conversion for CC backend
-                ql.set_option('write_qasm_files', 'yes')
-                ql.set_option('write_report_files', 'yes')
-                ql.compile(in_fn)
-
-            if 0:
-                # use compatibility
-                # fails in outputting debug info
-                ql.compile(
-                    in_fn,
-                    {
-                        # 'cqasm_file': in_fn,
-                        'output_prefix': 'test_output/%N.%P',
-                        'debug': 'yes'
-                    }
-                )
-
             if 1:
                 # use pass manager
                 pl = ql.Platform("cc", "config_cc_s17_direct_iq_openql_0_10.json")
                 c = pl.get_compiler()
 
+                # insert passes at front (in reverse run order)
                 if 1:
-                    # insert decomposer for legacy decompositions
-                    # See: see https://openql.readthedocs.io/en/latest/gen/reference_passes.html#instruction-decomposer
+                    # insert dead code elimination pass
+                    c.prefix_pass(
+                        'opt.DeadCodeElim',
+                        'dead_code_elim',
+                        {
+                            'output_prefix': 'test_output/%N.%P',
+                            'debug': 'yes'
+                        }
+                    )
+
+                    # insert constant propagation pass
                     c.prefix_pass(
-                        'dec.Instructions',
-                        'legacy',  # sets predicate key to use legacy decompositions (FIXME: TBC)
+                        'opt.ConstProp',
+                        'const_prop',
                         {
                             'output_prefix': 'test_output/%N.%P',
                             'debug': 'yes'
                         }
                     )
 
+
+                # insert decomposer for legacy decompositions (FIXME: and new style decompositions)
+                # See: see https://openql.readthedocs.io/en/latest/gen/reference_passes.html#instruction-decomposer
+                c.prefix_pass(
+                    'dec.Instructions',
+                    'legacy',  # sets predicate key to use legacy decompositions (FIXME: TBC)
+                    {
+                        'output_prefix': 'test_output/%N.%P',
+                        'debug': 'yes'
+                    }
+                )
+
                 # insert cQASM reader (as very first step)
                 c.prefix_pass(
                     'io.cqasm.Read',
@@ -78,8 +81,6 @@ def run_test_case(self, name):
                     }
                 )
 
-
-
                 # set scheduler options
                 # sch = c.get_pass('scheduler')
                 # sch.set_option('scheduler_target', 'asap')
@@ -109,6 +110,9 @@ def test_looping(self):
     def test_cond_gate(self):
         self.run_test_case('cond_gate')
 
+    def test_const_prop(self):
+        self.run_test_case('const_prop')
+
     # def test_from_string(self):
     #     pl = ql.Platform("cc", "config_cc_s17_direct_iq_openql_0_10.json")
     #     c = pl.get_compiler()
diff --git a/tests/cc/test_const_prop.cq b/tests/cc/test_const_prop.cq
new file mode 100644
index 000000000..d1dab547c
--- /dev/null
+++ b/tests/cc/test_const_prop.cq
@@ -0,0 +1,103 @@
+# File:         test_const_prop.cq
+# Purpose:      test cQasm constant propagation
+# Notes:        most constructs make no practical sense
+
+version 1.2
+qubits 17
+
+pragma @ql.name("test_const_prop")
+pragma @ql.platform("config_cc_s17_direct_iq_openql_0_10.json")
+
+.def
+map qInput1 = q[0]
+map qInput2 = q[1]
+map qAncilla3 = q[2]
+map qOutput4 = q[3]
+
+# var mAncilla3: bool   encountered incompatible object reference to mAncilla3 (size=0)
+map mAncilla3 = b[2]
+
+map i = creg(0)
+map j = creg(1)
+
+.condConstExpr
+while (4==4) {
+}
+
+.deadCodeElim
+if (3<2) {
+    set i = 1
+} else if (3<4) {
+    set i = 2
+    set j = 10
+} else {
+    set i = 3
+}
+
+.deadCodeElim2
+if (3<2) {
+    set i = 1
+} else if (2>3) {
+    set i = 3
+} else {
+    set i = 2
+    set j = 10
+}
+
+.deadCodeElim3
+_rx q[0], 90
+
+.deadCode2levels
+if (3<2) {
+    if (3<2) {
+        set i = 1
+    } else if (2>3) {
+        set i = 3
+    } else {
+        set i = 2
+        set j = 10
+    }
+} else if (2>3) {
+    if (3<2) {
+        set i = 1
+    } else if (2>3) {
+        set i = 3
+    } else {
+        set i = 2
+        set j = 10
+    }
+} else {
+    if (3<2) {
+        set i = 1
+    } else if (2>3) {
+        set i = 3
+    } else {
+        set i = 2
+        set j = 10
+    }
+}
+
+.deadCodeElimWithinLoop
+while(true) {
+    if (3<2) {
+        set i = 1
+    } else if (3<4) {
+        set i = 2
+        set j = 10
+    } else {
+        set i = 3
+    }
+    rx90 q[0]
+}
+
+.intRange
+#set i = 4294967295  # out of range for (signed) int
+set i = 2147483647  # max signed int
+#set i = 2147483647 + 1 # out of range
+
+.more
+#set i = 4<5
+set i = int(1+4)
+set i = 1+5
+set i = 1+2-3
+set i = int(j+4)
diff --git a/tests/cc/test_looping.cq b/tests/cc/test_looping.cq
index a7fa04f75..ef5698a68 100644
--- a/tests/cc/test_looping.cq
+++ b/tests/cc/test_looping.cq
@@ -189,10 +189,6 @@ while (b[0]) {
 #while (1 < i+3) { # Error: Input error: in pass VQ1Asm, phase main: in block 'condComplexExpr': in expression '1 < creg[0] + 3': cannot handle function call within function call 'creg[0] + 3'
 #}
 
-#.condConstExpr
-#while (4==4) {
-#}
-
 # single block, with structured control flow mid way
 # 'while' loop does not seem to be a barrier for scheduling by itself
 .multiKernel
@@ -205,17 +201,10 @@ barrier
 rx180 q[2]
 
 
-
-
-
 # FIXME: move to its own file
 .more
 # set i = true # Error: type of right-hand side (bool/bit) could not be coerced to left-hand side (int reference)
-# set i = 4<5
-# set i = int(1+4) # Error: Input error: in pass VQ1Asm, phase main: in block 'more': in expression '1 + 4': cannot handle parameter combination '1' , '4'. FIXME: no constant propagation
-# set i = 1+4
-set i = int(j+4)
-#set b[0] = true # Internal compiler error: in pass VQ1Asm, phase main: in block 'more': unsupported LHS for set instruction encountered: expected creg reference, but got something else
+# set b[0] = true # Internal compiler error: in pass VQ1Asm, phase main: in block 'more': in set_instruction '(bit)q[0] = true': in expression '(bit)q[0] = true': unsupported expression type
 # set b[0] = 1 # Error: type of right-hand side (int) could not be coerced to left-hand side (bool/bit reference)
 # set i = i<0 ? 2 : 3 # Error: dynamic values are not supported here
 # set i = 1<2 ? 2 : 3 # Error: dynamic values are not supported here