Skip to main content

Python API

SHDB provides a Python API for programmatic debugging, test automation, and integration with other tools.

Quick Start

import shdb

# Load a circuit
circuit = shdb.Circuit("adder16.shdl")

# Basic operations
circuit.reset()
circuit.poke("A", 42)
circuit.poke("B", 17)
circuit.step()
print(circuit.peek("Sum")) # 59

# Clean up
circuit.close()

Loading Circuits

from SHDL Source

circuit = shdb.Circuit("myCircuit.shdl")

Compiles with -g automatically.

From Compiled Library

circuit = shdb.Circuit(
library="libmyCircuit.dylib",
debug_info="libmyCircuit.shdb"
)

Context Manager

with shdb.Circuit("adder16.shdl") as circuit:
circuit.poke("A", 100)
circuit.step()
print(circuit.peek("Sum"))
# Automatically cleaned up

Basic Operations

Setting Values

circuit.poke("A", 42)
circuit.poke("B", 0xFF)
circuit.poke("A", 0b10101010)

# Bit slicing
circuit.poke_bits("A", 1, 8, 0xFF) # Set bits 1-8

Reading Values

value = circuit.peek("Sum")
cout = circuit.peek("Cout")

# Bit slicing
bit5 = circuit.peek_bit("Sum", 5)
upper = circuit.peek_bits("Sum", 8, 16)

Simulation Control

circuit.reset()           # Reset to initial state
circuit.step() # Advance 1 cycle
circuit.step(10) # Advance 10 cycles

Cycle Count

print(circuit.cycle)      # Current cycle number

Internal Gate Access

Reading Gates

# By hierarchical name
value = circuit.peek_gate("fa1.x1")
value = circuit.peek_gate("fa1_x1") # Flattened name also works

# Get gate info
gate = circuit.get_gate("fa1_x1")
print(gate.name) # "fa1_x1"
print(gate.type) # "XOR"
print(gate.output) # 1 or 0
print(gate.hierarchy) # "Adder16/fa1/x1"

Listing Gates

# All gates
for gate in circuit.gates():
print(f"{gate.name}: {gate.output}")

# Filtered
for gate in circuit.gates("fa1*"):
print(gate.name, gate.output)

# By type
xor_gates = circuit.gates(type="XOR")

Breakpoints and Watchpoints

Setting Breakpoints

# Break on any change
bp1 = circuit.breakpoint("Cout")

# Conditional
bp2 = circuit.breakpoint("Cout", condition="Cout == 1")
bp3 = circuit.breakpoint("Sum", condition="Sum > 255")

# On internal gates
bp4 = circuit.breakpoint("fa1.o1")

Watchpoints

wp = circuit.watchpoint("Sum")

Running to Breakpoint

result = circuit.continue_()  # Note underscore (continue is Python keyword)

if result.stopped:
print(f"Stopped at cycle {result.cycle}")
print(f"Reason: {result.reason}")
print(f"Signal: {result.signal}")
print(f"Old: {result.old_value}, New: {result.new_value}")

Callback-based Watching

def on_carry(signal, old_value, new_value):
print(f"Carry changed: {old_value} -> {new_value}")
return True # Continue execution (False to stop)

circuit.watch("Cout", on_carry)
circuit.run() # Runs until callback returns False or error

Managing Breakpoints

bp.disable()
bp.enable()
bp.delete()

# Clear all
circuit.clear_breakpoints()

Hierarchy Navigation

Getting Hierarchy

hier = circuit.hierarchy()
print(hier) # Tree structure

for instance in circuit.instances():
print(f"{instance.name}: {instance.type}")

Scope

circuit.scope("fa1")
print(circuit.current_scope) # "Adder16/fa1"

# Relative access
circuit.peek("x1") # Same as fa1.x1

circuit.scope("..") # Go up
circuit.scope("/") # Go to root

Debug Information

Port Info

for port in circuit.inputs:
print(f"{port.name}: {port.width} bits")

for port in circuit.outputs:
print(f"{port.name}: {port.width} bits")

Source Mapping

# What gates came from a source line?
gates = circuit.gates_from_line("adder16.shdl", 8)

# What source line defined this gate?
loc = circuit.source_location("fa1_x1")
print(f"{loc.file}:{loc.line}")

Waveform Recording

Recording

circuit.record_signals(["A", "B", "Sum", "Cout"])
circuit.record_start()

for a in range(256):
circuit.poke("A", a)
circuit.step()

circuit.record_stop()

Accessing Recorded Data

# Get all recorded data
data = circuit.record_data()
for sample in data:
print(f"Cycle {sample.cycle}: Sum={sample['Sum']}")

# Get specific signal
sum_values = circuit.record_signal("Sum")

Exporting

circuit.record_export("waves.vcd")
circuit.record_export("waves.json")
circuit.record_export("waves.csv")

Practical Examples

Test Harness

import shdb

def test_adder():
with shdb.Circuit("adder16.shdl") as c:
# Test cases: (a, b, expected_sum, expected_cout)
tests = [
(0, 0, 0, 0),
(1, 1, 2, 0),
(42, 17, 59, 0),
(0xFFFF, 1, 0, 1), # Overflow
]

for a, b, exp_sum, exp_cout in tests:
c.reset()
c.poke("A", a)
c.poke("B", b)
c.step()

assert c.peek("Sum") == exp_sum, f"Sum mismatch for {a}+{b}"
assert c.peek("Cout") == exp_cout, f"Cout mismatch for {a}+{b}"

print("All tests passed!")

test_adder()

Exhaustive Testing

import shdb

def exhaustive_test_4bit():
with shdb.Circuit("adder4.shdl") as c:
failures = 0

for a in range(16):
for b in range(16):
c.poke("A", a)
c.poke("B", b)
c.step()

expected = (a + b) & 0x1F
actual = c.peek("Sum") | (c.peek("Cout") << 4)

if actual != expected:
print(f"FAIL: {a}+{b}={actual}, expected {expected}")
failures += 1

print(f"Exhaustive test: {256 - failures}/256 passed")
return failures == 0

exhaustive_test_4bit()

Performance Benchmarking

import shdb
import time

with shdb.Circuit("adder16.shdl") as c:
start = time.time()

for i in range(1_000_000):
c.poke("A", i & 0xFFFF)
c.poke("B", (i >> 16) & 0xFFFF)
c.step()

elapsed = time.time() - start
print(f"1M cycles in {elapsed:.2f}s = {1_000_000/elapsed:.0f} cycles/sec")

Gate-Level Analysis

import shdb

with shdb.Circuit("adder16.shdl") as c:
c.poke("A", 0xFFFF)
c.poke("B", 1)
c.step()

# Analyze carry chain
print("Carry chain propagation:")
for i in range(1, 17):
gate = c.get_gate(f"fa{i}_o1") # OR gate = carry out
print(f" Bit {i}: carry = {gate.output}")

Waveform Analysis

import shdb

with shdb.Circuit("adder16.shdl") as c:
c.record_signals(["Sum", "Cout"])
c.record_start()

# Run test pattern
for a in range(0, 65536, 1000):
c.poke("A", a)
c.poke("B", 1000)
c.step()

c.record_stop()

# Analyze
data = c.record_data()
overflow_cycles = [s.cycle for s in data if s["Cout"] == 1]
print(f"Overflow occurred at cycles: {overflow_cycles}")

c.record_export("analysis.vcd")

Integration with pytest

# test_adder.py
import pytest
import shdb

@pytest.fixture
def adder():
circuit = shdb.Circuit("adder16.shdl")
yield circuit
circuit.close()

def test_zero_addition(adder):
adder.poke("A", 0)
adder.poke("B", 0)
adder.step()
assert adder.peek("Sum") == 0
assert adder.peek("Cout") == 0

def test_simple_addition(adder):
adder.poke("A", 42)
adder.poke("B", 17)
adder.step()
assert adder.peek("Sum") == 59

def test_overflow(adder):
adder.poke("A", 0xFFFF)
adder.poke("B", 1)
adder.step()
assert adder.peek("Sum") == 0
assert adder.peek("Cout") == 1

@pytest.mark.parametrize("a,b,expected", [
(0, 0, 0),
(1, 1, 2),
(100, 200, 300),
(0x8000, 0x8000, 0),
])
def test_additions(adder, a, b, expected):
adder.poke("A", a)
adder.poke("B", b)
adder.step()
assert adder.peek("Sum") == expected

Run with:

pytest test_adder.py -v

API Reference

Circuit Class

class Circuit:
# Loading
def __init__(self, source=None, library=None, debug_info=None)
def close()

# Properties
@property cycle: int
@property inputs: List[PortInfo]
@property outputs: List[PortInfo]
@property current_scope: str

# Simulation
def reset()
def step(cycles=1)
def poke(signal: str, value: int)
def peek(signal: str) -> int
def poke_bits(signal: str, start: int, end: int, value: int)
def peek_bits(signal: str, start: int, end: int) -> int

# Gates
def peek_gate(name: str) -> int
def get_gate(name: str) -> GateInfo
def gates(pattern: str = "*", type: str = None) -> List[GateInfo]

# Breakpoints
def breakpoint(signal: str, condition: str = None) -> Breakpoint
def watchpoint(signal: str, condition: str = None) -> Watchpoint
def watch(signal: str, callback: Callable) -> Watch
def clear_breakpoints()
def continue_() -> StopResult
def run()

# Hierarchy
def hierarchy() -> HierarchyNode
def instances() -> List[InstanceInfo]
def scope(path: str)

# Source mapping
def gates_from_line(file: str, line: int) -> List[str]
def source_location(gate: str) -> SourceLocation

# Recording
def record_signals(signals: List[str])
def record_start()
def record_stop()
def record_data() -> List[Sample]
def record_signal(name: str) -> List[int]
def record_export(path: str)

Data Classes

@dataclass
class PortInfo:
name: str
width: int

@dataclass
class GateInfo:
name: str
type: str
output: int
hierarchy: str
source_file: str
source_line: int

@dataclass
class StopResult:
stopped: bool
cycle: int
reason: str
signal: str
old_value: int
new_value: int