Support `break` and `continue`

Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
diff --git a/systemverilog-plugin/UhdmAst.cc b/systemverilog-plugin/UhdmAst.cc
index 62f7d67..46a91f8 100644
--- a/systemverilog-plugin/UhdmAst.cc
+++ b/systemverilog-plugin/UhdmAst.cc
@@ -1151,6 +1151,125 @@
     return nullptr;
 }
 
+void UhdmAst::transform_breaks_continues(AST::AstNode *loop, AST::AstNode *decl_block)
+{
+    AST::AstNode *break_wire = nullptr;
+    AST::AstNode *continue_wire = nullptr;
+    // Creates a 1-bit wire with the given name
+    const auto make_cond_var = [this](const std::string &var_name) {
+        auto cond_var =
+          make_ast_node(AST::AST_WIRE, {make_ast_node(AST::AST_RANGE, {AST::AstNode::mkconst_int(0, false), AST::AstNode::mkconst_int(0, false)}),
+                                        AST::AstNode::mkconst_int(0, false)});
+        cond_var->str = var_name;
+        cond_var->is_reg = true;
+        return cond_var;
+    };
+    // Creates a conditional like 'if (!casevar) block'
+    auto make_case = [this](AST::AstNode *block, const std::string &casevar_name) {
+        auto *case_node = make_ast_node(AST::AST_CASE);
+        auto *id = make_identifier(casevar_name);
+        case_node->children.push_back(id);
+        auto *constant = AST::AstNode::mkconst_int(0, false, 1);
+        auto *cond_node = make_ast_node(AST::AST_COND);
+        cond_node->children.push_back(constant);
+        cond_node->children.push_back(block);
+        case_node->children.push_back(cond_node);
+        return case_node;
+    };
+    // Pre-declare this function to be able to call it recursively
+    std::function<bool(AST::AstNode *)> transform_block;
+    // Transforms the given block if it has a break or continue; recurses into child blocks; return true if a break/continue was encountered
+    transform_block = [&](AST::AstNode *block) {
+        auto wrap_and_transform = [&](decltype(block->children)::iterator it) {
+            // Move the (it, end()) statements into a new block under 'if (!continue) {...}'
+            auto *new_block = make_ast_node(AST::AST_BLOCK, {it, block->children.end()});
+            block->children.erase(it, block->children.end());
+            auto *case_node = make_case(new_block, continue_wire->str);
+            block->children.push_back(case_node);
+            transform_block(new_block);
+        };
+
+        for (auto it = block->children.begin(); it != block->children.end(); it++) {
+            auto type = static_cast<int>((*it)->type);
+            switch (type) {
+            case AST::AST_BLOCK: {
+                if (transform_block(*it)) {
+                    // If there was a break/continue, we need to wrap the rest of the block in an if
+                    wrap_and_transform(it + 1);
+                    return true;
+                }
+                break;
+            }
+            case AST::AST_CASE: {
+                // Go over each block in a case
+                bool has_jump = false;
+                for (auto *node : (*it)->children) {
+                    if (node->type == AST::AST_COND)
+                        has_jump = has_jump || transform_block(node->children.back());
+                }
+                if (has_jump) {
+                    // If there was a break/continue, we need to wrap the rest of the block in an if
+                    wrap_and_transform(it + 1);
+                    return true;
+                }
+                break;
+            }
+            case AST::AST_BREAK:
+            case AST::AST_CONTINUE: {
+                std::for_each(it, block->children.end(), [](auto *node) { delete node; });
+                block->children.erase(it, block->children.end());
+                if (!continue_wire)
+                    continue_wire = make_cond_var("$continue");
+                auto *continue_id = make_identifier(continue_wire->str);
+                block->children.push_back(make_ast_node(AST::AST_ASSIGN_EQ, {continue_id, AST::AstNode::mkconst_int(1, false)}));
+                if (type == AST::AST_BREAK) {
+                    if (!break_wire)
+                        break_wire = make_cond_var("$break");
+                    auto *break_id = make_identifier(break_wire->str);
+                    block->children.push_back(make_ast_node(AST::AST_ASSIGN_EQ, {break_id, AST::AstNode::mkconst_int(1, false)}));
+                }
+                return true;
+            }
+            }
+        }
+        return false;
+    };
+
+    // Actual transformation starts here
+    transform_block(loop->children.back());
+    if (continue_wire) {
+        auto *continue_id = make_identifier(continue_wire->str);
+        // Reset $continue each iteration
+        auto *continue_assign = make_ast_node(AST::AST_ASSIGN_EQ, {continue_id, AST::AstNode::mkconst_int(0, false)});
+        decl_block->children.insert(decl_block->children.begin(), continue_wire);
+        loop->children.back()->children.insert(loop->children.back()->children.begin(), continue_assign);
+    }
+    if (break_wire) {
+        auto *break_id = make_identifier(break_wire->str);
+        // Reset $break before the loop
+        auto *break_assign = make_ast_node(AST::AST_ASSIGN_EQ, {break_id, AST::AstNode::mkconst_int(0, false)});
+        decl_block->children.insert(decl_block->children.begin(), break_assign);
+        decl_block->children.insert(decl_block->children.begin(), break_wire);
+        if (loop->type == AST::AST_REPEAT || loop->type == AST::AST_FOR) {
+            // Wrap loop body in 'if (!break) {...}'
+            // Changing the for loop condition won't work here,
+            // as then simplify fails with error "2nd expression of procedural for-loop is not constant!"
+            auto *case_node = make_case(loop->children.back(), break_wire->str);
+            auto *new_block = make_ast_node(AST::AST_BLOCK);
+            new_block->children.push_back(case_node);
+            new_block->str = loop->children.back()->str;
+            loop->children.back() = new_block;
+        } else if (loop->type == AST::AST_WHILE) {
+            // Add the break var to the loop condition
+            auto *break_id = make_identifier(break_wire->str);
+            AST::AstNode *&loop_cond = loop->children[0];
+            loop_cond = make_ast_node(AST::AST_LOGIC_AND, {make_ast_node(AST::AST_LOGIC_NOT, {break_id}), loop_cond});
+        } else {
+            log_error("break unsupported for this loop type");
+        }
+    }
+}
+
 AST::AstNode *UhdmAst::make_ast_node(AST::AstNodeType type, std::vector<AST::AstNode *> children, bool prefer_full_name)
 {
     auto node = new AST::AstNode(type);
@@ -1168,6 +1287,13 @@
     return node;
 }
 
+AST::AstNode *UhdmAst::make_identifier(const std::string &name)
+{
+    auto *node = make_ast_node(AST::AST_IDENTIFIER);
+    node->str = name;
+    return node;
+}
+
 void UhdmAst::process_packed_array_typespec()
 {
     std::vector<AST::AstNode *> packed_ranges;
@@ -2998,24 +3124,23 @@
 
 void UhdmAst::process_for()
 {
-    current_node = make_ast_node(AST::AST_FOR);
-    auto loop = current_node;
+    current_node = make_ast_node(AST::AST_BLOCK);
     auto loop_id = shared.next_loop_id();
-    current_node->str = "$loop" + std::to_string(loop_id);
+    current_node->str = "$fordecl_block" + std::to_string(loop_id);
+    auto loop = make_ast_node(AST::AST_FOR);
+    loop->str = "$loop" + std::to_string(loop_id);
+    current_node->children.push_back(loop);
     visit_one_to_many({vpiForInitStmt}, obj_h, [&](AST::AstNode *node) {
         if (node->type == AST::AST_ASSIGN_LE)
             node->type = AST::AST_ASSIGN_EQ;
         auto lhs = node->children[0];
         if (lhs->type == AST::AST_WIRE) {
-            current_node = make_ast_node(AST::AST_BLOCK);
-            current_node->str = "$fordecl_block" + std::to_string(loop_id);
             auto *wire = lhs->clone();
             wire->is_reg = true;
             current_node->children.push_back(wire);
             lhs->type = AST::AST_IDENTIFIER;
             lhs->is_signed = false;
             lhs->delete_children();
-            current_node->children.push_back(loop);
         }
         loop->children.push_back(node);
     });
@@ -3038,6 +3163,7 @@
             loop->children.push_back(node);
         }
     });
+    transform_breaks_continues(loop, current_node);
 }
 
 void UhdmAst::process_gen_scope()
@@ -3511,19 +3637,23 @@
 
 void UhdmAst::process_repeat()
 {
-    current_node = make_ast_node(AST::AST_REPEAT);
-    visit_one_to_one({vpiCondition}, obj_h, [&](AST::AstNode *node) { current_node->children.push_back(node); });
+    auto loop_id = shared.next_loop_id();
+    current_node = make_ast_node(AST::AST_BLOCK);
+    current_node->str = "$repeatdecl_block" + std::to_string(loop_id);
+    auto *loop = make_ast_node(AST::AST_REPEAT);
+    loop->str = "$loop" + std::to_string(loop_id);
+    current_node->children.push_back(loop);
+    visit_one_to_one({vpiCondition}, obj_h, [&](AST::AstNode *node) { loop->children.push_back(node); });
     visit_one_to_one({vpiStmt}, obj_h, [&](AST::AstNode *node) {
-        if (node) {
-            AST::AstNode *block = nullptr;
-            if (node->type != AST::AST_BLOCK) {
-                block = new AST::AstNode(AST::AST_BLOCK, node);
-            } else {
-                block = node;
-            }
-            current_node->children.push_back(block);
+        if (node->type != AST::AST_BLOCK) {
+            node = new AST::AstNode(AST::AST_BLOCK, node);
         }
+        if (node->str == "") {
+            node->str = loop->str; // Needed in simplify step
+        }
+        loop->children.push_back(node);
     });
+    transform_breaks_continues(loop, current_node);
 }
 
 void UhdmAst::process_var_select()
@@ -3838,21 +3968,23 @@
 
 void UhdmAst::process_while()
 {
-    current_node = make_ast_node(AST::AST_WHILE);
-    visit_one_to_one({vpiCondition}, obj_h, [&](AST::AstNode *node) { current_node->children.push_back(node); });
+    auto loop_id = shared.next_loop_id();
+    current_node = make_ast_node(AST::AST_BLOCK);
+    current_node->str = "$whiledecl_block" + std::to_string(loop_id);
+    auto *loop = make_ast_node(AST::AST_WHILE);
+    loop->str = "$loop" + std::to_string(loop_id);
+    current_node->children.push_back(loop);
+    visit_one_to_one({vpiCondition}, obj_h, [&](AST::AstNode *node) { loop->children.push_back(node); });
     visit_one_to_one({vpiStmt}, obj_h, [&](AST::AstNode *node) {
         if (node->type != AST::AST_BLOCK) {
-            auto *statements = make_ast_node(AST::AST_BLOCK);
-            statements->str = current_node->str; // Needed in simplify step
-            statements->children.push_back(node);
-            current_node->children.push_back(statements);
-        } else {
-            if (node->str == "") {
-                node->str = current_node->str;
-                current_node->children.push_back(node);
-            }
+            node = make_ast_node(AST::AST_BLOCK, {node});
         }
+        if (node->str.empty()) {
+            node->str = loop->str; // Needed in simplify step
+        }
+        loop->children.push_back(node);
     });
+    transform_breaks_continues(loop, current_node);
 }
 
 void UhdmAst::process_gate()
@@ -4045,6 +4177,14 @@
     case vpiFor:
         process_for();
         break;
+    case vpiBreak:
+        // Will be resolved later by loop processor
+        current_node = make_ast_node(static_cast<AST::AstNodeType>(AST::AST_BREAK));
+        break;
+    case vpiContinue:
+        // Will be resolved later by loop processor
+        current_node = make_ast_node(static_cast<AST::AstNodeType>(AST::AST_CONTINUE));
+        break;
     case vpiGenScopeArray:
         process_gen_scope_array();
         break;
diff --git a/systemverilog-plugin/UhdmAst.h b/systemverilog-plugin/UhdmAst.h
index af0b808..8f6a368 100644
--- a/systemverilog-plugin/UhdmAst.h
+++ b/systemverilog-plugin/UhdmAst.h
@@ -33,6 +33,9 @@
     // the given vpiHandle.
     AST::AstNode *make_ast_node(AST::AstNodeType type, std::vector<AST::AstNode *> children = {}, bool prefer_full_name = false);
 
+    // Create an identifier AstNode
+    AST::AstNode *make_identifier(const std::string &name);
+
     // Makes the passed node a cell node of the specified type
     void make_cell(vpiHandle obj_h, AST::AstNode *node, AST::AstNode *type);
 
@@ -48,6 +51,9 @@
     // Processes the value connected to the specified node
     AST::AstNode *process_value(vpiHandle obj_h);
 
+    // Transforms break and continue nodes into structures accepted by the AST frontend
+    void transform_breaks_continues(AST::AstNode *loop, AST::AstNode *decl_block);
+
     // The parent UhdmAst
     UhdmAst *parent;
 
diff --git a/systemverilog-plugin/UhdmAstUpstream.cc b/systemverilog-plugin/UhdmAstUpstream.cc
index d6d9be8..921acf1 100644
--- a/systemverilog-plugin/UhdmAstUpstream.cc
+++ b/systemverilog-plugin/UhdmAstUpstream.cc
@@ -1,7 +1,9 @@
 namespace AST
 {
 enum AstNodeTypeExtended {
-    AST_DOT = AST::AST_BIND + 1 // here we always want to point to the last element of yosys' AstNodeType
+    AST_DOT = AST::AST_BIND + 1, // here we always want to point to the last element of yosys' AstNodeType
+    AST_BREAK,
+    AST_CONTINUE
 };
 }
 
diff --git a/systemverilog-plugin/tests/Makefile b/systemverilog-plugin/tests/Makefile
index 14a1915..378788f 100644
--- a/systemverilog-plugin/tests/Makefile
+++ b/systemverilog-plugin/tests/Makefile
@@ -14,9 +14,9 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 
-TESTS = counter
+TESTS = counter break_continue
 
 include $(shell pwd)/../../Makefile_test.common
 
 counter_verify = true
-
+break_continue_verify = $(call diff_test,break_continue,out)
diff --git a/systemverilog-plugin/tests/break_continue/break_continue.golden.out b/systemverilog-plugin/tests/break_continue/break_continue.golden.out
new file mode 100644
index 0000000..74a27c2
--- /dev/null
+++ b/systemverilog-plugin/tests/break_continue/break_continue.golden.out
@@ -0,0 +1,2 @@
+top	a	-	-	po	110
+top	b	-	-	po	15
diff --git a/systemverilog-plugin/tests/break_continue/break_continue.tcl b/systemverilog-plugin/tests/break_continue/break_continue.tcl
new file mode 100644
index 0000000..166c38e
--- /dev/null
+++ b/systemverilog-plugin/tests/break_continue/break_continue.tcl
@@ -0,0 +1,13 @@
+yosys -import
+if { [info procs read_uhdm] == {} } { plugin -i systemverilog }
+yosys -import  ;# ingest plugin commands
+
+set TMP_DIR /tmp
+if { [info exists ::env(TMPDIR) ] } {
+  set TMP_DIR $::env(TMPDIR)
+}
+
+# Testing simple round-trip
+read_systemverilog -o $TMP_DIR/break-continue-test $::env(DESIGN_TOP).v
+prep
+write_table $::env(DESIGN_TOP).out
diff --git a/systemverilog-plugin/tests/break_continue/break_continue.v b/systemverilog-plugin/tests/break_continue/break_continue.v
new file mode 100644
index 0000000..d06d60b
--- /dev/null
+++ b/systemverilog-plugin/tests/break_continue/break_continue.v
@@ -0,0 +1,30 @@
+// Copyright 2020-2022 F4PGA Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// SPDX-License-Identifier: Apache-2.0
+module top(output int a, output int b);
+   initial begin
+      a = 0;
+      b = 0;
+      repeat(15) begin
+         if(a > 100) begin
+            if (b > 10)
+               break;
+            b = b + 5;
+            continue;
+         end
+         a = a + 10;
+      end
+   end
+endmodule