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 reqackconstreqack=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.
constg=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.
constnode1=g('node_label');An edge is created by calling a node. The optional argument is an object with two main
properties, width and capacity.
constedge1=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:
constadd=g('+',g()(),g()());// two inputs feeding an adderconstsum=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('+');// addg('-');// subtractg('*');// signed multiplyg('~ & |');// (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.
constmacros={custom: {// p.t = target (input) sockets, p.i = initiator (output) socketsdata: p=>p.i.map(lhs=>`assign ${lhs.wire} = ${p.t.map(e=>e.wire).join(' ^ ')};`)}};constg=reqack.circuit();constfn=g('custom');g()()(fn);g()()(fn);fn({capacity: 1})();constrtl=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.
constrtl=reqack.verilog(g,macros);// synthesizable Verilog (datapath + _ctrl module)constsvg=reqack.svg(g);// SVG diagram (dagre layout)constdot=reqack.dot(g);// Graphviz dot sourceconstmanifest=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:
constg=reqack.circuit('eb1');g()({width: 4,capacity: 1})();// target -> EB1 edge -> initiatorconstrtl=reqack.verilog(g,{});produces the datapath module and its handshake controller:
moduleeb1 (
// 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,
inputi_1_ack
);
wire [3:0] dat0, dat0_nxt;
// per nodeassign dat0_nxt = t_0_dat; // node:0 is target portassign i_1_dat = dat0; // node:1 is initiator port// per edgewire en0; // edge:0 EB1reg [3:0] dat0_r;
always @(posedge clk) if (en0) dat0_r <= dat0_nxt;
assign dat0 = dat0_r;
eb1_ctrluctrl (
.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// eb1moduleeb1_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,
outputen0
);
wire req0, ack0, ack0_0, req0_0;
// node:t_0 targetassign req0 = t_0_req;
assign t_0_ack = ack0;
// edge:0 EB1wire ack0m;
reg req0m;
assign en0 = req0 & ack0;
assign ack0 =~req0m | ack0m;
always @(posedge clk ornegedge reset_n) if (~reset_n) req0m <=1'b0; else req0m <=~ack0 | req0;
// edge:0 forkassign req0_0 = req0m;
assign ack0m = ack0_0;
// node:1 initiatorassign 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.
consthls=reqack.hls;constresultEdge=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