Merge pull request #499 from antmicro/mglb/StreamingOperator

Improve right-to-left streaming operator support.
diff --git a/systemverilog-plugin/UhdmAst.cc b/systemverilog-plugin/UhdmAst.cc
index 65bff57..94736fe 100644
--- a/systemverilog-plugin/UhdmAst.cc
+++ b/systemverilog-plugin/UhdmAst.cc
@@ -1,6 +1,8 @@
 #include <algorithm>
+#include <cstdlib>
 #include <cstring>
 #include <functional>
+#include <iostream>
 #include <limits>
 #include <regex>
 #include <string>
@@ -690,6 +692,51 @@
     wire_node->children.insert(wire_node->children.end(), ranges.begin(), ranges.end());
 }
 
+// Assert macro that prints location in C++ code and location of currently processed UHDM object.
+// Use only inside UhdmAst methods.
+#ifndef NDEBUG
+#if __GNUC__
+// gcc/clang's __builtin_trap() makes gdb stop on the line containing an assertion.
+#define uhdmast_assert(expr)                                                                                                                         \
+    if ((expr)) {                                                                                                                                    \
+    } else {                                                                                                                                         \
+        this->uhdmast_assert_log(#expr, __PRETTY_FUNCTION__, __FILE__, __LINE__);                                                                    \
+        __builtin_trap();                                                                                                                            \
+    }
+#else // #if __GNUC__
+// Just abort when using compiler other than gcc/clang.
+#define uhdmast_assert(expr)                                                                                                                         \
+    if ((expr)) {                                                                                                                                    \
+    } else {                                                                                                                                         \
+        this->uhdmast_assert_log(#expr, __func__, __FILE__, __LINE__);                                                                               \
+        std::abort();                                                                                                                                \
+    }
+#endif // #if __GNUC__
+#else  // #ifndef NDEBUG
+#define uhdmast_assert(expr)                                                                                                                         \
+    if ((expr)) {                                                                                                                                    \
+    } else {                                                                                                                                         \
+    }
+#endif // #ifndef NDEBUG
+
+void UhdmAst::uhdmast_assert_log(const char *expr_str, const char *func, const char *file, int line) const
+{
+    std::cerr << file << ':' << line << ": error: Assertion failed: " << expr_str << std::endl;
+    std::cerr << file << ':' << line << ": note: In function: " << func << std::endl;
+    if (obj_h != 0) {
+        const char *const svfile = vpi_get_str(vpiFile, obj_h);
+        int svline = vpi_get(vpiLineNo, obj_h);
+        int svcolumn = vpi_get(vpiColumnNo, obj_h);
+        std::string obj_type_name = UHDM::VpiTypeName(obj_h);
+        const char *obj_name = vpi_get_str(vpiName, obj_h);
+        std::cerr << svfile << ':' << svline << svcolumn << ": note: When processing object of type '" << obj_type_name << '\'';
+        if (obj_name && obj_name[0] != '\0') {
+            std::cerr << " named '" << obj_name << '\'';
+        }
+        std::cerr << '.' << std::endl;
+    }
+}
+
 static AST::AstNode *expand_dot(const AST::AstNode *current_struct, const AST::AstNode *search_node)
 {
     AST::AstNode *current_struct_elem = nullptr;
@@ -1620,40 +1667,81 @@
     }
 }
 
+void UhdmAst::apply_location_from_current_obj(AST::AstNode &target_node) const
+{
+    if (auto filename = vpi_get_str(vpiFile, obj_h)) {
+        target_node.filename = filename;
+    }
+    if (unsigned int first_line = vpi_get(vpiLineNo, obj_h)) {
+        target_node.location.first_line = first_line;
+    }
+    if (unsigned int last_line = vpi_get(vpiEndLineNo, obj_h)) {
+        target_node.location.last_line = last_line;
+    } else {
+        target_node.location.last_line = target_node.location.first_line;
+    }
+    if (unsigned int first_col = vpi_get(vpiColumnNo, obj_h)) {
+        target_node.location.first_column = first_col;
+    }
+    if (unsigned int last_col = vpi_get(vpiEndColumnNo, obj_h)) {
+        target_node.location.last_column = last_col;
+    } else {
+        target_node.location.last_column = target_node.location.first_column;
+    }
+}
+
+void UhdmAst::apply_name_from_current_obj(AST::AstNode &target_node, bool prefer_full_name) const
+{
+    target_node.str = get_name(obj_h, prefer_full_name);
+    auto it = node_renames.find(target_node.str);
+    if (it != node_renames.end())
+        target_node.str = it->second;
+}
+
+AstNodeBuilder UhdmAst::make_node(AST::AstNodeType type) const
+{
+    auto node = std::make_unique<AST::AstNode>(type);
+    apply_location_from_current_obj(*node);
+    return AstNodeBuilder(std::move(node));
+};
+
+AstNodeBuilder UhdmAst::make_named_node(AST::AstNodeType type, bool prefer_full_name) const
+{
+    auto node = std::make_unique<AST::AstNode>(type);
+    apply_location_from_current_obj(*node);
+    apply_name_from_current_obj(*node, prefer_full_name);
+    return AstNodeBuilder(std::move(node));
+};
+
+AstNodeBuilder UhdmAst::make_ident(std::string id) const { return make_node(::Yosys::AST::AST_IDENTIFIER).str(std::move(id)); };
+
+AstNodeBuilder UhdmAst::make_const(int32_t value, uint8_t width) const
+{
+    // Limited to width of the `value` argument.
+    log_assert(width <= 32);
+    return make_node(AST::AST_CONSTANT).value(value, true, width);
+};
+
+AstNodeBuilder UhdmAst::make_const(uint32_t value, uint8_t width) const
+{
+    // Limited to width of the `value` argument.
+    log_assert(width <= 32);
+    return make_node(AST::AST_CONSTANT).value(value, false, width);
+};
+
 AST::AstNode *UhdmAst::make_ast_node(AST::AstNodeType type, std::vector<AST::AstNode *> children, bool prefer_full_name)
 {
     auto node = new AST::AstNode(type);
-    node->str = get_name(obj_h, prefer_full_name);
-    auto it = node_renames.find(node->str);
-    if (it != node_renames.end())
-        node->str = it->second;
-    if (auto filename = vpi_get_str(vpiFile, obj_h)) {
-        node->filename = filename;
-    }
-    if (unsigned int first_line = vpi_get(vpiLineNo, obj_h)) {
-        node->location.first_line = first_line;
-    }
-    if (unsigned int last_line = vpi_get(vpiEndLineNo, obj_h)) {
-        node->location.last_line = last_line;
-    } else {
-        node->location.last_line = node->location.first_line;
-    }
-    if (unsigned int first_col = vpi_get(vpiColumnNo, obj_h)) {
-        node->location.first_column = first_col;
-    }
-    if (unsigned int last_col = vpi_get(vpiEndColumnNo, obj_h)) {
-        node->location.last_column = last_col;
-    } else {
-        node->location.last_column = node->location.first_column;
-    }
+    apply_name_from_current_obj(*node, prefer_full_name);
+    apply_location_from_current_obj(*node);
     node->children = children;
     return node;
 }
 
-AST::AstNode *UhdmAst::make_identifier(const std::string &name)
+AST::AstNode *UhdmAst::make_identifier(std::string name)
 {
     auto *node = make_ast_node(AST::AST_IDENTIFIER);
-    node->str = name;
+    node->str = std::move(name);
     return node;
 }
 
@@ -3058,16 +3146,29 @@
     current_node = make_ast_node(AST::AST_ALWAYS);
     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);
+                // Create implicit block.
+                AST::AstNode *block = make_ast_node(AST::AST_BLOCK);
+                // There are (at least) two cases where something could have been inserted into AST_ALWAYS node when `node` is not an AST_BLOCK:
+                // - stream_op inserts a block.
+                // - event_control inserts a non-block statement.
+                // Move the block inserted by a stream_op into an implicit group. Everything else stays where it is.
+                if (!current_node->children.empty() && current_node->children.back()->type == AST::AST_BLOCK) {
+                    block->children.push_back(current_node->children.back());
+                    current_node->children.pop_back();
+                }
+                block->children.push_back(node);
+                current_node->children.push_back(block);
             } else {
-                block = node;
+                // Child is an explicit block.
+                current_node->children.push_back(node);
             }
-            current_node->children.push_back(block);
         } else {
-            // create empty block
-            current_node->children.push_back(new AST::AstNode(AST::AST_BLOCK));
+            // TODO (mglb): This branch is probably unreachable? Is it possible to have empty `always`?
+            // No children, so nothing should have been inserted into the always node during visitation.
+            log_assert(current_node->children.empty());
+            // Create implicit empty block.
+            current_node->children.push_back(make_ast_node(AST::AST_BLOCK));
         }
     });
     switch (vpi_get(vpiAlwaysType, obj_h)) {
@@ -3109,15 +3210,30 @@
 void UhdmAst::process_initial()
 {
     current_node = make_ast_node(AST::AST_INITIAL);
+    // TODO (mglb): handler below is identical as in `process_always`. Extract it to avoid duplication.
     visit_one_to_one({vpiStmt}, obj_h, [&](AST::AstNode *node) {
         if (node) {
             if (node->type != AST::AST_BLOCK) {
-                auto block_node = make_ast_node(AST::AST_BLOCK);
-                block_node->children.push_back(node);
-                node = block_node;
+                // Create an implicit block.
+                AST::AstNode *block = make_ast_node(AST::AST_BLOCK);
+                // There is (at least) one case where something could have been inserted into AST_INITIAL node when `node` is not an AST_BLOCK:
+                // - stream_op inserts a block.
+                // Move the block inserted by a stream_op into an implicit group.
+                if (!current_node->children.empty() && current_node->children.back()->type == AST::AST_BLOCK) {
+                    block->children.push_back(current_node->children.back());
+                    current_node->children.pop_back();
+                }
+                block->children.push_back(node);
+                current_node->children.push_back(block);
+            } else {
+                // Child is an explicit block.
+                current_node->children.push_back(node);
             }
-            current_node->children.push_back(node);
         } else {
+            // TODO (mglb): This branch is probably unreachable? Is it possible to have empty `initial`?
+            // No children, so nothing should have been inserted into the always node during visitation.
+            log_assert(current_node->children.empty());
+            // Create implicit empty block.
             current_node->children.push_back(make_ast_node(AST::AST_BLOCK));
         }
     });
@@ -3430,120 +3546,174 @@
 
 void UhdmAst::process_stream_op()
 {
-    // Create a for loop that does what a streaming operator would do
-    auto block_node = find_ancestor({AST::AST_BLOCK, AST::AST_ALWAYS, AST::AST_INITIAL});
-    auto process_node = find_ancestor({AST::AST_ALWAYS, AST::AST_INITIAL});
-    auto module_node = find_ancestor({AST::AST_MODULE, AST::AST_FUNCTION, AST::AST_PACKAGE});
-    log_assert(module_node);
-    if (!process_node) {
-        if (module_node->type != AST::AST_FUNCTION) {
-            // Create a @* always block
-            process_node = make_ast_node(AST::AST_ALWAYS);
-            module_node->children.push_back(process_node);
-            block_node = make_ast_node(AST::AST_BLOCK);
-            process_node->children.push_back(block_node);
-        } else {
-            // Create only block
-            block_node = make_ast_node(AST::AST_BLOCK);
-            module_node->children.push_back(block_node);
-        }
-    }
-
-    auto loop_id = shared.next_loop_id();
-    auto loop_counter =
-      make_ast_node(AST::AST_WIRE, {make_ast_node(AST::AST_RANGE, {AST::AstNode::mkconst_int(31, false), AST::AstNode::mkconst_int(0, false)})});
-    loop_counter->is_reg = true;
-    loop_counter->is_signed = true;
-    loop_counter->str = "\\loop" + std::to_string(loop_id) + "::i";
-    module_node->children.insert(module_node->children.end() - 1, loop_counter);
-    auto loop_counter_ident = make_ast_node(AST::AST_IDENTIFIER);
-    loop_counter_ident->str = loop_counter->str;
-
-    auto lhs_node = find_ancestor({AST::AST_ASSIGN, AST::AST_ASSIGN_EQ, AST::AST_ASSIGN_LE})->children[0];
-    // Temp var to allow concatenation
-    AST::AstNode *temp_var = nullptr;
-    AST::AstNode *bits_call = nullptr;
-    if (lhs_node->type == AST::AST_WIRE) {
-        module_node->children.insert(module_node->children.begin(), lhs_node->clone());
-        temp_var = lhs_node->clone(); // if we already have wire as lhs, we want to create the same wire for temp_var
-        lhs_node->delete_children();
-        lhs_node->type = AST::AST_IDENTIFIER;
-        bits_call = make_ast_node(AST::AST_FCALL, {lhs_node->clone()});
-        bits_call->str = "\\$bits";
-    } else {
-        // otherwise, we need to calculate size using bits fcall
-        bits_call = make_ast_node(AST::AST_FCALL, {lhs_node->clone()});
-        bits_call->str = "\\$bits";
-        temp_var =
-          make_ast_node(AST::AST_WIRE, {make_ast_node(AST::AST_RANGE, {make_ast_node(AST::AST_SUB, {bits_call, AST::AstNode::mkconst_int(1, false)}),
-                                                                       AST::AstNode::mkconst_int(0, false)})});
-    }
-
-    temp_var->str = "\\loop" + std::to_string(loop_id) + "::temp";
-    module_node->children.insert(module_node->children.end() - 1, temp_var);
-    auto temp_var_ident = make_ast_node(AST::AST_IDENTIFIER);
-    temp_var_ident->str = temp_var->str;
-    auto temp_assign = make_ast_node(AST::AST_ASSIGN_EQ, {temp_var_ident});
-    block_node->children.push_back(temp_assign);
-
-    // Assignment in the loop's block
-    auto assign_node = make_ast_node(AST::AST_ASSIGN_EQ, {lhs_node->clone(), temp_var_ident->clone()});
-    AST::AstNode *slice_size = nullptr; // First argument in streaming op
-    visit_one_to_many({vpiOperand}, obj_h, [&](AST::AstNode *node) {
-        if (!slice_size && node->type == AST::AST_CONSTANT) {
-            slice_size = node;
-        } else {
-            temp_assign->children.push_back(node);
-        }
+    // Closest ancestor where new statements can be inserted.
+    AST::AstNode *stmt_list_node = find_ancestor({
+      AST::AST_MODULE,
+      AST::AST_PACKAGE,
+      AST::AST_BLOCK,
+      AST::AST_INITIAL,
+      AST::AST_ALWAYS,
+      AST::AST_FUNCTION,
     });
-    if (!slice_size) {
-        slice_size = AST::AstNode::mkconst_int(1, true);
+    uhdmast_assert(stmt_list_node != nullptr);
+
+    // Detect whether we're in a procedural context. If yes, `for` loop will be generated, and `generate for` otherwise.
+    const AST::AstNode *const proc_ctx = find_ancestor({AST::AST_ALWAYS, AST::AST_INITIAL, AST::AST_FUNCTION});
+    const bool is_proc_ctx = (proc_ctx != nullptr);
+
+    // Get a prefix for internal identifiers.
+    const auto stream_op_id = shared.next_loop_id();
+    const auto make_id_str = [stream_op_id](const char *suffix) {
+        return std::string("$systemverilog_plugin$stream_op_") + std::to_string(stream_op_id) + "_" + suffix;
+    };
+
+    if (is_proc_ctx) {
+        // Put logic inside a sub-block to avoid issues with declarations not being at the beginning of a block.
+        AST::AstNode *block = make_node(Yosys::AST::AST_BLOCK).str(make_id_str("impl"));
+        stmt_list_node->children.push_back(block);
+        stmt_list_node = block;
     }
 
-    // Initialization of the loop counter to 0
-    auto init_stmt = make_ast_node(AST::AST_ASSIGN_EQ, {loop_counter_ident, AST::AstNode::mkconst_int(0, true)});
+    // TODO (mglb): Only concat expression's size factors are supported as a slice size. Add support for other slice sizes as well.
+    AST::AstNode *slice_size_arg = nullptr;
+    AST::AstNode *stream_concat_arg = nullptr;
+    {
+        std::vector<AST::AstNode *> operands;
+        // Expected operands: [slice_size] stream_concatenation
+        visit_one_to_many({vpiOperand}, obj_h, [&](AST::AstNode *node) {
+            uhdmast_assert(node != nullptr);
+            uhdmast_assert(operands.size() < 2);
+            operands.push_back(node);
+        });
+        uhdmast_assert(operands.size() > 0);
 
-    // Loop condition (loop counter < $bits(RHS))
-    auto cond_stmt =
-      make_ast_node(AST::AST_LE, {loop_counter_ident->clone(), make_ast_node(AST::AST_SUB, {bits_call->clone(), slice_size->clone()})});
+        if (operands.size() == 2) {
+            slice_size_arg = operands.at(0);
+            // SV spec says slice_size can be a constant or a type. However, Surelog converts type to its width, so we always expect a const.
+            uhdmast_assert(slice_size_arg->type == AST::AST_CONSTANT);
+        } else {
+            slice_size_arg = make_const(1u);
+        }
+        stream_concat_arg = operands.back();
+    }
 
-    // Increment loop counter
-    auto inc_stmt =
-      make_ast_node(AST::AST_ASSIGN_EQ, {loop_counter_ident->clone(), make_ast_node(AST::AST_ADD, {loop_counter_ident->clone(), slice_size})});
+    AST::AstNode *const stream_concat_width_lp = //
+      (make_node(AST::AST_LOCALPARAM).str(make_id_str("width")))({
+        (make_node(AST::AST_FCALL).str("\\$bits"))({
+          (stream_concat_arg->clone()),
+        }),
+        (make_range(31, 0, true)),
+      });
 
-    // Range on the LHS of the assignment
-    auto lhs_range = make_ast_node(AST::AST_RANGE);
-    auto lhs_selfsz = make_ast_node(
-      AST::AST_SELFSZ, {make_ast_node(AST::AST_SUB, {make_ast_node(AST::AST_SUB, {bits_call->clone(), AST::AstNode::mkconst_int(1, true)}),
-                                                     loop_counter_ident->clone()})});
-    lhs_range->children.push_back(make_ast_node(AST::AST_ADD, {lhs_selfsz, AST::AstNode::mkconst_int(0, true)}));
-    lhs_range->children.push_back(
-      make_ast_node(AST::AST_SUB, {make_ast_node(AST::AST_ADD, {lhs_selfsz->clone(), AST::AstNode::mkconst_int(1, true)}), slice_size->clone()}));
+    // TODO (mglb): src_wire and dst_wire should probably take argument signedness and logicness into account.
+    AST::AstNode *const src_wire = //
+      (make_node(AST::AST_WIRE).str(make_id_str("src")).is_reg(is_proc_ctx))({
+        (make_node(AST::AST_RANGE))({
+          (make_const(0)),
+          (make_node(AST::AST_SUB))({
+            (make_ident(stream_concat_width_lp->str)),
+            (make_const(1)),
+          }),
+        }),
+      });
 
-    // Range on the RHS of the assignment
-    auto rhs_range = make_ast_node(AST::AST_RANGE);
-    auto rhs_selfsz = make_ast_node(AST::AST_SELFSZ, {loop_counter_ident->clone()});
-    rhs_range->children.push_back(
-      make_ast_node(AST::AST_SUB, {make_ast_node(AST::AST_ADD, {rhs_selfsz, slice_size->clone()}), AST::AstNode::mkconst_int(1, true)}));
-    rhs_range->children.push_back(make_ast_node(AST::AST_ADD, {rhs_selfsz->clone(), AST::AstNode::mkconst_int(0, true)}));
+    AST::AstNode *const dst_wire = //
+      (make_node(AST::AST_WIRE).str(make_id_str("dst")).is_reg(is_proc_ctx))({
+        (make_node(AST::AST_RANGE))({
+          (make_node(AST::AST_SUB))({
+            (make_ident(stream_concat_width_lp->str)),
+            (make_const(1)),
+          }),
+          (make_const(0)),
+        }),
+      });
 
-    // Put ranges on the sides of the assignment
-    assign_node->children[0]->children.push_back(lhs_range);
-    assign_node->children[1]->children.push_back(rhs_range);
+    AST::AstNode *const assign_stream_concat_to_src_wire = //
+      (make_node(is_proc_ctx ? AST::AST_ASSIGN_EQ : AST::AST_ASSIGN))({
+        (make_ident(src_wire->str)),
+        (stream_concat_arg),
+      });
 
-    // Putting the loop together
-    auto loop_node = make_ast_node(AST::AST_FOR);
-    loop_node->str = "$loop" + std::to_string(loop_id);
-    loop_node->children.push_back(init_stmt);
-    loop_node->children.push_back(cond_stmt);
-    loop_node->children.push_back(inc_stmt);
-    loop_node->children.push_back(make_ast_node(AST::AST_BLOCK, {assign_node}));
-    loop_node->children[3]->str = "\\stream_op_block" + std::to_string(loop_id);
+    AST::AstNode *const loop_counter = //
+      (make_node(is_proc_ctx ? AST::AST_WIRE : AST::AST_GENVAR).str(make_id_str("counter")).is_reg(true))({
+        (make_range(31, 0, true)),
+      });
 
-    block_node->children.push_back(make_ast_node(AST::AST_BLOCK, {loop_node}));
+    AST::AstNode *const for_loop = //
+      (make_node(is_proc_ctx ? AST::AST_FOR : AST::AST_GENFOR))({
+        // init statement
+        (make_node(AST::AST_ASSIGN_EQ))({
+          (make_ident(loop_counter->str)),
+          (make_const(0)),
+        }),
+        // condition
+        (make_node(AST::AST_LT))({
+          (make_ident(loop_counter->str)),
+          (make_ident(stream_concat_width_lp->str)),
+        }),
+        // iteration expression
+        (make_node(AST::AST_ASSIGN_EQ))({
+          (make_ident(loop_counter->str)),
+          (make_node(Yosys::AST::AST_ADD))({
+            (make_ident(loop_counter->str)),
+            (slice_size_arg->clone()),
+          }),
+        }),
+        // loop body
+        (make_node(is_proc_ctx ? AST::AST_BLOCK : AST::AST_GENBLOCK).str(make_id_str("loop_body")))({
+          (make_node(is_proc_ctx ? AST::AST_ASSIGN_EQ : AST::AST_ASSIGN))({
+            (make_ident(dst_wire->str))({
+              (make_node(AST::AST_RANGE))({
+                (make_node(Yosys::AST::AST_SUB))({
+                  (make_node(Yosys::AST::AST_ADD))({
+                    (make_node(Yosys::AST::AST_SELFSZ))({
+                      (make_ident(loop_counter->str)),
+                    }),
+                    (slice_size_arg->clone()),
+                  }),
+                  (make_const(1)),
+                }),
+                (make_node(Yosys::AST::AST_ADD))({
+                  (make_node(Yosys::AST::AST_SELFSZ))({
+                    (make_ident(loop_counter->str)),
+                  }),
+                  (make_const(0)),
+                }),
+              }),
+            }),
+            (make_ident(src_wire->str))({
+              (make_node(AST::AST_RANGE))({
+                (make_node(Yosys::AST::AST_SUB))({
+                  (make_node(Yosys::AST::AST_ADD))({
+                    (make_node(Yosys::AST::AST_SELFSZ))({
+                      (make_ident(loop_counter->str)),
+                    }),
+                    (slice_size_arg),
+                  }),
+                  (make_const(1)),
+                }),
+                (make_node(Yosys::AST::AST_ADD))({
+                  (make_node(Yosys::AST::AST_SELFSZ))({
+                    (make_ident(loop_counter->str)),
+                  }),
+                  (make_const(0)),
+                }),
+              }),
+            }),
+          }),
+        }),
+      });
 
-    // Do not create a node
-    shared.report.mark_handled(obj_h);
+    stmt_list_node->children.insert(stmt_list_node->children.end(), {
+                                                                      stream_concat_width_lp,
+                                                                      src_wire,
+                                                                      dst_wire,
+                                                                      assign_stream_concat_to_src_wire,
+                                                                      loop_counter,
+                                                                      for_loop,
+                                                                    });
+
+    current_node = make_ident(is_proc_ctx ? (stmt_list_node->str + '.' + dst_wire->str) : dst_wire->str);
 }
 
 void UhdmAst::process_list_op()
diff --git a/systemverilog-plugin/UhdmAst.h b/systemverilog-plugin/UhdmAst.h
index e1931e9..adee39c 100644
--- a/systemverilog-plugin/UhdmAst.h
+++ b/systemverilog-plugin/UhdmAst.h
@@ -6,14 +6,20 @@
 #undef cover
 
 #include "uhdmastshared.h"
+#include <memory>
 #include <uhdm/uhdm.h>
 
 namespace systemverilog_plugin
 {
 
+class AstNodeBuilder;
+
 class UhdmAst
 {
   private:
+    // Logging method for exclusive use of `uhdmast_assert` macro.
+    void uhdmast_assert_log(const char *expr_str, const char *func, const char *file, int line) const;
+
     // Walks through one-to-many relationships from given parent
     // node through the VPI interface, visiting child nodes belonging to
     // ChildrenNodeTypes that are present in the given object.
@@ -30,13 +36,31 @@
     // Visit the default expression assigned to a variable.
     void visit_default_expr(vpiHandle obj_h);
 
+    // Reads location info (start/end line/column numbers, file name) from `obj_h` and sets them on `target_node`.
+    void apply_location_from_current_obj(::Yosys::AST::AstNode &target_node) const;
+    // Reads object name from `obj_h` and assigns it to `target_node`.
+    void apply_name_from_current_obj(::Yosys::AST::AstNode &target_node, bool prefer_full_name = false) const;
+
+    // Creates node of specified `type` with location properties read from `obj_h`.
+    AstNodeBuilder make_node(::Yosys::AST::AstNodeType type) const;
+    // Creates node of specified `type` with location properties and name read from `obj_h`.
+    AstNodeBuilder make_named_node(::Yosys::AST::AstNodeType type, bool prefer_full_name = false) const;
+    // Creates AST_IDENTIFIER node with specified `id` and location properties read from `obj_h`.
+    AstNodeBuilder make_ident(std::string id) const;
+    // Creates signed AST_CONSTANT node with specified `value` and location properties read from `obj_h`.
+    AstNodeBuilder make_const(int32_t value, uint8_t width = 32) const;
+    // Creates unsigned AST_CONSTANT node with specified `value` and location properties read from `obj_h`.
+    AstNodeBuilder make_const(uint32_t value, uint8_t width = 32) const;
+
     // Create an AstNode of the specified type with metadata extracted from
     // the given vpiHandle.
+    // OBSOLETE: use `make_node` or `make_named_node` instead.
     ::Yosys::AST::AstNode *make_ast_node(::Yosys::AST::AstNodeType type, std::vector<::Yosys::AST::AstNode *> children = {},
                                          bool prefer_full_name = false);
 
     // Create an identifier AstNode
-    ::Yosys::AST::AstNode *make_identifier(const std::string &name);
+    // OBSOLETE: use `make_ident` instead.
+    ::Yosys::AST::AstNode *make_identifier(std::string name);
 
     // Makes the passed node a cell node of the specified type
     void make_cell(vpiHandle obj_h, ::Yosys::AST::AstNode *node, ::Yosys::AST::AstNode *type);
@@ -179,6 +203,125 @@
     static const ::Yosys::IdString &low_high_bound();
 };
 
+// Utility for building AstNode trees.
+//
+// The object members that set AstNode properties return rvalue reference to *this (i.e. to the builder object), so they can be chained.
+// The children list is set using call operator (`builder_object({child0, child1, ...})`).
+// Build finalization is done through cast operator to either `AstNode*` or `std::unique_ptr<AstNode>`.
+//
+// Usage example:
+//
+// 1. Define one or more factory functions for creating base AstNode object:
+//
+//     const auto make_node = [](AST::AstNodeType type) {
+//         auto node = std::make_unique<AST::AstNode>(type);
+//         // ...initialize the node if needed...
+//         return AstNodeBuilder(std::move(node));
+//     };
+//
+// 2. Use the factories to create a tree:
+//
+//     // AST::AstNode *const variable_node = ...
+//     // AST::AstNode *const value_node = ...
+//     AST::AstNode *const assign = //
+//       (make_node(AST::AST_ASSIGN_EQ))({
+//         (make_node(AST::AST_IDENTIFIER).str(variable_node->str)),
+//         (make_node(Yosys::AST::AST_ADD))({
+//           (make_node(AST::AST_IDENTIFIER).str(value_node->str)),
+//           (make_node(AST::AST_CONSTANT).value(4)),
+//         }),
+//       });
+//
+// In the real code instead of custom factories illustrated in point 1 above, you probably should use predefined methods from `UhdmAst` class.
+// The syntax above puts the factory call and all its method calls (but not the function call operator with the children list) in `()`. This is done
+// to make `clang-format` format the code as presented. Otherwise it is heavily wrapped and a lot less readable. `()` are technically not required
+// in leafs to make them format as expected, but its nice to use them for consistency.
+class AstNodeBuilder
+{
+    using AstNode = ::Yosys::AST::AstNode;
+    using AstNodeType = ::Yosys::AST::AstNodeType;
+
+    std::unique_ptr<AstNode> node;
+
+  public:
+    explicit AstNodeBuilder(AstNodeType node_type) : node(new AstNode(node_type)) {}
+    explicit AstNodeBuilder(std::unique_ptr<AstNode> node) : node(std::move(node)) {}
+    ~AstNodeBuilder() { log_assert(node == nullptr); }
+
+    AstNodeBuilder(AstNodeBuilder &&) = default;
+
+    AstNodeBuilder() = delete;
+    AstNodeBuilder(const AstNodeBuilder &) = delete;
+    AstNodeBuilder &operator=(const AstNodeBuilder &) = delete;
+    AstNodeBuilder &operator=(AstNodeBuilder &&) = delete;
+
+    // Property setters
+
+    // Sets `AstNode::children` vector
+    AstNodeBuilder &&operator()(std::vector<AstNode *> children) { return node->children = std::move(children), std::move(*this); }
+
+    // Sets `AstNode::str` value.
+    AstNodeBuilder &&str(std::string s) { return node->str = std::move(s), std::move(*this); }
+
+    // Sets `AstNode::integer` value.
+    AstNodeBuilder &&integer(uint32_t v) { return node->integer = v, std::move(*this); }
+
+    // Sets `AstNode::is_signed` value.
+    AstNodeBuilder &&is_signed(bool v) { return node->is_signed = v, std::move(*this); }
+
+    // Sets `AstNode::is_reg` value.
+    AstNodeBuilder &&is_reg(bool v) { return node->is_reg = v, std::move(*this); }
+
+    // Sets `AstNode::range_valid`.
+    AstNodeBuilder &&range_valid(bool v) { return node->range_valid = v, std::move(*this); }
+
+    // Convenience range setters
+
+    // Sets `AstNode::range_left`, `AstNode::range_right`, `AstNode::range_valid`.
+    AstNodeBuilder &&range(bool v, int left = -1, int right = 0)
+    {
+        node->range_valid = v;
+        node->range_left = left;
+        node->range_right = right;
+        return std::move(*this);
+    }
+
+    // Sets `AstNode::range_left`, `AstNode::range_right`, `AstNode::range_valid = true`.
+    AstNodeBuilder &&range(int left, int right) { return range(true, left, right); }
+
+    // Convenience value setters, mainly for constants.
+
+    // Sets node's value.
+    // Sets: `AstNode::integer`, `AstNode::is_signed`, `AstNode::bits`.
+    AstNodeBuilder &&value(uint32_t v, bool is_signed, int width = 32)
+    {
+        log_assert(width >= 0);
+        node->integer = v;
+        node->is_signed = is_signed;
+        // `AstNode::mkconst_int` does this too.
+        for (int i = 0; i < width; i++) {
+            node->bits.push_back((v & 1) ? Yosys::RTLIL::State::S1 : Yosys::RTLIL::State::S0);
+            v = v >> 1;
+        }
+        range(width - 1, 0);
+        return std::move(*this);
+    }
+
+    // Sets node's value to signed 32 bit integer.
+    // Sets: `AstNode::integer`, `AstNode::is_signed`, `AstNode::bits`.
+    AstNodeBuilder &&value(int32_t v) { return value(v, true); }
+
+    // Sets node's value to unsigned 32 bit integer.
+    // Sets: `AstNode::integer`, `AstNode::is_signed`, `AstNode::bits`.
+    AstNodeBuilder &&value(uint32_t v) { return value(v, false); }
+
+    // Type-cast operators used for building.
+
+    operator AstNode *() { return node.release(); }
+
+    operator std::unique_ptr<AstNode>() { return std::move(node); }
+};
+
 } // namespace systemverilog_plugin
 
 #endif