Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Installation

RustSFQ is provided as a Rust library crate. To use it, you must first install Rust and its package manager, Cargo.

1. Install Rust

Please install Rust by following the instructions on the official website:

https://www.rust-lang.org/tools/install

Installing Rust will also install Cargo, the tool used to manage Rust projects and packages.

2. Create a New Rust Project

Once Cargo is installed, you can create a new Rust project using the following command:

cargo new my_rustsfq_project

3. Add RustSFQ to Dependencies

To use RustSFQ in your project, add it to your Cargo.toml under the [dependencies] section:

[dependencies]
rust_sfq = "0.1"  # Replace with the actual version

4. Optional: Using VSCode and rust-analyzer

You can use any text editor to develop with RustSFQ, but we recommend using Visual Studio Code along with the rust-analyzer extension.

Once installed, rust-analyzer provides powerful IDE features such as:

  • Real-time type inference
  • Inline compilation errors and warnings
  • Code navigation and autocompletion

These features significantly improve development productivity and code quality within the editor.

Getting Started with RustSFQ

This section walks you through a simple example that demonstrates the basic syntax and core functionality of the RustSFQ library.

We’ll build a half-adder circuit and export it as a SPICE netlist.


Example: Half Adder in RustSFQ

The following Rust program defines a half-adder circuit and generates a corresponding SPICE-format netlist.
We’ll review each part step-by-step below.

use rust_sfq::*;

fn half_adder() -> Circuit<3, 0, 2, 0> {
    let (mut circuit, [a, b, clk], [], [c_out, s_out], []) =
        Circuit::create(["a", "b", "clk"], [], ["c", "s"], [], "HalfAdder");

    let (a1, a2) = circuit.split(a);
    let (b1, b2) = circuit.split(b);
    let (clk1, clk2) = circuit.split(clk);
    circuit.label(&clk1, "clk_1");

    let c = circuit.and(a1, b1, clk1);
    let s = circuit.xor_labeled(a2, b2, clk2, "s");

    circuit.unify(c, c_out);
    circuit.unify(s, s_out);

    return circuit;
}

fn main() {
    let ha = half_adder();
    println!("{}", RsfqlibSpice::generate(&ha));
}
Half Adder Schematic

Project Setup and Import

#![allow(unused)]
fn main() {
use rust_sfq::*;
}

Start by importing all items from the rust_sfq crate. This gives you access to the main types like Circuit, Wire, and backend generators such as RsfqlibSpice.

Creating a New Circuit

#![allow(unused)]
fn main() {
let (mut circuit, [a, b, clk], [], [c_out, s_out], []) =
    Circuit::create(["a", "b", "clk"], [], ["c", "s"], [], "HalfAdder");
}

This call to Circuit::create() initializes a new circuit with:

  • Inputs: a, b, clk
  • Outputs: c, s
  • Name: "HalfAdder"

The second and fourth arguments (empty arrays) represent CounterInputs and CounterOutputs, which are unused in this example.

The returned values include:

  • a mutable Circuit object
  • Wire objects for the inputs
  • CounterWire objects for outputs

Splitting Wires for Fan-Out

#![allow(unused)]
fn main() {
let (a1, a2) = circuit.split(a);
let (b1, b2) = circuit.split(b);
let (clk1, clk2) = circuit.split(clk);
}

Circuit object has split() function, which takes one Wire object and returns two Wire objects.

By calling this function, a SPLIT gate is added to the circuit.

Labeling Wires

#![allow(unused)]
fn main() {
circuit.label(&clk1, "clk_1");
}

You can manually assign a label to any wire, which will appear in the generated netlist.
If you don’t provide a label, a unique name will be automatically assigned.

Creating Gates

#![allow(unused)]
fn main() {
let c = circuit.and(a1, b1, clk1);
let s = circuit.xor_labeled(a2, b2, clk2, "s");
}

Logic gates such as AND and XOR are created by calling methods on the Circuit object.
In this example:

  • and() creates a carry gate
  • xor_labeled() creates a sum gate with a labeled output wire "s"

xor_labeled() is a convenience wrapper for xor() followed by label().

Connecting to Circuit Outputs

#![allow(unused)]
fn main() {
circuit.unify(c, c_out);
circuit.unify(s, s_out);
}

To complete the circuit, connect each gate’s output to the corresponding circuit output using unify().

Exporting the Circuit

fn half_adder() -> Circuit<3, 0, 2, 0> {
    ...
    return circuit;
}
fn main() {
    let ha = half_adder();
    println!("{}", RsfqlibSpice::generate(&ha));
}

The half_adder() function returns a fully constructed Circuit object.
This object is parameterized by the number of inputs, counter inputs, outputs, and counter outputs:
Circuit<3, 0, 2, 0>.

The RsfqlibSpice::generate() function converts the circuit into a SPICE-format netlist based on the RSFQlib.
You can use a different backend (e.g. RsfqlibVerilog) to export in other formats.


Running the Program

You can generate the SPICE netlist by compiling and running the program:

cargo run

This will print the netlist to standard output.

Output: Example SPICE Netlist

Here is the SPICE netlist generated by the example:

.subckt HalfAdder a b clk c s
XSPLIT1 a _XSPLIT1_q1 _XSPLIT1_q2 THmitll_SPLIT
XSPLIT2 b _XSPLIT2_q1 _XSPLIT2_q2 THmitll_SPLIT
XSPLIT3 clk clk_1 _XSPLIT3_q2 THmitll_SPLIT
XAND4 _XSPLIT1_q1 _XSPLIT2_q1 clk_1 c THmitll_AND
XXOR5 _XSPLIT1_q2 _XSPLIT2_q2 _XSPLIT3_q2 s THmitll_XOR
.ends
  • Each logic gate is named sequentially (e.g., XAND4, XXOR5)
  • Wires starting with an underscore (e.g., _XSPLIT1_q1) are automatically generated
  • Labels like clk_1 and s appear as specified in the code

Summary

In this example, you learned how to:

  • Create a new circuit using Circuit::create()
  • Instantiate gates and label outputs
  • Connect gate outputs to declared circuit outputs
  • Export the circuit as a netlist

Circuit

Overview

The Circuit struct corresponds to a subcircuit in SPICE and serves as the main object for constructing SFQ circuits in RustSFQ.

The Circuit instance holds all the necessary information to generate a netlist.

By passing a reference to a backend's generate() function, you can convert the circuit into a string in formats such as SPICE or Verilog, depending on the backend.


Type Parameter

A Circuit is parameterized by four compile-time constants representing the number of I/O channels:

#![allow(unused)]
fn main() {
Circuit<NUM_INPUT, NUM_COUNTER_INPUT, NUM_OUTPUT, NUM_COUNTER_OUTPUT>
}

Input and output are I/O channels for Wire. These are normal I/O channels.

Counter input and counter output are I/O chennels for CounterWire. Counter input is logical input for CounterWire, but it is physical output in netlist.


Creating a Circuit Instance

A new Circuit can be instantiated using the Circuit::create() function. This function defines the structure of the circuit by specifying its I/O ports and name.

#![allow(unused)]
fn main() {
pub fn create(
    inputs: [&str; N_I],
    counter_inputs: [&str; N_CI],
    outputs: [&str; N_O],
    counter_outputs: [&str; N_CO],
    name: &str,
) -> (Circuit<N_I, N_CI, N_O, N_CO>, [Wire; N_I], [CounterWire; N_CI], [CounterWire; N_O], [Wire; N_CO])
}

Parameters:

  • inputs: Array of names for input wires
  • counter_inputs: Array of names for counter input wires
  • outputs: Array of names for output wires
  • counter_outputs: Array of names for counter output wires
  • name: A string representing the name of the subcircuit (used in the netlist)

The lengths of these arrays determine the type parameters of the Circuit.

Return Value:

This function returns a tuple of:

  • The Circuit instance itself
  • An array of Wire instances for the input ports
  • An array of CounterWire instances for the counter input ports
  • An array of CounterWire instances for the output ports
  • An array of Wire instances for the counter output ports

The wires for input ports are used to start constructing the circuit while the wires for output ports are used to finish constructing.


Functions

To construct a circuit, a Circuit instance provides the following functions:

Logic Gates

The and() function adds an AND gate to the circuit. The and_labeled() is a convenience function that combines and() with label(), allowing you to define the gate and assign a label to its output wire in a single step.

#![allow(unused)]
fn main() {
pub fn and(&self, a: Wire, b: Wire, clk: Wire) -> Wire
pub fn and_labeled(&self, a: Wire, b: Wire, clk: Wire, label: &str) -> Wire
}

This function takes ownership of the input wires: a, b, and clk. After being passed into the function, these wire values cannot be used again elsewhere in the program.

The function returns a new Wire that represents the output of the AND gate.


Other logic and routing gates (OR, XOR, XNOR, NOT, DFF, NDRO, JTL, BUFF, MERGE, and ZERO_ASYNC) are provided as similar functions. While the number of their input wires may differ, all of these functions return a single Wire corresponding to the gate's output.


SPLIT

The split() function adds an SPLIT gate to the circuit.

#![allow(unused)]
fn main() {
pub fn split(&mut self, a: Wire) -> (Wire, Wire)
pub fn split_labeled(&mut self, a: Wire, label1: &str, label2: &str) -> (Wire, Wire)
}

The function takes one input Wire and returns a tuple of two new Wire instances.

This is necessary whenever the same signal needs to be used as input to multiple gates, ensuring that each use has its own distinct Wire object.


Gates for CounterWire

To support circuits employing counter-flow clocking, BUFF and SPLIT are available for CounterWire.

#![allow(unused)]
fn main() {
pub fn cbuff(&mut self, q: CounterWire) -> CounterWire
pub fn cbuff_labeled(&mut self, q: CounterWire, label: &str) -> CounterWire
pub fn csplit(&mut self, q1: CounterWire) -> (Wire, CounterWire)
pub fn csplit_labeled(&mut self, q1: CounterWire, label_q2: &str, label_a: &str) -> (Wire, CounterWire) 
pub fn csplit2(&mut self, q1: CounterWire, q2: CounterWire) -> CounterWire 
pub fn csplit2_labeled(&mut self, q1: CounterWire, q2: CounterWire, label: &str) -> CounterWire
}

The cbuff() function takes a CounterWire representing the output of a BUFF gate then returns a new CounterWire representing the input of the gate.

The csplit() function takes one CounterWire representing the one output of a SPLIT gate then returns a tuple of a new Wire representing the other output of the gate and a new CounterWire representing the input of the gate.

The csplit2() function takes two CounterWires representing the outputs of a SPLIT gate then returns a new CounterWire representing the input of the gate.


Subcircuits

The subcircuit() function allows you to instantiate a reusable subcircuit within a larger circuit.

#![allow(unused)]
fn main() {
pub fn subcircuit<const M_I: usize, const M_CI: usize, const M_O: usize, const M_CO: usize>(
    &mut self,
    circuit: &Circuit<M_I, M_CI, M_O, M_CO>,
    inputs: [Wire; M_I],
    counter_inputs: [CounterWire; M_CI],
) -> ([Wire; M_O], [CounterWire; M_CO])
}

It takes:

  • A reference to an existing Circuit
  • An array of Wires for the subcircuit's inputs
  • An array of CounterWires for the subcircuit's counter inputs

It returns a tuple of:

  • An array of Wires for the subcircuit’s outputs
  • An array of CounterWires for the subcircuit’s counter outputs

The input arrays are checked at compile time for correct lengths, ensuring type safety

The subcircuit is passed by reference, so ownership is preserved and the same subcircuit can be reused multiple times in different contexts


Loops

To construct feedback loops, use the gen_loop() function:

#![allow(unused)]
fn main() {
pub fn gen_loop(&mut self, label: &str) -> (Wire, CounterWire)
}

This returns a pair of Wire and CounterWire representing the same wire.

By unifying the CounterWire with another Wire later, you can ensure the Wire has a driver.


Unification

The unify() function connects a Wire and a CounterWire.

Since a Wire has a driver and a CounterWire has a receiver, unifying them creates no new wire. Therefore, the function has no return value.

#![allow(unused)]
fn main() {
pub fn unify(&mut self, wire: Wire, cwire: CounterWire)
}

This is primarily used in two scenarios:

  • To connect a wire to an output port of the circuit
  • To close a feedback loop created with gen_loop()

The label of the unified wire is determined as follows:

  • If only one side has an explicit label, that label is used
  • If both sides have labels and they match, the label is retained
  • If both sides have labels and they differ, the function raises an error

Labeling

You can assign an explicit label to a wire using the label() function.

This function is defined generically for both Wire and CounterWire, meaning that T can be either type:

#![allow(unused)]
fn main() {
pub fn label<T>(&mut self, wire: &T, label: &str)
}

The function takes a reference to the wire, so ownership is not moved.

Explicit labels must not begin with an underscore to avoid conflicts with automatically generated labels.

A wire that already has an explicit label cannot be labeled again.

RustSFQ does not check for collisions among explicitly assigned labels; it is the user's responsibility to ensure uniqueness.


Exporting

By passing a reference of Circuit instance into a backend’s generate() function, you can convert the circuit into a backend-specific string representation.

The resulting string can be printed to standard output to obtain the final netlist.

use rust_sfq::*;

fn half_adder() -> Circuit<3, 0, 2, 0> { ... }
fn full_adder(ha: &Circuit<3, 0, 2, 0>) -> Circuit<4, 0, 2, 0> { ... }

fn main() {
    let half_adder = half_adder();
    let full_adder = full_adder(&half_adder);
    
    type Backend = RsfqlibSpice;
    println!("{}", Backend::generate(&half_adder));
    println!("{}", Backend::generate(&full_adder));
}

Wire and CounterWire

Overview

Wire and CounterWire represent electrical nets in a netlist and are used to describe the connections between gates within a circuit.

A Wire appears when constructing a circuit in a forward direction. It represents a net that has already been driven, but has not yet been received.
A CounterWire is used when constructing circuits in a counter-flow manner, such as in counter-flow clocking. It represents a net that has already been received, but has not yet been driven.

These objects are governed by Rust's ownership system, which ensures that each wire has exactly one driver and one receiver.

Generating Wires

Although Wire and CounterWire are structs, their constructors are private. Users cannot instantiate them freely.

They can only be obtained in the following ways:

  • As circuit's inputs or outputs when calling Circuit::create()
  • As outputs from gate functions such as and(), xor(), etc.
  • From loop construction using gen_loop()

Also, Wire instances cannot be cloned.

Using Wires

Wires are used in the following scenarios:

  • Gate inputs: You pass Wire instances as arguments to gate functions like circuit.and(a, b, clk). In doing so, ownership of the Wire is moved and cannot be reused.
  • Labeling: If you want to assign a label to a wire, pass a reference to label() like circuit.label(&a).
  • Unification: When connecting a Wire and a CounterWire, you pass them to unify() like circuit.unify(a, b). The Wire and CounterWire is consumed (ownership is moved).

The reason Circuit::create() returns CounterWires for outputs is because those nets are "to be driven." You use unify() to connect a Wire (already driven) to these CounterWires to complete the connection.

Prevention of Multiple Use

In SFQ circuits, multiple receivers (fanout) from a single wire is prohibited. This restriction is enforced statically via Rust’s ownership system.

When you use a Wire as an input to a gate function, ownership is moved.

You cannot pass the same Wire to another gate function afterward, preventing accidental multi-use.

#![allow(unused)]
fn main() {
let (mut circuit, [a, clk], [], [], []) =
    Circuit::create(["a", "clk"], [], [], [], "invalid");

let c = circuit.dff(a, clk);
let d = circuit.not(a, clk);    // use of moved value. failed to compile
}

Prevention of Unused Wires

Rust’s ownership system guarantees that a Wire is used at most once, but not necessarily at least once.

In most cases, an unused Wire will be caught by the Rust compiler as an unused variable. However, it is possible to bypass this warning like this:

#![allow(unused)]
fn main() {
let (mut circuit, [a, b, c], [], [], []) =
    Circuit::create(["a", "b", "c"], [], [], [], "invalid");

// underscored variable name
let _a = circuit.jtl(a);

// discard return value
circuit.jtl(b);

// irrelevant use
println!("{:?}", c.type_id());
}

To catch such silent mistakes, RustSFQ performs runtime validation:

  • Each Wire internally tracks a private counter for receivers.
  • When a Wire is dropped (i.e., its destructor is called), RustSFQ checks whether it has exactly one receiver.
  • If not, a runtime error is reported.

This mechanism ensures correctness even in subtle or intentionally suppressed cases.

Available Gates and Backends

Compatibility table (as of version 0.1.2)

GateRsfqlibSpiceRsfqlibVerilog
JTL
SPLIT
MERGE
AND
OR
XOR
NOT
XNOR
DFF
NDRO
BUFF
ZERO_ASYNC

Backends

For Rust Beginners

This page is intended for readers who have never used Rust before.
It explains the minimum Rust knowledge required to use RustSFQ, including basic syntax, data types, ownership, and how to run your first Rust program.


Hello World

Creating a Project

To create a new project using Cargo (Rust’s build system and package manager), use the following command:

cargo new hello_rust

This creates a new folder hello_rust with the basic project structure.

The main Function

In Rust, the execution of any program begins at the main function. This is the entry point of the program.

Here is the simplest possible Rust program:

fn main() {
    println!("Hello, World!");
}

Running a Project

Navigate into the project directory and run it:

cd hello_rust
cargo run

This will compile the code and execute the main function, producing the output:

Hello, World!

The print! Macro

Rust uses macros for certain language features, and printing to the console is done using the print! macro (note the exclamation mark !).

println! macro prints text with a newline.

#![allow(unused)]
fn main() {
print!("Hello");
println!(", World!");
}

output:

Hello, World!

You can also print variables by using {} inside the string:

#![allow(unused)]
fn main() {
let name = "Alice";
println!("Hello, {}!", name); // Hello, Alice!
let x = 1;
let y = 2;
println!("{} + {} = {}", x, y, x + y);  // 1 + 2 = 3
}

Variables and Types in Rust

In Rust, variables are declared using the let keyword. By default, variables are immutable, but you can make them mutable by adding mut.

#![allow(unused)]
fn main() {
let x = 5;
println!("x = {}", x);  // x = 5
// x = 6; // Error: `x` is immutable

let mut y = 10;
y = 20; // Allowed: `y` is mutable
println!("y = {}", y);  // y = 20
}

Shadowing (Re-declaring a Variable)

Rust allows you to re-declare a variable using let again. This is called shadowing and can be used to change the value.

#![allow(unused)]
fn main() {
let z = 100;
let z = z + 1;
println!("z = {}", z);  // z = 101
}

Integers

Rust has both signed and unsigned integer types with an explicit size, such as:

  • i8, i16, i32, i64, i128: signed
  • u8, u16, u32, u64, u128: unsigned

These types depend on the system architecture (32-bit or 64-bit):

  • isize = signed integer the size of a pointer
  • usize = unsigned integer the size of a pointer

By default:

#![allow(unused)]
fn main() {
let a = 10;      // inferred as i32
let b = 20u64;   // explicitly u64
let c: i16 = -5; // type annotation
}

Type Annotations

While Rust often infers types automatically, you can specify them explicitly when needed:

#![allow(unused)]
fn main() {
let x: u32 = 123;
let y: f64 = 3.1415;
let flag: bool = true;
}

String Types

Rust has two kinds of strings:

  1. String slices (&str): used for fixed string data
  2. String: a growable heap-allocated string
#![allow(unused)]
fn main() {
let name: &str = "Alice";
println!("{}", name);   // Alice

let mut greeting = String::from("Hello");
greeting.push_str(", World!");
println!("{}", greeting);   // Hello, World!
}

Converting String to &str

  1. Using .as_str()
  2. Using & for implicit conversion
fn print_str(s: &str) {
    println!("{}", s);
}

fn main() {
    let s = String::from("Hello");

    print_str(s.as_str());  // OK. explicit conversion
    print_str(&s);          // OK. implicit conversion from &String to &str
    print_str(s);           // NG. Cannot implicitly convert from String to &str
}

Functions

In Rust, functions are defined using the fn keyword. All parameters and return types must be explicitly typed.
Unlike Python or JavaScript, Rust does not support default parameter values.
You must explicitly pass all arguments when calling a function.

Basic Syntax

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn square(x: i32) -> i32 {
    return x * x;
}

fn main() {
    let sum = add(3, 5);
    let sq = square(sum);
}
  • fn — starts a function definition

  • (a: i32, b: i32) — parameters with required type annotations

  • -> i32 — return type (also required)

  • The final expression (without a semicolon) is returned

    • You can also use return (with a semicolon)

Ownership and References

Rust has a unique ownership system that ensures memory safety without a garbage collector.

Copy vs Move

Some simple types, like integers, implement the Copy trait.
This means assigning them creates a copy, and both variables can be used.

#![allow(unused)]
fn main() {
let a = 5;
let b = a; // a is copied into b

println!("a = {}, b = {}", a, b); // OK. both are usable
}

However, complex types like String do not implement Copy.
Instead, assigning them transfers ownership — this is called a move.

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ownership moves from s1 to s2

// println!("{}", s1); // Error: s1 was moved
println!("{}", s2);   // OK
}

Once ownership has moved, the original variable (s1) can no longer be used.

References: Borrowing Instead of Moving

To use a value without taking ownership, you can borrow it by using a reference (&).

fn print_message(msg: &String) {
    println!("Message: {}", msg);
}

fn main() {
    let s = String::from("Rust");
    print_message(&s); // pass by reference
    println!("{}", s); // OK. still usable
}
  • &s is a immutable reference to s
  • Ownership is not transferred
  • The original variable remains usable

Mutable References

If you want to modify a value through a reference, use a mutable reference (&mut).

fn change(s: &mut String) {
    s.push_str(", Rust!");
}

fn main() {
    let mut s = String::from("Hi");
    change(&mut s); // pass by mutable reference
    println!("{}", s); // "Hi, Rust!"
}

Reference Safety Rules

  • Only one mutable reference (&mut) is allowed at a time
  • A mutable reference (&mut) and immutable reference (&) cannot coexist
  • Multiple immutable references (&) are allowed simultaneously

These rules are enforced at compile time, ensuring memory safety without the need for runtime checks.


Array, Vector, and Tuple

Rust provides multiple ways to store collections of values.
Here are the most common types: array, vector, and tuple.

Array [T; N]

  • Fixed-size, stack-allocated collection of elements of the same type.
  • Size N must be known at compile time.
  • You cannot resize an array after creation.
  • Arrays can also be unpacked (destructured) just like tuples.
#![allow(unused)]
fn main() {
let arr: [i32; 3] = [10, 20, 30];
println!("{}", arr[0]); // 10

let [x, y, z] = arr;
println!("x = {}, y = {}, z = {}", x, y, z); // x = 10, y = 20, z = 30
}

Vector Vec<T>

  • Growable, heap-allocated list of elements.
  • All elements must be the same type.
  • You cannot destructure a vector in the same way as an array.
#![allow(unused)]
fn main() {
let mut vec: Vec<i32> = Vec::new();
vec.push(1);
vec.push(2);
println!("{:?}", vec); // [1, 2]
}

Shortcut initialization with macro:

#![allow(unused)]
fn main() {
let v = vec![1, 2, 3];
}

Tuple (T1, T2, ...)

  • Group values of different types together.
  • Fixed size and order matters.
#![allow(unused)]
fn main() {
let tup: (i32, bool, &str) = (42, true, "hello");
let (a, b, c) = tup; // destructuring

println!("First: {}", tup.0); // 42
}

Tuples are especially useful for returning multiple values from a function.


Using Modules

Rust uses a modular system to organize code. When working with external crates (libraries) or internal modules, you use the use keyword to bring items into scope.

External Libraries

To use an external library (crate), you must:

  1. Add it to your Cargo.toml dependencies:

    [dependencies]
    rust_sfq = "0.1"
    
  2. Then import items from the crate in your code:

    #![allow(unused)]
    fn main() {
    use rust_sfq::*;
    }

Rust will download the crate from crates.io the first time you build the project.

This brings all public items from the crate into scope, including:

  • Circuit
  • Wire, CounterWire
  • Backend modules like RsfqlibSpice or RsfqlibVerilog

If you prefer more explicit imports:

#![allow(unused)]
fn main() {
use rust_sfq::{Circuit, RsfqlibSpice};
}