From 0bd028cb20c738d2490ed0efa3b553cc09230af7 Mon Sep 17 00:00:00 2001 From: Anshuman Mohan <10830208+anshumanmohan@users.noreply.github.com> Date: Tue, 15 Aug 2023 09:42:35 -0400 Subject: [PATCH] PIFO in Calyx (#1625) --- calyx-py/calyx/builder.py | 25 ++- calyx-py/calyx/builder_util.py | 95 +++++----- calyx-py/calyx/queue_call.py | 141 ++++++++++++++ calyx-py/test/correctness/fifo.py | 150 +-------------- calyx-py/test/correctness/pifo.data | 45 +++++ calyx-py/test/correctness/pifo.expect | 31 ++++ calyx-py/test/correctness/pifo.py | 255 ++++++++++++++++++++++++++ 7 files changed, 547 insertions(+), 195 deletions(-) create mode 100644 calyx-py/calyx/queue_call.py create mode 100644 calyx-py/test/correctness/pifo.data create mode 100644 calyx-py/test/correctness/pifo.expect create mode 100644 calyx-py/test/correctness/pifo.py diff --git a/calyx-py/calyx/builder.py b/calyx-py/calyx/builder.py index acd3adb8e2..3bbf0d867e 100644 --- a/calyx-py/calyx/builder.py +++ b/calyx-py/calyx/builder.py @@ -268,13 +268,6 @@ def seq_mem_d1( name, ast.Stdlib.seq_mem_d1(bitwidth, len, idx_size), is_external, is_ref ) - def is_seq_mem_d1(self, cell: CellBuilder) -> bool: - """Check if the cell is a SeqMemD1 cell.""" - return ( - isinstance(cell._cell.comp, ast.CompInst) - and cell._cell.comp.name == "seq_mem_d1" - ) - def add(self, name: str, size: int, signed=False) -> CellBuilder: """Generate a StdAdd cell.""" self.prog.import_("primitives/binary_operators.futil") @@ -319,6 +312,10 @@ def and_(self, name: str, size: int) -> CellBuilder: """Generate a StdAnd cell.""" return self.cell(name, ast.Stdlib.op("and", size, False)) + def not_(self, name: str, size: int) -> CellBuilder: + """Generate a StdNot cell.""" + return self.cell(name, ast.Stdlib.op("not", size, False)) + def pipelined_mult(self, name: str) -> CellBuilder: """Generate a pipelined multiplier.""" self.prog.import_("primitives/pipelined.futil") @@ -620,13 +617,21 @@ def port(self, name: str) -> ExprBuilder: """Build a port access expression.""" return ExprBuilder(ast.Atom(ast.CompPort(self._cell.id, name))) - def is_mem_d1(self) -> bool: - """Check if the cell is a StdMemD1 cell.""" + def is_primitive(self, prim_name) -> bool: + """Check if the cell is an instance of the primitive {prim_name}.""" return ( isinstance(self._cell.comp, ast.CompInst) - and self._cell.comp.id == "std_mem_d1" + and self._cell.comp.id == prim_name ) + def is_std_mem_d1(self) -> bool: + """Check if the cell is a StdMemD1 cell.""" + return self.is_primitive("std_mem_d1") + + def is_seq_mem_d1(self) -> bool: + """Check if the cell is a SeqMemD1 cell.""" + return self.is_primitive("seq_mem_d1") + @classmethod def unwrap_id(cls, obj): if isinstance(obj, cls): diff --git a/calyx-py/calyx/builder_util.py b/calyx-py/calyx/builder_util.py index 14c961a063..1848ae3443 100644 --- a/calyx-py/calyx/builder_util.py +++ b/calyx-py/calyx/builder_util.py @@ -109,6 +109,20 @@ def insert_sub(comp: cb.ComponentBuilder, left, right, cellname, width): return insert_comb_group(comp, left, right, sub_cell, f"{cellname}_group") +def insert_bitwise_flip_reg(comp: cb.ComponentBuilder, reg, cellname, width): + """Inserts wiring into component {comp} to bitwise-flip the contents of {reg}. + + Returns a handle to the group that does this. + """ + not_cell = comp.not_(cellname, width) + with comp.group(f"{cellname}_group") as not_group: + not_cell.in_ = reg.out + reg.write_en = 1 + reg.in_ = not_cell.out + not_group.done = reg.done + return not_group + + def insert_incr(comp: cb.ComponentBuilder, reg, cellname, val=1): """Inserts wiring into component {comp} to increment register {reg} by {val}. 1. Within component {comp}, creates a group called {cellname}_group. @@ -145,13 +159,27 @@ def insert_decr(comp: cb.ComponentBuilder, reg, cellname, val=1): return decr_group -def mem_load(comp: cb.ComponentBuilder, mem, i, reg, group): - """Loads a value from one memory into a register. +def insert_reg_store(comp: cb.ComponentBuilder, reg, val, group): + """Stores a value in a register. + 1. Within component {comp}, creates a group called {group}. + 2. Within {group}, sets the register {reg} to {val}. + 3. Returns the group that does this. + """ + with comp.group(group) as reg_grp: + reg.in_ = val + reg.write_en = 1 + reg_grp.done = reg.done + return reg_grp + + +def mem_load_std_d1(comp: cb.ComponentBuilder, mem, i, reg, group): + """Loads a value from one memory (std_d1) into a register. 1. Within component {comp}, creates a group called {group}. 2. Within {group}, reads from memory {mem} at address {i}. 3. Writes the value into register {reg}. 4. Returns the group that does this. """ + assert mem.is_std_mem_d1() with comp.group(group) as load_grp: mem.addr0 = i reg.write_en = 1 @@ -160,13 +188,14 @@ def mem_load(comp: cb.ComponentBuilder, mem, i, reg, group): return load_grp -def mem_store(comp: cb.ComponentBuilder, mem, i, val, group): - """Stores a value from one memory into another. +def mem_store_std_d1(comp: cb.ComponentBuilder, mem, i, val, group): + """Stores a value into a (std_d1) memory. 1. Within component {comp}, creates a group called {group}. 2. Within {group}, reads from {val}. 3. Writes the value into memory {mem} at address i. 4. Returns the group that does this. """ + assert mem.is_std_mem_d1() with comp.group(group) as store_grp: mem.addr0 = i mem.write_en = 1 @@ -175,24 +204,16 @@ def mem_store(comp: cb.ComponentBuilder, mem, i, val, group): return store_grp -def insert_reg_store(comp: cb.ComponentBuilder, reg, val, group): - """Stores a value in a register. - 1. Within component {comp}, creates a group called {group}. - 2. Within {group}, sets the register {reg} to {val}. - 3. Returns the group that does this. - """ - with comp.group(group) as reg_grp: - reg.in_ = val - reg.write_en = 1 - reg_grp.done = reg.done - return reg_grp - - -def mem_read_seqd1(comp: cb.ComponentBuilder, mem, i, group): +def mem_read_seq_d1(comp: cb.ComponentBuilder, mem, i, group): """Given a seq_mem_d1, reads from memory at address i. Note that this does not write the value anywhere. + + 1. Within component {comp}, creates a group called {group}. + 2. Within {group}, reads from memory {mem} at address {i}, + thereby "latching" the value. + 3. Returns the group that does this. """ - assert mem.is_seq_mem_d1 + assert mem.is_seq_mem_d1() with comp.group(group) as read_grp: mem.addr0 = i mem.read_en = 1 @@ -200,11 +221,16 @@ def mem_read_seqd1(comp: cb.ComponentBuilder, mem, i, group): return read_grp -def mem_write_seqd1_to_reg(comp: cb.ComponentBuilder, mem, reg, group): +def mem_write_seq_d1_to_reg(comp: cb.ComponentBuilder, mem, reg, group): """Given a seq_mem_d1 that is already assumed to have a latched value, reads the latched value and writes it to a register. + + 1. Within component {comp}, creates a group called {group}. + 2. Within {group}, reads from memory {mem}. + 3. Writes the value into register {reg}. + 4. Returns the group that does this. """ - assert mem.is_seq_mem_d1 + assert mem.is_seq_mem_d1() with comp.group(group) as write_grp: reg.write_en = 1 reg.in_ = mem.read_data @@ -213,13 +239,14 @@ def mem_write_seqd1_to_reg(comp: cb.ComponentBuilder, mem, reg, group): def mem_store_seq_d1(comp: cb.ComponentBuilder, mem, i, val, group): - """Stores a value from one memory into another. + """Given a seq_mem_d1, stores a value into memory at address i. + 1. Within component {comp}, creates a group called {group}. 2. Within {group}, reads from {val}. 3. Writes the value into memory {mem} at address i. 4. Returns the group that does this. """ - assert mem.is_seq_mem_d1 + assert mem.is_seq_mem_d1() with comp.group(group) as store_grp: mem.addr0 = i mem.write_en = 1 @@ -228,26 +255,6 @@ def mem_store_seq_d1(comp: cb.ComponentBuilder, mem, i, val, group): return store_grp -def reg_swap(comp: cb.ComponentBuilder, a, b, group): - """Swaps the values of two registers. - 1. Within component {comp}, creates a group called {group}. - 2. Reads the value of {a} into a temporary register. - 3. Writes the value of {b} into {a}. - 4. Writes the value of the temporary register into {b}. - 5. Returns the group that does this. - """ - with comp.group(group) as swap_grp: - tmp = comp.reg("tmp", 1) - tmp.write_en = 1 - tmp.in_ = a.out - a.write_en = 1 - a.in_ = b.out - b.write_en = 1 - b.in_ = tmp.out - swap_grp.done = b.done - return swap_grp - - def insert_mem_load_to_mem(comp: cb.ComponentBuilder, mem, i, ans, j, group): """Loads a value from one std_mem_d1 memory into another. 1. Within component {comp}, creates a group called {group}. @@ -255,7 +262,7 @@ def insert_mem_load_to_mem(comp: cb.ComponentBuilder, mem, i, ans, j, group): 3. Writes the value into memory {ans} at address {j}. 4. Returns the group that does this. """ - assert mem.is_mem_d1() and ans.is_mem_d1() + assert mem.is_std_mem_d1() and ans.is_std_mem_d1() with comp.group(group) as load_grp: mem.addr0 = i ans.write_en = 1 diff --git a/calyx-py/calyx/queue_call.py b/calyx-py/calyx/queue_call.py new file mode 100644 index 0000000000..bf20799125 --- /dev/null +++ b/calyx-py/calyx/queue_call.py @@ -0,0 +1,141 @@ +# pylint: disable=import-error +import calyx.builder as cb +import calyx.builder_util as util + +MAX_CMDS = 15 + + +def insert_raise_err_if_i_eq_max_cmds(prog): + """Inserts a the component `raise_err_if_i_eq_MAX_CMDS` into the program. + + It has: + - one input, `i`. + - one ref register, `err`. + + If `i` equals MAX_CMDS, it raises the `err` flag. + """ + raise_err_if_i_eq_max_cmds: cb.ComponentBuilder = prog.component( + "raise_err_if_i_eq_MAX_CMDS" + ) + i = raise_err_if_i_eq_max_cmds.input("i", 32) + err = raise_err_if_i_eq_max_cmds.reg("err", 1, is_ref=True) + + i_eq_max_cmds = util.insert_eq( + raise_err_if_i_eq_max_cmds, i, MAX_CMDS, "i_eq_MAX_CMDS", 32 + ) + raise_err = util.insert_reg_store(raise_err_if_i_eq_max_cmds, err, 1, "raise_err") + + raise_err_if_i_eq_max_cmds.control += [ + cb.if_( + i_eq_max_cmds[0].out, + i_eq_max_cmds[1], + raise_err, + ) + ] + + return raise_err_if_i_eq_max_cmds + + +def insert_main(prog, queue): + """Inserts the component `main` into the program. + This will be used to `invoke` the component `queue` and feed it a list of commands. + """ + main: cb.ComponentBuilder = prog.component("main") + + # The user-facing interface of the `main` component is: + # - a list of commands (the input) + # where each command is a 32-bit unsigned integer, with the following format: + # `0`: pop + # any other value: push that value + # - a list of answers (the output). + # + # The user-facing interface of the `queue` component is: + # - one input, `cmd`. + # where each command is a 32-bit unsigned integer, with the following format: + # `0`: pop + # any other value: push that value + # - one ref register, `ans`, into which the result of a pop is written. + # - one ref register, `err`, which is raised if an error occurs. + + commands = main.seq_mem_d1("commands", 32, MAX_CMDS, 32, is_external=True) + ans_mem = main.seq_mem_d1("ans_mem", 32, 10, 32, is_external=True) + + # The two components we'll use: + queue = main.cell("myqueue", queue) + raise_err_if_i_eq_max_cmds = main.cell( + "raise_err_if_i_eq_MAX_CMDS", insert_raise_err_if_i_eq_max_cmds(prog) + ) + + # We will use the `invoke` method to call the `queue` component. + # The queue component takes two inputs by reference and one input directly. + # The two `ref` inputs: + err = main.reg("err", 1) # A flag to indicate an error + ans = main.reg("ans", 32) # A memory to hold the answer of a pop + + # We will set up a while loop that runs over the command list, relaying + # the commands to the `queue` component. + # It will run until the `err` flag is raised by the `queue` component. + + i = main.reg("i", 32) # The index of the command we're currently processing + j = main.reg("j", 32) # The index on the answer-list we'll write to + cmd = main.reg("command", 32) # The command we're currently processing + + incr_i = util.insert_incr(main, i, "incr_i") # i++ + incr_j = util.insert_incr(main, j, "incr_j") # j++ + err_eq_0 = util.insert_eq(main, err.out, 0, "err_eq_0", 1) # is `err` flag down? + cmd_eq_0 = util.insert_eq(main, cmd.out, 0, "cmd_eq_0", 32) # cmd == 0 + cmd_neq_0 = util.insert_neq( + main, cmd.out, cb.const(32, 0), "cmd_neq_0", 32 + ) # cmd != 0 + + read_cmd = util.mem_read_seq_d1(main, commands, i.out, "read_cmd_phase1") + write_cmd_to_reg = util.mem_write_seq_d1_to_reg( + main, commands, cmd, "write_cmd_phase2" + ) + + write_ans = util.mem_store_seq_d1(main, ans_mem, j.out, ans.out, "write_ans") + + main.control += [ + cb.while_( + err_eq_0[0].out, + err_eq_0[1], # Run while the `err` flag is down + [ + read_cmd, # Read `commands[i]` + write_cmd_to_reg, # Write it to `cmd` + cb.par( # Now, in parallel, act based on the value of `cmd` + cb.if_( + # Is this a pop? + cmd_eq_0[0].out, + cmd_eq_0[1], + [ # A pop + cb.invoke( # First we call pop + queue, + in_cmd=cmd.out, + ref_ans=ans, + ref_err=err, + ), + # AM: my goal is that, + # if err flag comes back raised, + # we do not perform this write or this incr_j + write_ans, + incr_j, + ], + ), + cb.if_( # Is this a push? + cmd_neq_0[0].out, + cmd_neq_0[1], + cb.invoke( # A push + queue, + in_cmd=cmd.out, + ref_ans=ans, + ref_err=err, + ), + ), + ), + incr_i, # Increment the command index + cb.invoke( # If i = MAX_CMDS, raise error flag + raise_err_if_i_eq_max_cmds, in_i=i.out, ref_err=err + ), # AM: hella hacky + ], + ), + ] diff --git a/calyx-py/test/correctness/fifo.py b/calyx-py/test/correctness/fifo.py index fd01e5049a..4789f40332 100644 --- a/calyx-py/test/correctness/fifo.py +++ b/calyx-py/test/correctness/fifo.py @@ -1,33 +1,7 @@ # pylint: disable=import-error import calyx.builder as cb import calyx.builder_util as util - - -def insert_raise_err_if_i_eq_15(prog): - """Inserts a the component `raise_err_if_i_eq_15` into the program. - - It has: - - one input, `i`. - - one ref register, `err`. - - If `i` equals 15, it raises the `err` flag. - """ - raise_err_if_i_eq_15: cb.ComponentBuilder = prog.component("raise_err_if_i_eq_15") - i = raise_err_if_i_eq_15.input("i", 32) - err = raise_err_if_i_eq_15.reg("err", 1, is_ref=True) - - i_eq_15 = util.insert_eq(raise_err_if_i_eq_15, i, 15, "i_eq_15", 32) - raise_err = util.insert_reg_store(raise_err_if_i_eq_15, err, 1, "raise_err") - - raise_err_if_i_eq_15.control += [ - cb.if_( - i_eq_15[0].out, - i_eq_15[1], - raise_err, - ) - ] - - return raise_err_if_i_eq_15 +import calyx.queue_call as qc def insert_fifo(prog, name): @@ -37,7 +11,7 @@ def insert_fifo(prog, name): - one input, `cmd`. - one memory, `mem`, of size 10. - two registers, `next_write` and `next_read`. - - three ref registers, `ans`, `err`, and `len`. + - two ref registers, `ans` and `err`. """ fifo: cb.ComponentBuilder = prog.component(name) @@ -45,10 +19,8 @@ def insert_fifo(prog, name): # If this is 0, we pop. If it is 1, we peek. Otherwise, we push the value. mem = fifo.seq_mem_d1("mem", 32, 10, 32) - write = fifo.reg("next_write", 32) # The next address to write to read = fifo.reg("next_read", 32) # The next address to read from - # We will orchestrate `mem`, along with the two pointers above, to # simulate a circular queue of size 10. @@ -56,13 +28,9 @@ def insert_fifo(prog, name): # If the user wants to pop, we will write the popped value to `ans` err = fifo.reg("err", 1, is_ref=True) - # We'll raise this as a general error flag: - # overflow, - # underflow, - # if the user calls pop and push at the same time, - # or if the user issues no command. + # We'll raise this as a general error flag for overflow and underflow - len = fifo.reg("len", 32, is_ref=True) # The length of the queue + len = fifo.reg("len", 32) # The length of the FIFO # Cells and groups to compute equality cmd_eq_0 = util.insert_eq(fifo, cmd, 0, "cmd_eq_0", 32) # `cmd` == 0 @@ -96,15 +64,16 @@ def insert_fifo(prog, name): write_to_mem = util.mem_store_seq_d1( fifo, mem, write.out, cmd, "write_payload_to_mem" ) - read_from_mem = util.mem_read_seqd1( + read_from_mem = util.mem_read_seq_d1( fifo, mem, read.out, "read_payload_from_mem_phase1" ) - write_to_ans = util.mem_write_seqd1_to_reg( + write_to_ans = util.mem_write_seq_d1_to_reg( fifo, mem, ans, "read_payload_from_mem_phase2" ) fifo.control += [ cb.par( + # Was it a pop or a push? We can do both cases in parallel. cb.if_( # Did the user call pop? cmd_eq_0[0].out, @@ -171,112 +140,11 @@ def insert_fifo(prog, name): return fifo -def insert_main(prog): - """Inserts the component `main` into the program. - This will be used to `invoke` the component `fifo`. - """ - main: cb.ComponentBuilder = prog.component("main") - - # The user-facing interface of the `main` component is: - # - a list of commands (the input) - # where each command is a 32-bit unsigned integer, with the following format: - # `0`: pop - # `1`: peek - # any value greater than 1: push that value - # - a list of answers (the output). - commands = main.seq_mem_d1("commands", 32, 15, 32, is_external=True) - ans_mem = main.seq_mem_d1("ans_mem", 32, 10, 32, is_external=True) - - # The two components we'll use: - fifo = main.cell("myfifo", insert_fifo(prog, "fifo")) - raise_err_if_i_eq_15 = main.cell( - "raise_err_if_i_eq_15", insert_raise_err_if_i_eq_15(prog) - ) - - # We will use the `invoke` method to call the `fifo` component. - # The fifo component takes three `ref` inputs: - err = main.reg("err", 1) # A flag to indicate an error - ans = main.reg("ans", 32) # A memory to hold the answer of a pop - len = main.reg("len", 32) # A register to hold the len of the queue - - # We will set up a while loop that runs over the command list, relaying - # the commands to the `fifo` component. - # It will run until the `err` flag is raised by the `fifo` component. - - i = main.reg("i", 32) # The index of the command we're currently processing - j = main.reg("j", 32) # The index on the answer-list we'll write to - cmd = main.reg("command", 32) # The command we're currently processing - - zero_i = util.insert_reg_store(main, i, 0, "zero_i") # zero out `i` - zero_j = util.insert_reg_store(main, j, 0, "zero_j") # zero out `j` - incr_i = util.insert_incr(main, i, "incr_i") # i = i + 1 - incr_j = util.insert_incr(main, j, "incr_j") # j = j + 1 - err_eq_0 = util.insert_eq(main, err.out, 0, "err_eq_0", 1) # is `err` flag down? - cmd_eq_0 = util.insert_eq(main, cmd.out, 0, "cmd_eq_0", 32) # cmd == 0 - cmd_neq_0 = util.insert_neq( - main, cmd.out, cb.const(32, 0), "cmd_neq_0", 32 - ) # cmd != 0 - - read_cmd = util.mem_read_seqd1(main, commands, i.out, "read_cmd_phase1") - write_cmd_to_reg = util.mem_write_seqd1_to_reg( - main, commands, cmd, "write_cmd_phase2" - ) - - write_ans = util.mem_store_seq_d1(main, ans_mem, j.out, ans.out, "write_ans") - - main.control += [ - zero_i, - zero_j, - cb.while_( - err_eq_0[0].out, - err_eq_0[1], # Run while the `err` flag is down - [ - read_cmd, # Read `commands[i]` - write_cmd_to_reg, # Write it to `cmd` - cb.par( - cb.if_( - # Is this a pop? - cmd_eq_0[0].out, - cmd_eq_0[1], - [ # A pop - cb.invoke( # First we call pop - fifo, - in_cmd=cmd.out, - ref_ans=ans, - ref_err=err, - ref_len=len, - ), - # AM: if err flag comes back raised, - # do not perform this write or this incr - write_ans, - incr_j, - ], - ), - cb.if_( # Is this a push? - cmd_neq_0[0].out, - cmd_neq_0[1], - cb.invoke( # A push - fifo, - in_cmd=cmd.out, - ref_ans=ans, - ref_err=err, - ref_len=len, - ), - ), - ), - incr_i, # Increment the command index - cb.invoke( # If i = 15, raise error flag - raise_err_if_i_eq_15, in_i=i.out, ref_err=err - ), # AM: hella hacky - ], - ), - ] - - def build(): """Top-level function to build the program.""" prog = cb.Builder() - insert_main(prog) + fifo = insert_fifo(prog, "fifo") + qc.insert_main(prog, fifo) return prog.program diff --git a/calyx-py/test/correctness/pifo.data b/calyx-py/test/correctness/pifo.data new file mode 100644 index 0000000000..303b30a41c --- /dev/null +++ b/calyx-py/test/correctness/pifo.data @@ -0,0 +1,45 @@ +{ + "commands": { + "data": [ + 101, + 102, + 0, + 0, + 103, + 104, + 201, + 202, + 0, + 0, + 0, + 0, + 105, + 106, + 0 + ], + "format": { + "is_signed": false, + "numeric_type": "bitnum", + "width": 32 + } + }, + "ans_mem": { + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "format": { + "is_signed": false, + "numeric_type": "bitnum", + "width": 32 + } + } +} diff --git a/calyx-py/test/correctness/pifo.expect b/calyx-py/test/correctness/pifo.expect new file mode 100644 index 0000000000..307166fe21 --- /dev/null +++ b/calyx-py/test/correctness/pifo.expect @@ -0,0 +1,31 @@ +{ + "ans_mem": [ + 101, + 102, + 201, + 103, + 202, + 104, + 105, + 0, + 0, + 0 + ], + "commands": [ + 101, + 102, + 0, + 0, + 103, + 104, + 201, + 202, + 0, + 0, + 0, + 0, + 105, + 106, + 0 + ] +} diff --git a/calyx-py/test/correctness/pifo.py b/calyx-py/test/correctness/pifo.py new file mode 100644 index 0000000000..9d416dfc53 --- /dev/null +++ b/calyx-py/test/correctness/pifo.py @@ -0,0 +1,255 @@ +# pylint: disable=import-error +import fifo +import calyx.builder as cb +import calyx.builder_util as util +import calyx.queue_call as qc + + +def insert_flow_inference(comp: cb.ComponentBuilder, cmd, flow, group): + """The flow is needed when the command is a push. + If the value to be pushed is less than 200, we push to flow 0. + Otherwise, we push to flow 1. + This method adds a group to the component {comp} that does this. + 1. Within component {comp}, creates a group called {group}. + 2. Within {group}, creates a cell {cell} that checks for less-than. + 3. Puts the values of 199 and {cmd} into {cell}. + 4. Then puts the answer of the computation into {flow}. + 5. Returns the group that does this. + """ + cell = comp.lt("flow_inf", 32) + with comp.group(group) as infer_flow_grp: + cell.left = 199 + cell.right = cmd + flow.write_en = 1 + flow.in_ = cell.out + infer_flow_grp.done = flow.done + return infer_flow_grp + + +def invoke_fifo(fifo_cell, cmd, ans, err) -> cb.invoke: + """Invokes the cell {fifo_cell} with: + {cmd} passed by value + {ans} passed by reference + {err} passed by reference + """ + return cb.invoke( + fifo_cell, + in_cmd=cmd, + ref_ans=ans, + ref_err=err, + ) + + +def insert_pifo(prog, name): + """Inserts the component `pifo` into the program. + + The PIFO achieves a 50/50 split between two "flows" or "kinds". + That is, up to the availability of values, this PIFO seeks to alternate + between values of the two flows. + + We say "up to availability" because, if one flow is silent and the other + is active, the active ones gets to emit consecutive values (in temporary + violation of the 50/50 rule) until the silent flow starts transmitting again. + At that point we go back to 50/50. + + The PIFO's maximum capacity is 10. Create two FIFOs, each of capacity 10. + Let's say the two flow are called `0` and `1`, and our FIFOs are called + `fifo_0` and `fifo_1`. + Maintain additionally a register that points to which of these FIFOs is "hot". + Start off with `hot` pointing to `fifo_0` (arbitrarily). + + - `push(v, PIFO)`: + + If len(PIFO) = 10, raise an "overflow" err and exit. + + Otherwise, the charge is to enqueue value `v`. + Find out which flow `f` the value `v` should go to; + `f` better be either `0` or `1`. + Enqueue `v` into `fifo_f`. + Note that the FIFO's enqueue method is itself partial: it may raise + "overflow", in which case we propagate the overflow flag. + - `pop(PIFO)`: + + If `len(PIFO)` = 0, raise an "underflow" flag and exit. + + Try `pop(FIFO_{hot})`. + * If it succeeds it will return a value `v`; just propagate `v`. + Also flip `hot` so it points to the other FIFO. + * If it fails because of underflow, return `pop(FIFO_{not-hot})`. + If the _second_ pop also fails, propagate the error. + Leave `hot` as it was. + """ + + pifo: cb.ComponentBuilder = prog.component(name) + cmd = pifo.input("cmd", 32) + # If this is 0, we pop. Otherwise, we push the value. + + # Create the two FIFOs and ready them for invocation. + fifo_0 = pifo.cell("myfifo_0", fifo.insert_fifo(prog, "fifo_0")) + fifo_1 = pifo.cell("myfifo_1", fifo.insert_fifo(prog, "fifo_1")) + + flow = pifo.reg("flow", 1) # The flow to push to: 0 or 1. + # We will infer this using a separate component; + # it is a function of the value being pushed. + infer_flow = insert_flow_inference(pifo, cmd, flow, "infer_flow") + + ans = pifo.reg("ans", 32, is_ref=True) + # If the user wants to pop, we will write the popped value to `ans`. + + err = pifo.reg("err", 1, is_ref=True) + # We'll raise this as a general error flag for overflow and underflow. + + len = pifo.reg("len", 32) # The length of the PIFO. + + # Two registers that mark the next FIFO to `pop` from. + hot = pifo.reg("hot", 1) + + # Some equality checks. + hot_eq_0 = util.insert_eq(pifo, hot.out, 0, "hot_eq_0", 1) + hot_eq_1 = util.insert_eq(pifo, hot.out, 1, "hot_eq_1", 1) + flow_eq_0 = util.insert_eq(pifo, flow.out, 0, "flow_eq_0", 1) + flow_eq_1 = util.insert_eq(pifo, flow.out, 1, "flow_eq_1", 1) + len_eq_0 = util.insert_eq(pifo, len.out, 0, "len_eq_0", 32) + len_eq_10 = util.insert_eq(pifo, len.out, 10, "len_eq_10", 32) + cmd_eq_0 = util.insert_eq(pifo, cmd, 0, "cmd_eq_0", 32) + cmd_neq_0 = util.insert_neq(pifo, cmd, cb.const(32, 0), "cmd_neq_0", 32) + err_eq_0 = util.insert_eq(pifo, err.out, 0, "err_eq_0", 1) + err_neq_0 = util.insert_neq(pifo, err.out, cb.const(1, 0), "err_neq_0", 1) + + flip_hot = util.insert_bitwise_flip_reg(pifo, hot, "flip_hot", 1) + raise_err = util.insert_reg_store(pifo, err, 1, "raise_err") # set `err` to 1 + lower_err = util.insert_reg_store(pifo, err, 0, "lower_err") # set `err` to 0 + zero_out_ans = util.insert_reg_store(pifo, ans, 0, "zero_out_ans") + + len_incr = util.insert_incr(pifo, len, "len_incr") # len++ + len_decr = util.insert_decr(pifo, len, "len_decr") # len-- + + # The main logic. + pifo.control += [ + cb.par( + # Was it a pop or a push? We can do both cases in parallel. + cb.if_( + # Did the user call pop? + cmd_eq_0[0].out, + cmd_eq_0[1], + cb.if_( + # Yes, the user called pop. But is the queue empty? + len_eq_0[0].out, + len_eq_0[1], + [raise_err, zero_out_ans], # The queue is empty: underflow. + [ # The queue is not empty. Proceed. + # We must check if `hot` is 0 or 1. + lower_err, + cb.par( # We'll check both cases in parallel. + cb.if_( + # Check if `hot` is 0. + hot_eq_0[0].out, + hot_eq_0[1], + [ # `hot` is 0. We'll invoke `pop` on `fifo_0`. + invoke_fifo(fifo_0, cmd, ans, err), + # Our next step depends on whether `fifo_0` + # raised the error flag. + # We can check these cases in parallel. + cb.par( + cb.if_( + err_neq_0[0].out, + err_neq_0[1], + [ # `fifo_0` raised an error. + # We'll try to pop from `fifo_1`. + # We'll pass it a lowered err + lower_err, + invoke_fifo(fifo_1, cmd, ans, err), + ], + ), + cb.if_( + err_eq_0[0].out, + err_eq_0[1], + [ # `fifo_0` succeeded. + # Its answer is our answer. + flip_hot + # We'll just make `hot` point + # to the other FIFO. + ], + ), + ), + ], + ), + # If `hot` is 1, we proceed symmetrically. + cb.if_( + hot_eq_1[0].out, + hot_eq_1[1], + [ + invoke_fifo(fifo_1, cmd, ans, err), + cb.par( + cb.if_( + err_neq_0[0].out, + err_neq_0[1], + [ + lower_err, + invoke_fifo(fifo_0, cmd, ans, err), + ], + ), + cb.if_( + err_eq_0[0].out, + err_eq_0[1], + [flip_hot], + ), + ), + ], + ), + ), + len_decr, # Decrement the length. + # It is possible that an irrecoverable error was raised above, + # in which case the length should _not_ in fact be decremented. + # However, in that case the PIFO's `err` flag would also + # have been raised, and no one will check this length anyway. + ], + ), + ), + cb.if_( + # Did the user call push? + cmd_neq_0[0].out, + cmd_neq_0[1], + cb.if_( + # Yes, the user called push. But is the queue full? + len_eq_10[0].out, + len_eq_10[1], + [raise_err, zero_out_ans], # The queue is full: overflow. + [ # The queue is not full. Proceed. + lower_err, + # We need to check which flow this value should be pushed to. + infer_flow, # Infer the flow and write it to `fifo_{flow}`. + cb.par( + cb.if_( + flow_eq_0[0].out, + flow_eq_0[1], + # This value should be pushed to flow 0. + invoke_fifo(fifo_0, cmd, ans, err), + ), + cb.if_( + flow_eq_1[0].out, + flow_eq_1[1], + # This value should be pushed to flow 1. + invoke_fifo(fifo_1, cmd, ans, err), + ), + ), + len_incr, # Increment the length. + # It is possible that an irrecoverable error was raised above, + # in which case the length should _not_ in fact be incremented. + # However, in that case the PIFO's `err` flag would also + # have been raised, and no one will check this length anyway. + ], + ), + ), + ), + ] + + return pifo + + +def build(): + """Top-level function to build the program.""" + prog = cb.Builder() + pifo = insert_pifo(prog, "pifo") + qc.insert_main(prog, pifo) + return prog.program + + +if __name__ == "__main__": + build().emit()