From 9db003b56366fbc2ae17fd2404db27bc67a3b0e5 Mon Sep 17 00:00:00 2001
From: Mikael Henriksson <mike.zx@hotmail.com>
Date: Fri, 17 Mar 2023 17:06:46 +0100
Subject: [PATCH] codegen: add functions 'write' and 'write_lines', reformat
 code

---
 b_asic/codegen/vhdl/__init__.py     |  39 +++-
 b_asic/codegen/vhdl/architecture.py | 297 +++++++++++++++++-----------
 b_asic/codegen/vhdl/common.py       | 132 ++++++++-----
 b_asic/codegen/vhdl/entity.py       |  33 ++--
 4 files changed, 324 insertions(+), 177 deletions(-)

diff --git a/b_asic/codegen/vhdl/__init__.py b/b_asic/codegen/vhdl/__init__.py
index e6976531..168593c1 100644
--- a/b_asic/codegen/vhdl/__init__.py
+++ b/b_asic/codegen/vhdl/__init__.py
@@ -3,6 +3,7 @@ Module for basic VHDL code generation.
 """
 
 from io import TextIOWrapper
+from typing import List, Optional, Tuple, Union
 
 # VHDL code generation tab length
 VHDL_TAB = r"    "
@@ -12,7 +13,9 @@ def write(
     f: TextIOWrapper,
     indent_level: int,
     text: str,
+    *,
     end: str = '\n',
+    start: Optional[str] = None,
 ):
     """
     Base VHDL code generation utility. `f'{VHDL_TAB*indent_level}'` is first written to the :class:`io.TextIOWrapper`
@@ -21,15 +24,45 @@ def write(
     Parameters
     ----------
     f : :class:`io.TextIOWrapper`
-        The file object to emit the VHDL code to.
+        The file object to emit VHDL code to.
     indent_level : int
-        Indentation level to use. Exactly `f'{VHDL_TAB*indent_level}` is written before the text is written.
+        Indentation level to use. Exactly ``f'{VHDL_TAB*indent_level}`` is written before the text is written.
     text : str
         The text to write to.
     end : str, default: '\n'
-        Text to write exactly after `text` is written to `f`.
+        Text to write exactly after *text* is written to *f*.
+    start : str, optional
+        Text to write before both indentation and *text*.
     """
+    if start is not None:
+        f.write(start)
     f.write(f'{VHDL_TAB*indent_level}{text}{end}')
 
 
+def write_lines(
+    f: TextIOWrapper, lines: List[Union[Tuple[int, str], Tuple[int, str, str]]]
+):
+    """
+    Multiline VHDL code generation utility. Each tuple (int, str, [int]) in the list `lines` is written to the
+    :class:`io.TextIOWrapper` object `f` using the :function:`vhdl.write` function.
+
+    Parameters
+    ----------
+    f : :class:`io.TextIOWrapper`
+        The file object to emit VHDL code to.
+    lines : list of tuple (int,str) [1], or list of tuple (int,str,str) [2]
+        [1]: The first `int` of the tuple is used as indentation level for the line and
+             the second `str` of the tuple is the content of the line.
+        [2]: Same as [1], but the third `str` of the tuple is passed to parameter `end` when calling
+             :function:`vhdl.write`.
+    """
+    for tpl in lines:
+        if len(tpl) == 2:
+            write(f, indent_level=tpl[0], text=tpl[1])
+        elif len(tpl) == 3:
+            write(f, indent_level=tpl[0], text=tpl[1], end=tpl[2])
+        else:
+            raise ValueError('All tuples in list `lines` must have length 2 or 3')
+
+
 from b_asic.codegen.vhdl import architecture, common, entity
diff --git a/b_asic/codegen/vhdl/architecture.py b/b_asic/codegen/vhdl/architecture.py
index 0ab01d1c..94948285 100644
--- a/b_asic/codegen/vhdl/architecture.py
+++ b/b_asic/codegen/vhdl/architecture.py
@@ -53,12 +53,14 @@ def write_memory_based_storage(
     schedule_time = next(iter(assignment))._schedule_time
 
     # Write architecture header
-    f.write(f'architecture {architecture_name} of {entity_name} is\n\n')
+    vhdl.write(
+        f, 0, f'architecture {architecture_name} of {entity_name} is', end='\n\n'
+    )
 
     #
     # Architecture declerative region begin
     #
-    f.write(f'{VHDL_TAB}-- HDL memory description\n')
+    vhdl.write(f, 1, '-- HDL memory description')
     vhdl.common.write_constant_decl(
         f, name='MEM_WL', type='integer', value=word_length, name_pad=12
     )
@@ -89,7 +91,7 @@ def write_memory_based_storage(
         vhdl.common.write_signal_decl(f, f'write_en_{i}', 'std_logic', name_pad=14)
 
     # Schedule time counter
-    f.write(f'\n{VHDL_TAB}-- Schedule counter\n')
+    vhdl.write(f, 1, f'-- Schedule counter', start='\n')
     vhdl.common.write_signal_decl(
         f,
         name='schedule_cnt',
@@ -99,7 +101,7 @@ def write_memory_based_storage(
 
     # Input sync signals
     if input_sync:
-        f.write(f'\n{VHDL_TAB}-- Input synchronization\n')
+        vhdl.write(f, 1, f'-- Input synchronization', start='\n')
         for i in range(read_ports):
             vhdl.common.write_signal_decl(
                 f, f'p_{i}_in_sync', 'std_logic_vector(WL-1 downto 0)', name_pad=14
@@ -108,36 +110,44 @@ def write_memory_based_storage(
     #
     # Architecture body begin
     #
-    f.write(f'\nbegin\n\n')
-    f.write(f'{VHDL_TAB}-- Schedule counter\n')
-    vhdl.common.write_synchronous_process(
+    vhdl.write(f, 0, f'begin', start='\n', end='\n\n')
+    vhdl.write(f, 1, f'-- Schedule counter')
+    vhdl.common.write_synchronous_process_prologue(
+        f=f,
+        name='schedule_cnt_proc',
+        clk='clk',
+    )
+    vhdl.write_lines(
+        f,
+        [
+            (3, f'if rst = \'1\' then'),
+            (4, f'schedule_cnt <= 0;'),
+            (3, f'else'),
+            (4, f'if en = \'1\' then'),
+            (5, f'if schedule_cnt = {schedule_time-1} then'),
+            (6, f'schedule_cnt <= 0;'),
+            (5, f'else'),
+            (6, f'schedule_cnt <= schedule_cnt + 1;'),
+            (5, f'end if;'),
+            (4, f'end if;'),
+            (3, f'end if;'),
+        ],
+    )
+    vhdl.common.write_synchronous_process_epilogue(
         f=f,
         name='schedule_cnt_proc',
         clk='clk',
-        body=(
-            f'{0*VHDL_TAB}if rst = \'1\' then\n'
-            f'{1*VHDL_TAB}schedule_cnt <= 0;\n'
-            f'{0*VHDL_TAB}else\n'
-            f'{1*VHDL_TAB}if en = \'1\' then\n'
-            f'{2*VHDL_TAB}if schedule_cnt = {schedule_time-1} then\n'
-            f'{3*VHDL_TAB}schedule_cnt <= 0;\n'
-            f'{2*VHDL_TAB}else\n'
-            f'{3*VHDL_TAB}schedule_cnt <= schedule_cnt + 1;\n'
-            f'{2*VHDL_TAB}end if;\n'
-            f'{1*VHDL_TAB}end if;\n'
-            f'{0*VHDL_TAB}end if;\n'
-        ),
     )
 
     if input_sync:
-        f.write(f'\n{VHDL_TAB}-- Input synchronization\n')
+        vhdl.write(f, 1, f'-- Input synchronization', start='\n')
         vhdl.common.write_synchronous_process_prologue(
             f=f,
             name='input_sync_proc',
             clk='clk',
         )
         for i in range(read_ports):
-            f.write(f'{3*VHDL_TAB}p_{i}_in_sync <= p_{i}_in;\n')
+            vhdl.write(f, 3, f'p_{i}_in_sync <= p_{i}_in;')
         vhdl.common.write_synchronous_process_epilogue(
             f=f,
             name='input_sync_proc',
@@ -145,7 +155,7 @@ def write_memory_based_storage(
         )
 
     # Infer memory
-    f.write(f'\n{VHDL_TAB}-- Memory\n')
+    vhdl.write(f, 1, f'-- Memory', start='\n')
     vhdl.common.write_asynchronous_read_memory(
         f=f,
         clk='clk',
@@ -161,7 +171,7 @@ def write_memory_based_storage(
     )
 
     # Write address generation
-    f.write(f'\n{VHDL_TAB}-- Memory write address generation\n')
+    vhdl.write(f, 1, f'-- Memory write address generation', start='\n')
     if input_sync:
         vhdl.common.write_synchronous_process_prologue(
             f, clk="clk", name="mem_write_address_proc"
@@ -170,19 +180,29 @@ def write_memory_based_storage(
         vhdl.common.write_process_prologue(
             f, sensitivity_list="schedule_cnt", name="mem_write_address_proc"
         )
-    f.write(f'{3*VHDL_TAB}case schedule_cnt is\n')
+    vhdl.write(f, 3, f'case schedule_cnt is')
     for i, collection in enumerate(assignment):
         for mv in collection:
             mv = cast(MemoryVariable, mv)
             if mv.execution_time:
-                f.write(f'{4*VHDL_TAB}-- {mv!r}\n')
-                f.write(f'{4*VHDL_TAB}when {(mv.start_time) % schedule_time} =>\n')
-                f.write(f'{5*VHDL_TAB}write_adr_0 <= {i};\n')
-                f.write(f'{5*VHDL_TAB}write_en_0 <= \'1\';\n')
-    f.write(f'{4*VHDL_TAB}when others =>\n')
-    f.write(f'{5*VHDL_TAB}write_adr_0 <= 0;\n')
-    f.write(f'{5*VHDL_TAB}write_en_0 <= \'0\';\n')
-    f.write(f'{3*VHDL_TAB}end case;\n')
+                vhdl.write_lines(
+                    f,
+                    [
+                        (4, f'-- {mv!r}'),
+                        (4, f'when {(mv.start_time) % schedule_time} =>'),
+                        (5, f'write_adr_0 <= {i};'),
+                        (5, f'write_en_0 <= \'1\';'),
+                    ],
+                )
+    vhdl.write_lines(
+        f,
+        [
+            (4, 'when others =>'),
+            (5, 'write_adr_0 <= 0;'),
+            (5, 'write_en_0 <= \'0\';'),
+            (3, 'end case;'),
+        ],
+    )
     if input_sync:
         vhdl.common.write_synchronous_process_epilogue(
             f, clk="clk", name="mem_write_address_proc"
@@ -193,35 +213,47 @@ def write_memory_based_storage(
         )
 
     # Read address generation
-    f.write(f'\n{VHDL_TAB}-- Memory read address generation\n')
+    vhdl.write(f, 1, f'-- Memory read address generation', start='\n')
     vhdl.common.write_synchronous_process_prologue(
         f, clk="clk", name="mem_read_address_proc"
     )
-    f.write(f'{3*VHDL_TAB}case schedule_cnt is\n')
+    vhdl.write(f, 3, f'case schedule_cnt is')
     for i, collection in enumerate(assignment):
         for mv in collection:
             mv = cast(PlainMemoryVariable, mv)
-            f.write(f'{4*VHDL_TAB}-- {mv!r}\n')
+            vhdl.write(f, 4, f'-- {mv!r}')
             for read_time in mv.reads.values():
-                f.write(
-                    f'{4*VHDL_TAB}when'
-                    f' {(mv.start_time+read_time-int(not(input_sync))) % schedule_time} =>\n'
+                vhdl.write(
+                    f,
+                    4,
+                    'when'
+                    f' {(mv.start_time+read_time-int(not(input_sync))) % schedule_time} =>',
+                )
+                vhdl.write_lines(
+                    f,
+                    [
+                        (5, f'read_adr_0 <= {i};'),
+                        (5, f'read_en_0 <= \'1\';'),
+                    ],
                 )
-                f.write(f'{5*VHDL_TAB}read_adr_0 <= {i};\n')
-                f.write(f'{5*VHDL_TAB}read_en_0 <= \'1\';\n')
-    f.write(f'{4*VHDL_TAB}when others =>\n')
-    f.write(f'{5*VHDL_TAB}read_adr_0 <= 0;\n')
-    f.write(f'{5*VHDL_TAB}read_en_0 <= \'0\';\n')
-    f.write(f'{3*VHDL_TAB}end case;\n')
+    vhdl.write_lines(
+        f,
+        [
+            (4, f'when others =>'),
+            (5, f'read_adr_0 <= 0;'),
+            (5, f'read_en_0 <= \'0\';'),
+            (3, f'end case;'),
+        ],
+    )
     vhdl.common.write_synchronous_process_epilogue(
         f, clk="clk", name="mem_read_address_proc"
     )
 
-    f.write(f'\n{1*VHDL_TAB}-- Input and output assignment\n')
+    vhdl.write(f, 1, f'-- Input and output assignmentn', start='\n')
     if input_sync:
-        f.write(f'{1*VHDL_TAB}write_port_0 <= p_0_in_sync;\n')
+        vhdl.write(f, 1, f'write_port_0 <= p_0_in_sync;')
     else:
-        f.write(f'{1*VHDL_TAB}write_port_0 <= p_0_in;\n')
+        vhdl.write(f, 1, f'write_port_0 <= p_0_in;')
     p_zero_exec = filter(
         lambda p: p.execution_time == 0, (p for pc in assignment for p in pc)
     )
@@ -230,26 +262,27 @@ def write_memory_based_storage(
         clk='clk',
         name='output_reg_proc',
     )
-    f.write(f'{3*VHDL_TAB}case schedule_cnt is\n')
+    vhdl.write(f, 3, f'case schedule_cnt is')
     for p in p_zero_exec:
         if input_sync:
-            f.write(
-                f'{4*VHDL_TAB}when {(p.start_time+1)%schedule_time} => p_0_out <='
-                ' p_0_in_sync;\n'
-            )
+            write_time = (p.start_time + 1) % schedule_time
+            vhdl.write(f, 4, f'when {write_time} => p_0_out <= p_0_in_sync;')
         else:
-            f.write(
-                f'{4*VHDL_TAB}when {(p.start_time)%schedule_time} => p_0_out <='
-                ' p_0_in;\n'
-            )
-    f.write(f'{4*VHDL_TAB}when others => p_0_out <= read_port_0;\n')
-    f.write(f'{3*VHDL_TAB}end case;\n')
+            write_time = (p.start_time) % schedule_time
+            vhdl.write(f, 4, f'when {write_time} => p_0_out <= p_0_in;')
+    vhdl.write_lines(
+        f,
+        [
+            (4, f'when others => p_0_out <= read_port_0;'),
+            (3, f'end case;'),
+        ],
+    )
     vhdl.common.write_synchronous_process_epilogue(
         f,
         clk='clk',
         name='output_reg_proc',
     )
-    f.write(f'\nend architecture {architecture_name};')
+    vhdl.write(f, 0, f'end architecture {architecture_name};', start='\n')
 
 
 def write_register_based_storage(
@@ -260,6 +293,8 @@ def write_register_based_storage(
     read_ports: int,
     write_ports: int,
     total_ports: int,
+    sync_rst: bool = False,
+    async_rst: bool = False,
 ):
     architecture_name = "rtl"
     schedule_time = len(forward_backward_table)
@@ -290,10 +325,12 @@ def write_register_based_storage(
     # Architecture declerative region begin
     #
     # Write architecture header
-    f.write(f'architecture {architecture_name} of {entity_name} is\n\n')
+    vhdl.write(
+        f, 0, f'architecture {architecture_name} of {entity_name} is', end='\n\n'
+    )
 
     # Schedule time counter
-    f.write(f'{VHDL_TAB}-- Schedule counter\n')
+    vhdl.write(f, 1, f'-- Schedule counter')
     vhdl.common.write_signal_decl(
         f,
         name='schedule_cnt',
@@ -303,7 +340,7 @@ def write_register_based_storage(
     )
 
     # Shift register
-    f.write(f'\n{VHDL_TAB}-- Shift register\n')
+    vhdl.write(f, 1, f'-- Shift register', start='\n')
     vhdl.common.write_type_decl(
         f,
         name='shift_reg_type',
@@ -317,7 +354,7 @@ def write_register_based_storage(
     )
 
     # Back edge mux decoder
-    f.write(f'\n{VHDL_TAB}-- Back-edge mux select signal\n')
+    vhdl.write(f, 1, f'-- Back-edge mux select signal', start='\n')
     vhdl.common.write_signal_decl(
         f,
         name='back_edge_mux_sel',
@@ -326,7 +363,7 @@ def write_register_based_storage(
     )
 
     # Output mux selector
-    f.write(f'\n{VHDL_TAB}-- Output mux select signal\n')
+    vhdl.write(f, 1, f'-- Output mux select signal', start='\n')
     vhdl.common.write_signal_decl(
         f,
         name='out_mux_sel',
@@ -337,26 +374,33 @@ def write_register_based_storage(
     #
     # Architecture body begin
     #
-    f.write(f'begin\n\n')
-
-    f.write(f'{VHDL_TAB}-- Schedule counter\n')
-    vhdl.common.write_synchronous_process(
+    vhdl.write(f, 0, f'begin', start='\n', end='\n\n')
+    vhdl.write(f, 1, f'-- Schedule counter')
+    vhdl.common.write_synchronous_process_prologue(
+        f=f,
+        name='schedule_cnt_proc',
+        clk='clk',
+    )
+    vhdl.write_lines(
+        f,
+        [
+            (4, f'if en = \'1\' then'),
+            (5, f'if schedule_cnt = {schedule_time}-1 then'),
+            (6, f'schedule_cnt <= 0;'),
+            (5, f'else'),
+            (6, f'schedule_cnt <= schedule_cnt + 1;'),
+            (5, f'end if;'),
+            (4, f'end if;'),
+        ],
+    )
+    vhdl.common.write_synchronous_process_epilogue(
         f=f,
         name='schedule_cnt_proc',
         clk='clk',
-        body=(
-            f'{0*VHDL_TAB}if en = \'1\' then\n'
-            f'{1*VHDL_TAB}if schedule_cnt = {schedule_time}-1 then\n'
-            f'{2*VHDL_TAB}schedule_cnt <= 0;\n'
-            f'{1*VHDL_TAB}else\n'
-            f'{2*VHDL_TAB}schedule_cnt <= schedule_cnt + 1;\n'
-            f'{1*VHDL_TAB}end if;\n'
-            f'{0*VHDL_TAB}end if;\n'
-        ),
     )
 
     # Shift register back-edge decoding
-    f.write(f'\n{VHDL_TAB}-- Shift register back-edge decoding\n')
+    vhdl.write(f, 1, f'-- Shift register back-edge decoding', start='\n')
     vhdl.common.write_synchronous_process_prologue(
         f,
         clk='clk',
@@ -368,12 +412,22 @@ def write_register_based_storage(
             assert len(entry.back_edge_to) == 1
             for src, dst in entry.back_edge_to.items():
                 mux_idx = back_edge_table[(src, dst)]
-                vhdl.write(f, 4, f'when {(time-1)%schedule_time} =>')
-                vhdl.write(f, 5, f'-- ({src} -> {dst})')
-                vhdl.write(f, 5, f'back_edge_mux_sel <= {mux_idx};')
-    vhdl.write(f, 4, f'when others =>')
-    vhdl.write(f, 5, f'back_edge_mux_sel <= 0;')
-    vhdl.write(f, 3, f'end case;')
+                vhdl.write_lines(
+                    f,
+                    [
+                        (4, f'when {(time-1)%schedule_time} =>'),
+                        (5, f'-- ({src} -> {dst})'),
+                        (5, f'back_edge_mux_sel <= {mux_idx};'),
+                    ],
+                )
+    vhdl.write_lines(
+        f,
+        [
+            (4, f'when others =>'),
+            (5, f'back_edge_mux_sel <= 0;'),
+            (3, f'end case;'),
+        ],
+    )
     vhdl.common.write_synchronous_process_epilogue(
         f,
         clk='clk',
@@ -381,28 +435,46 @@ def write_register_based_storage(
     )
 
     # Shift register multiplexer logic
-    f.write(f'\n{VHDL_TAB}-- Multiplexers for shift register\n')
+    vhdl.write(f, 1, f'-- Multiplexers for shift register', start='\n')
     vhdl.common.write_synchronous_process_prologue(
         f,
         clk='clk',
         name='shift_reg_proc',
     )
-    f.write(f'{3*VHDL_TAB}-- Default case\n')
-    f.write(f'{3*VHDL_TAB}shift_reg(0) <= p_0_in;\n')
+    if sync_rst:
+        vhdl.write(f, 3, f'if rst = \'1\' then')
+        for reg_idx in range(reg_cnt):
+            vhdl.write(f, 4, f'shift_reg({reg_idx}) <= (others => \'0\');')
+        vhdl.write(f, 3, f'else')
+
+    vhdl.write_lines(
+        f,
+        [
+            (3, f'-- Default case'),
+            (3, f'shift_reg(0) <= p_0_in;'),
+        ],
+    )
     for reg_idx in range(1, reg_cnt):
-        f.write(f'{3*VHDL_TAB}shift_reg({reg_idx}) <= shift_reg({reg_idx-1});\n')
+        vhdl.write(f, 3, f'shift_reg({reg_idx}) <= shift_reg({reg_idx-1});')
     vhdl.write(f, 3, f'case back_edge_mux_sel is')
     for edge, mux_sel in back_edge_table.items():
-        vhdl.write(f, 4, f'when {mux_sel} =>')
-        vhdl.write(f, 5, f'shift_reg({edge[1]}) <= shift_reg({edge[0]});')
-    # f.write(f'{3*VHDL_TAB}case schedule_cnt is\n')
-    # for i, entry in enumerate(forward_backward_table):
-    #    if entry.back_edge_from:
-    #        f.write(f'{4*VHDL_TAB} when {schedule_time-1 if (i-1)<0 else (i-1)} =>\n')
-    #        for dst, src in entry.back_edge_from.items():
-    #            f.write(f'{5*VHDL_TAB} shift_reg({dst}) <= shift_reg({src});\n')
-    f.write(f'{4*VHDL_TAB}when others => null;\n')
-    f.write(f'{3*VHDL_TAB}end case;\n')
+        vhdl.write_lines(
+            f,
+            [
+                (4, f'when {mux_sel} =>'),
+                (5, f'shift_reg({edge[1]}) <= shift_reg({edge[0]});'),
+            ],
+        )
+    vhdl.write_lines(
+        f,
+        [
+            (4, f'when others => null;'),
+            (3, f'end case;'),
+        ],
+    )
+
+    if sync_rst:
+        vhdl.write(f, 3, f'end if;')
 
     vhdl.common.write_synchronous_process_epilogue(
         f,
@@ -411,41 +483,40 @@ def write_register_based_storage(
     )
 
     # Output multiplexer decoding logic
-    f.write(f'\n{VHDL_TAB}-- Output muliplexer decoding logic\n')
+    vhdl.write(f, 1, f'-- Output muliplexer decoding logic', start='\n')
     vhdl.common.write_synchronous_process_prologue(
         f, clk='clk', name='out_mux_decode_proc'
     )
-    f.write(f'{3*VHDL_TAB}case schedule_cnt is\n')
+    vhdl.write(f, 3, f'case schedule_cnt is')
     for i, entry in enumerate(forward_backward_table):
         if entry.outputs_from is not None:
-            f.write(f'{4*VHDL_TAB}when {(i-1)%schedule_time} =>\n')
-            f.write(
-                f'{5*VHDL_TAB}out_mux_sel <= {output_mux_table[entry.outputs_from]};\n'
-            )
-    f.write(f'{3*VHDL_TAB}end case;\n')
+            sel = output_mux_table[entry.outputs_from]
+            vhdl.write(f, 4, f'when {(i-1)%schedule_time} =>')
+            vhdl.write(f, 5, f'out_mux_sel <= {sel};')
+    vhdl.write(f, 3, f'end case;')
     vhdl.common.write_synchronous_process_epilogue(
         f, clk='clk', name='out_mux_decode_proc'
     )
 
     # Output multiplexer logic
-    f.write(f'\n{VHDL_TAB}-- Output muliplexer\n')
+    vhdl.write(f, 1, f'-- Output muliplexer', start='\n')
     vhdl.common.write_synchronous_process_prologue(
         f,
         clk='clk',
         name='out_mux_proc',
     )
-    f.write(f'{3*VHDL_TAB}case out_mux_sel is\n')
+    vhdl.write(f, 3, f'case out_mux_sel is')
     for reg_i, mux_i in output_mux_table.items():
-        f.write(f'{4*VHDL_TAB}when {mux_i} =>\n')
+        vhdl.write(f, 4, f'when {mux_i} =>')
         if reg_i < 0:
-            f.write(f'{5*VHDL_TAB}p_0_out <= p_{-1-reg_i}_in;\n')
+            vhdl.write(f, 5, f'p_0_out <= p_{-1-reg_i}_in;')
         else:
-            f.write(f'{5*VHDL_TAB}p_0_out <= shift_reg({reg_i});\n')
-    f.write(f'{3*VHDL_TAB}end case;\n')
+            vhdl.write(f, 5, f'p_0_out <= shift_reg({reg_i});')
+    vhdl.write(f, 3, f'end case;')
     vhdl.common.write_synchronous_process_epilogue(
         f,
         clk='clk',
         name='out_mux_proc',
     )
 
-    f.write(f'end architecture {architecture_name};')
+    vhdl.write(f, 0, f'end architecture {architecture_name};', start='\n')
diff --git a/b_asic/codegen/vhdl/common.py b/b_asic/codegen/vhdl/common.py
index 6dbcb397..7925b6f3 100644
--- a/b_asic/codegen/vhdl/common.py
+++ b/b_asic/codegen/vhdl/common.py
@@ -8,7 +8,6 @@ from subprocess import PIPE, Popen
 from typing import Any, Optional, Set, Tuple
 
 from b_asic.codegen import vhdl
-from b_asic.codegen.vhdl import VHDL_TAB
 
 
 def write_b_asic_vhdl_preamble(f: TextIOWrapper):
@@ -27,13 +26,23 @@ def write_b_asic_vhdl_preamble(f: TextIOWrapper):
         git_commit_id = process.communicate()[0].decode('utf-8').strip()
     except:
         pass
-    vhdl.write(f, 0, f'--')
-    vhdl.write(f, 0, f'-- This code was automatically generated by the B-ASIC toolbox.')
-    vhdl.write(f, 0, f'-- Code generation timestamp: ({datetime.now()})')
+    vhdl.write_lines(
+        f,
+        [
+            (0, f'--'),
+            (0, f'-- This code was automatically generated by the B-ASIC toolbox.'),
+            (0, f'-- Code generation timestamp: ({datetime.now()})'),
+        ],
+    )
     if git_commit_id:
         vhdl.write(f, 0, f'-- B-ASIC short commit hash: {git_commit_id}')
-    vhdl.write(f, 0, f'-- URL: https://gitlab.liu.se/da/B-ASIC')
-    vhdl.write(f, 0, f'--', end='\n\n')
+    vhdl.write_lines(
+        f,
+        [
+            (0, f'-- URL: https://gitlab.liu.se/da/B-ASIC'),
+            (0, f'--', '\n\n'),
+        ],
+    )
 
 
 def write_ieee_header(
@@ -95,20 +104,26 @@ def write_signal_decl(
         If set, exactly one of: "M4K", "M9K", "M10K", "M20K", "M144K", "MLAB" or "logic".
     """
     # Spacing of VHDL signals declaration always with a single tab
-    name_pad = 0 if name_pad is None else name_pad
+    name_pad = name_pad or 0
     vhdl.write(f, 1, f'signal {name:<{name_pad}} : {type}', end='')
     if default_value is not None:
         vhdl.write(f, 0, f' := {default_value}', end='')
     vhdl.write(f, 0, ';')
-    if vivado_ram_style:
-        vhdl.write(f, 1, f'attribute ram_style : string;')
-        vhdl.write(
-            f, 1, f'attribute ram_style of {name} : signal is "{vivado_ram_style}";'
+    if vivado_ram_style is not None:
+        vhdl.write_lines(
+            f,
+            [
+                (1, f'attribute ram_style : string;'),
+                (1, f'attribute ram_style of {name} : signal is "{vivado_ram_style}";'),
+            ],
         )
-    if quartus_ram_style:
-        vhdl.write(f, 1, f'attribute ramstyle : string;')
-        vhdl.write(
-            f, 1, f'attribute ramstyle of {name} : signal is "{quartus_ram_style}";'
+    if quartus_ram_style is not None:
+        vhdl.write_lines(
+            f,
+            [
+                (1, f'attribute ramstyle : string;'),
+                (1, f'attribute ramstyle of {name} : signal is "{quartus_ram_style}";'),
+            ],
         )
 
 
@@ -163,12 +178,12 @@ def write_type_decl(
 def write_process_prologue(
     f: TextIOWrapper,
     sensitivity_list: str,
-    indent: str = VHDL_TAB,
+    indent: int = 1,
     name: Optional[str] = None,
 ):
     """
     Write only the prologue of a regular VHDL process with a user provided sensitivity list.
-    This method should almost always guarantely be followed by a write_asynchronous_process_epilogue.
+    This method should almost always guarantely be followed by a write_process_epilogue.
 
     Parameters
     ----------
@@ -176,22 +191,22 @@ def write_process_prologue(
         The TextIOWrapper object to write the type declaration to.
     sensitivity_list : str
         Content of the process sensitivity list.
-    indent : str, default: 1*VHDL_TAB
-        Indentation used in the process. This string is applied to the first written line of all output.
+    indent : int, default: 1
+        Indentation level to use for this process.
     name : Optional[str]
         An optional name for the process.
     """
     if name is not None:
-        f.write(f'{indent}{name}: process({sensitivity_list})\n')
+        vhdl.write(f, indent, f'{name}: process({sensitivity_list})')
     else:
-        f.write(f'{indent}process({sensitivity_list})\n')
-    f.write(f'{indent}begin\n')
+        vhdl.write(f, indent, f'process({sensitivity_list})')
+    vhdl.write(f, indent, f'begin')
 
 
 def write_process_epilogue(
     f: TextIOWrapper,
     sensitivity_list: Optional[str] = None,
-    indent: str = VHDL_TAB,
+    indent: int = 1,
     name: Optional[str] = None,
 ):
     """
@@ -201,22 +216,24 @@ def write_process_epilogue(
         The TextIOWrapper object to write the type declaration to.
     sensitivity_list : str
         Content of the process sensitivity list. Not needed when writing the epligoue.
-    indent : str, default: 1*VHDL_TAB
-        Indentation used in the process. This string is applied to the first written line of all output.
+    indent : int, default: 1
+        Indentation level to use for this process.
+    indent : int, default: 1
+        Indentation level to use for this process.
     name : Optional[str]
         An optional name of the ending process.
     """
     _ = sensitivity_list
-    f.write(f'{indent}end process')
+    vhdl.write(f, indent, f'end process', end="")
     if name is not None:
-        f.write(' ' + name)
-    f.write(';\n')
+        vhdl.write(f, 0, ' ' + name, end="")
+    vhdl.write(f, 0, ';')
 
 
 def write_synchronous_process_prologue(
     f: TextIOWrapper,
     clk: str,
-    indent: str = VHDL_TAB,
+    indent: int = 1,
     name: Optional[str] = None,
 ):
     """
@@ -230,19 +247,19 @@ def write_synchronous_process_prologue(
         The TextIOWrapper to write the VHDL code onto.
     clk : str
         Name of the clock.
-    indent : str, default: VHDL_TAB
-        Indentation used in the process. This string is applied to the first written line of all output.
+    indent : int, default: 1
+        Indentation level to use for this process.
     name : Optional[str]
         An optional name for the process.
     """
     write_process_prologue(f, sensitivity_list=clk, indent=indent, name=name)
-    f.write(f'{indent}{VHDL_TAB}if rising_edge(clk) then\n')
+    vhdl.write(f, indent + 1, f'if rising_edge(clk) then')
 
 
 def write_synchronous_process_epilogue(
     f: TextIOWrapper,
     clk: Optional[str],
-    indent: str = VHDL_TAB,
+    indent: int = 1,
     name: Optional[str] = None,
 ):
     """
@@ -256,13 +273,13 @@ def write_synchronous_process_epilogue(
         The TextIOWrapper to write the VHDL code onto.
     clk : str
         Name of the clock.
-    indent : str, default: VHDL_TAB
-        Indent this process block with `indent` columns
+    indent : int, default: 1
+        Indentation level to use for this process.
     name : Optional[str]
         An optional name for the process
     """
     _ = clk
-    f.write(f'{indent}{VHDL_TAB}end if;\n')
+    vhdl.write(f, indent + 1, f'end if;')
     write_process_epilogue(f, sensitivity_list=clk, indent=indent, name=name)
 
 
@@ -270,7 +287,7 @@ def write_synchronous_process(
     f: TextIOWrapper,
     clk: str,
     body: str,
-    indent: str = VHDL_TAB,
+    indent: int = 1,
     name: Optional[str] = None,
 ):
     """
@@ -285,15 +302,15 @@ def write_synchronous_process(
         Name of the clock.
     body : str
         Body of the `if rising_edge(clk) then` block.
-    indent : int, default: VHDL_TAB
-        Indent this process block with `indent` columns
+    indent : int, default: 1
+        Indentation level to use for this process.
     name : Optional[str]
         An optional name for the process
     """
     write_synchronous_process_prologue(f, clk, indent, name)
     for line in body.split('\n'):
         if len(line):
-            f.write(f'{indent}{2*VHDL_TAB}{line}\n')
+            vhdl.write(f, indent + 2, f'{line}')
     write_synchronous_process_epilogue(f, clk, indent, name)
 
 
@@ -324,13 +341,23 @@ def write_synchronous_memory(
     assert len(write_ports) >= 1
     write_synchronous_process_prologue(f, clk=clk, name=name)
     for read_name, address, re in read_ports:
-        f.write(f'{3*VHDL_TAB}if {re} = \'1\' then\n')
-        f.write(f'{4*VHDL_TAB}{read_name} <= memory({address});\n')
-        f.write(f'{3*VHDL_TAB}end if;\n')
+        vhdl.write_lines(
+            f,
+            [
+                (3, f'if {re} = \'1\' then'),
+                (4, f'{read_name} <= memory({address});'),
+                (3, f'end if;'),
+            ],
+        )
     for write_name, address, we in write_ports:
-        f.write(f'{3*VHDL_TAB}if {we} = \'1\' then\n')
-        f.write(f'{4*VHDL_TAB}memory({address}) <= {write_name};\n')
-        f.write(f'{3*VHDL_TAB}end if;\n')
+        vhdl.write_lines(
+            f,
+            [
+                (3, f'if {we} = \'1\' then'),
+                (4, f'memory({address}) <= {write_name};'),
+                (3, f'end if;'),
+            ],
+        )
     write_synchronous_process_epilogue(f, clk=clk, name=name)
 
 
@@ -361,9 +388,14 @@ def write_asynchronous_read_memory(
     assert len(write_ports) >= 1
     write_synchronous_process_prologue(f, clk=clk, name=name)
     for write_name, address, we in write_ports:
-        f.write(f'{3*VHDL_TAB}if {we} = \'1\' then\n')
-        f.write(f'{4*VHDL_TAB}memory({address}) <= {write_name};\n')
-        f.write(f'{3*VHDL_TAB}end if;\n')
+        vhdl.write_lines(
+            f,
+            [
+                (3, f'if {we} = \'1\' then'),
+                (4, f'memory({address}) <= {write_name};'),
+                (3, f'end if;'),
+            ],
+        )
     write_synchronous_process_epilogue(f, clk=clk, name=name)
     for read_name, address, _ in read_ports:
-        f.write(f'{1*VHDL_TAB}{read_name} <= memory({address});\n')
+        vhdl.write(f, 1, f'{read_name} <= memory({address});')
diff --git a/b_asic/codegen/vhdl/entity.py b/b_asic/codegen/vhdl/entity.py
index 9da241ce..1e0cba8f 100644
--- a/b_asic/codegen/vhdl/entity.py
+++ b/b_asic/codegen/vhdl/entity.py
@@ -4,6 +4,7 @@ Module for code generation of VHDL entity declarations
 from io import TextIOWrapper
 from typing import Set
 
+from b_asic.codegen import vhdl
 from b_asic.codegen.vhdl import VHDL_TAB
 from b_asic.port import Port
 from b_asic.process import MemoryVariable, PlainMemoryVariable
@@ -28,19 +29,29 @@ def write_memory_based_storage(
     entity_name = entity_name
 
     # Write the entity header
-    f.write(f'entity {entity_name} is\n')
-    f.write(f'{VHDL_TAB}generic(\n')
-    f.write(f'{2*VHDL_TAB}-- Data word length\n')
-    f.write(f'{2*VHDL_TAB}WL : integer := {word_length}\n')
-    f.write(f'{VHDL_TAB});\n')
-    f.write(f'{VHDL_TAB}port(\n')
+    vhdl.write_lines(
+        f,
+        [
+            (0, f'entity {entity_name} is'),
+            (1, f'generic('),
+            (2, f'-- Data word length'),
+            (2, f'WL : integer := {word_length}'),
+            (1, f');'),
+            (1, f'port('),
+        ],
+    )
 
     # Write the clock and reset signal
-    f.write(f'{2*VHDL_TAB}-- Clock, synchronous reset and enable signals\n')
-    f.write(f'{2*VHDL_TAB}clk : in std_logic;\n')
-    f.write(f'{2*VHDL_TAB}rst : in std_logic;\n')
-    f.write(f'{2*VHDL_TAB}en  : in std_logic;\n')
-    f.write(f'\n')
+    vhdl.write_lines(
+        f,
+        [
+            (0, f'-- Clock, synchronous reset and enable signals'),
+            (2, f'clk : in std_logic;'),
+            (2, f'rst : in std_logic;'),
+            (2, f'en  : in std_logic;'),
+            (0, f''),
+        ],
+    )
 
     # Write the input port specification
     f.write(f'{2*VHDL_TAB}-- Memory port I/O\n')
-- 
GitLab