ExSystolic.Backend.LinkOps (ex_systolic v0.2.0)

Copy Markdown View Source

Shared link-buffer operations used by all backends.

Centralizes the three operations that every backend must perform every tick:

  1. inject_streams/2 -- push items from external input streams into boundary link buffers, deferring values when a target link is full or absent.
  2. drain_links/2 -- read every link buffer once, returning a map of {coord, port} => value (or :empty) and the drained link list.
  3. write_pe_outputs/2 -- write PE-produced values into the link whose from endpoint matches; silently drop output for ports with no matching link or for full buffers.

Why a shared module?

Three near-identical implementations previously lived in Clock, Backend.Interpreted, and Backend.Partitioned, each with subtle differences. Centralization eliminates the bug-in-three-places risk.

Determinism

All operations are pure functions over their inputs. Iteration order is deterministic (Map iteration over the Erlang term order is stable for a given map shape).

Performance

The current implementation uses Enum.find_index + List.replace_at patterns, which are O(n) per write. For a 4×4 array with 24 links this is acceptable; for larger arrays an indexed map representation should be considered. See review item 2.1.

Summary

Functions

Reads every link buffer once, returning the input map and drained links.

Injects items from input_streams into the matching boundary link.

Writes PE outputs into the link whose from endpoint matches.

Types

input_streams()

@type input_streams() :: %{required(stream_key()) => [term()]}

inputs_map()

@type inputs_map() :: %{required(stream_key()) => term() | :empty}

stream_key()

@type stream_key() :: {term(), atom()}

tick_outputs()

@type tick_outputs() :: %{required(term()) => %{required(atom()) => term()}}

Functions

drain_links(links, input_ports)

@spec drain_links([ExSystolic.Link.t()], [stream_key()]) ::
  {inputs_map(), [ExSystolic.Link.t()]}

Reads every link buffer once, returning the input map and drained links.

Each {coord, port} in input_ports is looked up in the link list:

  • If a matching link exists, its head value is read (drained) and placed in the output map; the link is updated to remove the head.
  • If no matching link exists, the value is :empty.

Returns {inputs_map, drained_links}.

inject_streams(links, input_streams)

@spec inject_streams([ExSystolic.Link.t()], input_streams()) ::
  {[ExSystolic.Link.t()], input_streams()}

Injects items from input_streams into the matching boundary link.

For each {coord, port} key in input_streams:

  • If the corresponding link is found and accepts the write, the head of the stream is consumed. When the stream is exhausted the key is removed; otherwise the tail is retained.
  • If the link is full, the entire stream (including the head) is deferred to the next tick.
  • If no matching link exists, the stream is silently dropped.

Returns {updated_links, remaining_streams}.

write_pe_outputs(links, tick_outputs)

@spec write_pe_outputs([ExSystolic.Link.t()], tick_outputs()) :: [ExSystolic.Link.t()]

Writes PE outputs into the link whose from endpoint matches.

For each {coord, output_port_map} in tick_outputs, every {port, value} is written to the link with from == {coord, port}.

Outputs with no matching link are silently dropped (typical for the :result port which has no outgoing link). Writes that fail because the buffer is full are also silently dropped (back-pressure is not modelled in the current backend).

Returns the updated link list.