JavaScript tool set to construct, transform, and analyze digital circuits based on an elastic (latency-insensitive) transactional protocol and a Request-Acknowledge handshake.
You describe a circuit through a small JavaScript API, wire together standard components from the library, or define your own. The resulting graph is then rendered to synthesizable Verilog RTL and to diagrams (SVG / Graphviz dot).
- Elastic by construction β every channel carries
req/ack/dat. Backpressure and flow control are generated for you. - Correct-by-construction transformations β change a channel's buffer capacity, retime, or insert bubbles without altering functional behavior.
- Pluggable components β standard controllers (fork, join, MIMO) plus custom node macros.
npm i reqackconst reqack = require('reqack');A circuit is a directed hypergraph:
- Circuit β the top-level graph. Becomes a Verilog module.
- Node β an operation, a custom component, or an I/O port. A node with no incoming edges is a target (module input); a node with no outgoing edges is an initiator (module output).
- Edge (channel) β a link between nodes carrying
{width, capacity}. Thecapacityselects the elastic buffer inserted on that channel.
The core API in lib/fhyper.js is intentionally compact: circuits, nodes, and edges are built by calling the value returned from the previous step.
A circuit is created by calling reqack.circuit. The optional label becomes the
Verilog module name.
const g = reqack.circuit('circuit_name');A node is created by calling the circuit. The optional label is used as a standard operator (see Operators), a custom macro name, or the root of a signal name.
const node1 = g('node_label');An edge is created by calling a node. The optional argument is an object with two main
properties, width and capacity.
const edge1 = node1({width: 32, capacity: 1});An edge is connected to a destination node by calling it with that node. The call returns the same edge, so connections chain (this is a fork β one source, many targets).
edge1(node2)(node3); // fan edge1 out to node2 and node3Nodes also accept upstream edges as extra arguments (a join β many sources, one node), including arrays of edges:
const add = g('+', g()(), g()()); // two inputs feeding an adder
const sum = g('+', [a, b, c].map(x => x())); // n inputs from an arraycapacity selects the channel's elastic buffer:
| capacity | buffer | latency | notes |
|---|---|---|---|
0 / omitted |
wire | 0 | asynchronous, combinational backpressure |
1 |
EB1 | 1 | async backpressure |
1.5 |
EB1.5 | 1 | synchronous backpressure, stall cap 2 |
1.7 |
EB1.7 | 1 | synchronous backpressure |
integer >= 2 |
FIFO | 1 | N-entry queue |
See docs/controller.md for the controller logic behind each.
A node whose label is a string is treated as a datapath operator. Multi-operator labels are evaluated in RPN (space-separated), consuming the node's inputs in order β see lib/rpn.js.
g('+'); // add
g('-'); // subtract
g('*'); // signed multiply
g('~ & |'); // (in RPN) invert, AND, ORSupported binary operators: + - * / % & | ^ << >> > >= < <= == != && ||.
Unary: ! ~. Numeric literals and swap are also recognized.
Pass a macros object to reqack.verilog to define nodes that emit their own datapath
and/or control logic. A macro may provide data, ctrl, ctrl2data, or parameters.
const macros = {
custom: {
// p.t = target (input) sockets, p.i = initiator (output) sockets
data: p => p.i.map(lhs =>
`assign ${lhs.wire} = ${p.t.map(e => e.wire).join(' ^ ')};`)
}
};
const g = reqack.circuit();
const fn = g('custom');
g()()(fn);
g()()(fn);
fn({capacity: 1})();
const rtl = reqack.verilog(g, macros);The built-in reqack.macros (currently deconcat) and reqack.ctrl.fork provide ready
components. See test/basic.js for many worked examples.
const rtl = reqack.verilog(g, macros); // synthesizable Verilog (datapath + _ctrl module)
const svg = reqack.svg(g); // SVG diagram (dagre layout)
const dot = reqack.dot(g); // Graphviz dot source
const manifest = reqack.manifest(g, name); // project manifest (module.exports = {...})reqack.verilog emits two modules: the datapath <name> and its handshake controller
<name>_ctrl.
A single 4-bit channel with a 1-entry elastic buffer:
const g = reqack.circuit('eb1');
g()({width: 4, capacity: 1})(); // target -> EB1 edge -> initiator
const rtl = reqack.verilog(g, {});produces the datapath module and its handshake controller:
module eb1 (
// per node (target / initiator)
input clk,
input reset_n,
input [3:0] t_0_dat,
input t_0_req,
output t_0_ack,
output [3:0] i_1_dat,
output i_1_req,
input i_1_ack
);
wire [3:0] dat0, dat0_nxt;
// per node
assign dat0_nxt = t_0_dat; // node:0 is target port
assign i_1_dat = dat0; // node:1 is initiator port
// per edge
wire en0; // edge:0 EB1
reg [3:0] dat0_r;
always @(posedge clk) if (en0) dat0_r <= dat0_nxt;
assign dat0 = dat0_r;
eb1_ctrl uctrl (
.clk(clk),
.reset_n(reset_n),
.t_0_req(t_0_req),
.t_0_ack(t_0_ack),
.i_1_req(i_1_req),
.i_1_ack(i_1_ack),
.en0(en0)
);
endmodule // eb1
module eb1_ctrl (
// per node (target / initiator)
input clk,
input reset_n,
input t_0_req,
output t_0_ack,
output i_1_req,
input i_1_ack,
output en0
);
wire req0, ack0, ack0_0, req0_0;
// node:t_0 target
assign req0 = t_0_req;
assign t_0_ack = ack0;
// edge:0 EB1
wire ack0m;
reg req0m;
assign en0 = req0 & ack0;
assign ack0 = ~req0m | ack0m;
always @(posedge clk or negedge reset_n) if (~reset_n) req0m <= 1'b0; else req0m <= ~ack0 | req0;
// edge:0 fork
assign req0_0 = req0m;
assign ack0m = ack0_0;
// node:1 initiator
assign i_1_req = req0_0;
assign ack0_0 = i_1_ack;
endmodule // eb1_ctrlThe data path (eb1) holds registers and multiplexing; the controller (eb1_ctrl)
holds the req/ack handshake FSM and drives each register's enable (en0).
reqack.hls turns a JavaScript arithmetic expression into a circuit, mapping operators to
elastic nodes β see lib/hls.js.
const hls = reqack.hls;
const resultEdge = hls((a, b, c) => (a + b) - c)('a', 'b', 'c');reqack.dagreβ the bundled dagre layout engine.reqack.firrtlβ FIRRTL backend (placeholder, not yet implemented).
- Link protocol β the
req/ack/datsignaling rules. - Controllers β fork, join, MIMO, and edge buffers (EB0/1/1.5/β¦).
- Transformations β retiming, bubble insertion, memory bypass.
- Labeling β how graph/node/edge labels are interpreted.
- References β background reading.
- LLM-friendly API β roadmap / proposal. Describes a future
named-method API (
addNode,addEdge,connectTo, validation, types). Not yet implemented β the current API is the one documented above.
npm i
npm testMIT LICENSE