Skip to content

Procedures for I/O

Waiting for a token on a port — checking with peek, retrying pull in phase 0, or retrying push in phase 1 — is a recurring pattern. When a module has many ports or must monitor several ports simultaneously, rewriting this loop for every port makes the behavior hard to read and easy to get wrong.

Encapsulating the I/O pattern in a procedure separates the "what to do with the token" from the "how to receive or send it". The procedure is defined once and instantiated for each port that needs it.


The port-pointer pattern

A procedure cannot declare ports in the structural sense — ports belong to modules. Instead, the procedure holds a C++ pointer to the port it manages. The parent module assigns this pointer in its init block.

procedure GetToken
    parameter int W = 4
    decl $
    inport<W>* src;   // pointer to the managed port, set by parent
    token<W>   tok;   // result token, readable by parent after run
    bool       pulled;
    $
    init $src = nullptr;$
    behavior
        ...
    end behavior
end procedure

In the parent:

module Merger
    inport in_a : width 4
    procedure get_a : GetToken<4>
    init $
    get_a.src = &in_a;   // wire procedure to port
    $
    ...
end module

The procedure's decl variables are C++ member variables of the procedure object. Because get_a is a member of Merger, get_a.tok, get_a.src, and get_a.pulled are all accessible from Merger's code blocks and init.


GetToken — one-shot receive

GetToken waits until a token is available on src, pulls it into tok, and returns. Its behavior is finite: it ends naturally after one successful pull, which returns control to the run get_a; statement in the caller.

// GetToken: waits for one token from a designated inport and pulls it into tok.
// The port is not declared in the procedure — the parent passes a pointer in init.
procedure GetToken
    parameter int W = 4

    decl $
    inport<W>* src;   // pointer to the associated inport — set by parent
    token<W>   tok;   // result: the received token, readable by parent after run
    bool       pulled;
    $
    init $src = nullptr;  pulled = false;$

    behavior
        $pulled = false;$;
        do
            wait until (this_phase == 0);
            $pulled = src->pull(tok);$;
            if (not pulled) then wait end if;
        while (not pulled) end do;
        // Behavior ends here; control returns to the run statement in the caller.
    end behavior
end procedure

SendToken — one-shot send with retry

SendToken pushes tok (set by the parent before calling run send) to dst, retrying each phase until push succeeds.

// SendToken: pushes tok (set by parent before run) to the designated outport,
// retrying each phase until the push succeeds, then returns.
procedure SendToken
    parameter int W = 4

    decl $
    outport<W>* dst;   // pointer to the associated outport — set by parent
    token<W>    tok;   // the token to send — set by parent before each run
    bool        done;
    $
    init $dst = nullptr;$

    behavior
        $done = false;$;
        do
            wait until (this_phase == 1);
            $done = dst->push(tok);$;
            if (not done) then wait end if;
        while (not done) end do;
        // Behavior ends; push succeeded; control returns to caller.
    end behavior
end procedure

Parallel I/O with procedures

A parallel block makes it natural to wait for tokens on multiple ports simultaneously. The block completes only when all branches have finished:

// Merger: combines one token from each input by summing the integer payloads.
// Uses GetToken and SendToken procedures for all port interaction.
module Merger
    inport  in_a : width 4
    inport  in_b : width 4
    outport outp : width 4

    // One I/O procedure instance per port
    procedure get_a : GetToken<4>
    procedure get_b : GetToken<4>
    procedure send  : SendToken<4>

    decl $int val_a;  int val_b;  int merged;$

    // Wire each procedure to its port
    init $
    get_a.src = &in_a;
    get_b.src = &in_b;
    send.dst  = &outp;
    $

    behavior
        do
            // Concurrently wait for one token from each inport.
            // The parallel block completes only when both GetToken procedures
            // have successfully pulled a token.
            [
                run get_a;
            ||
                run get_b;
            ];

            // Both procedures have completed; read their results.
            $
            sitar::unpack(get_a.tok, val_a);
            sitar::unpack(get_b.tok, val_b);
            merged = val_a + val_b;
            log << endl << "merged " << val_a << " + " << val_b << " = " << merged;
            sitar::pack(send.tok, merged);
            $;

            // Push the result (the SendToken procedure handles the retry loop).
            run send;
        while (1) end do;
    end behavior
end module

The key lines:

[
    run get_a;
||
    run get_b;
];

This suspends Merger until get_a and get_b have both completed — that is, until a token has arrived on both in_a and in_b. The two procedures run concurrently: if in_b has a token but in_a does not, the branch running get_b finishes first and waits for the get_a branch before the parallel block exits.

After the parallel block, the parent reads the results directly from the procedure variables:

$
sitar::unpack(get_a.tok, val_a);
sitar::unpack(get_b.tok, val_b);
$;

Complete example

The full example connects two counter sources and a sink to the Merger:

// Test harness: two producers feeding the merger, one sink consuming the output.
module Top
    submodule sys : System
end module

module System
    submodule src_a  : Counter<0>     // produces 0, 1, 2, ...
    submodule src_b  : Counter<2>     // produces 0, 2, 4, ... (step=2)
    submodule merger : Merger
    submodule sink   : PrintSink

    net na : capacity 2 width 4
    net nb : capacity 2 width 4
    net nc : capacity 2 width 4

    src_a.outp   => na    merger.in_a <= na
    src_b.outp   => nb    merger.in_b <= nb
    merger.outp  => nc    sink.inp    <= nc
end module

module Counter
    parameter int STEP  = 1
    outport outp : width 4
    decl $int val; token<4> t; bool ok;$
    init $val = 0;$
    behavior
        do
            wait until (this_phase == 1);
            $
            sitar::pack(t, val);
            ok = outp.push(t);
            if (ok) val += STEP;
            $;
            if (not ok) then wait end if;
            if (val >= 10 * STEP) then stop simulation; end if;
        while (1) end do;
    end behavior
end module

module PrintSink
    inport inp : width 4
    decl $token<4> t; int v;$
    behavior
        do
            wait until (this_phase == 0);
            $while (inp.pull(t)) { sitar::unpack(t, v); log << endl << "sink: " << v; }$;
            wait;
        while (1) end do;
    end behavior
end module

Expected output

(1,0) TOP.sys.merger : merged 0 + 0 = 0
(2,0) TOP.sys.merger : merged 1 + 2 = 3
(3,0) TOP.sys.merger : merged 2 + 4 = 6
(4,0) TOP.sys.merger : merged 3 + 6 = 9
...
Simulation stopped at time (11,0)

Design notes

Procedures vs submodules for I/O

Use a procedure for I/O when the I/O logic is tightly coupled to the parent's behavior — particularly when the parent needs to read the result immediately after the operation, or when it needs to wait for multiple ports in parallel.

Use a submodule when the I/O work should run continuously and independently of the parent's main loop. A submodule has its own behavior that runs concurrently with all other modules; it is not blocked by the parent's state.

Procedure variables and re-entrancy

Each procedure instance (get_a, get_b, send) has its own copy of the decl variables. Two instances of GetToken manage separate ports and separate tok fields without interference. However, Sitar does not allow recursive procedure calls.