A Python DSL for hardware description with LLM-powered compilation. Define hardware modules as Python functions, let an LLM generate the logic, and compile to MLIR or Verilog.
Python DSL (@module) → LLM Compilation → JSON-IR → MLIR / Verilog
- Frontend:
@moduledecorator extracts ports and logic descriptions from Python functions - Compiler: LLM generates JSON-IR body from natural-language descriptions (with validation feedback loop)
- IR: SSA-based intermediate representation with parsing, validation, and bit-width inference
- Interpreter: pure-Python, two-state functional and cycle simulation of JSON-IR
- Codegen: MLIR/CIRCT emission via pycde, with Verilog export
from cppl import module, In, Out, Clock, Design
@module
def Adder8(a: In[8], b: In[8]) -> Out[8]:
"""out equals a plus b (8-bit addition)."""
pass
@module
def ALU(op_code: In[2], op_a: In[8], op_b: In[8]) -> {"res": Out[8], "zero": Out[1]}:
adder8_out = Adder8(op_a, op_b)
return f"""
Based on op_code:
- 00: res = {adder8_out} (from Adder8 instance)
- 01: res = op_a - op_b
- 10: res = op_a & op_b
- 11: res = op_a | op_b
zero is 1 when res equals 0, otherwise 0.
"""
design = Design()
design.add(ALU) # automatically discovers and adds Adder8
print(design.to_verilog())Key points:
In[N]/Out[N]specify N-bit input/output portsClockspecifies a dedicated clock input port, for exampleclk: Clock- Single output uses
-> Out[N](named"out"), multiple outputs use-> {"name": Out[N], ...} - Docstrings describe the logic in natural language for the LLM
- Modules can instantiate other modules by calling them; use f-string returns to reference instance outputs
Design.add()recursively discovers and adds all sub-modules — only the top-level module needs to be added
Use @module(patterns=[...]) to provide examples that are executed against each
generated IR candidate. Patterns are included in the generation prompt and are
also independently checked by the IR interpreter. A mismatch is returned to the
repair agent with the failing inputs, expected outputs, and actual outputs.
from cppl import Case, Clock, In, Out, Sequence, Step, module
@module(patterns=[
Case(inputs={"a": 1, "b": 2}, outputs={"out": 3}),
])
def Adder(a: In[8], b: In[8]) -> Out[8]:
"""out equals a plus b."""
pass
@module(patterns=[
Sequence(steps=[
Step(inputs={"clk": 0, "d": 7}),
Step(inputs={"clk": 1}, outputs={"out": 7}),
]),
])
def Register(clk: Clock, d: In[8]) -> Out[8]:
"""Capture d on the rising edge of clk."""
passEach Case or Sequence starts from reset interpreter state. Steps within one
sequence share state, omitted inputs retain their previous values, and only the
listed outputs are checked. Passing patterns demonstrate agreement with those
examples; they are executable tests rather than a formal proof for every input.
CPPL's complete agent system is implemented with LangChain and LangGraph.
Every user-written @module is one atomic compilation boundary; the agent never
splits it into hidden submodules. LangChain owns provider chat models, prompt
pipelines, JSON parsing, and transport retries. A per-module LangGraph handles
cache lookup, generation, validation, requirement compression, and repair loops;
a deterministic hierarchy scheduler compiles the module DAG bottom-up, runs all
dependency-ready siblings concurrently, publishes validated child artifacts, and
then releases their parents for hierarchical validation. Validated results are cached by content under
.cppl/cache, so compiling an unchanged design again requires no LLM calls.
Existing code continues to work. Optional runtime controls and reports are
available through AgentConfig:
from cppl import AgentConfig, Design
design = Design(agent_config=AgentConfig(
max_parallelism=4,
context_window_tokens=32768,
cache_enabled=True,
tool_mode="auto", # auto | off | required
max_tool_rounds=12,
))
design.add(ALU)
report = design.compile_with_report(max_retries=3)
print(report.llm_calls, report.cache_hits)
for name, module_report in report.module_reports.items():
print(
name,
module_report.status,
module_report.attempts, # generation/repair sessions
module_report.model_turns, # actual provider requests
module_report.tool_calls, # isolated IR tool calls
module_report.simulation_attempts, # candidates checked by patterns
module_report.static_repairs, # JSON/schema/SSA/width repairs
)max_retries limits only executable simulation/pattern attempts. JSON syntax,
schema, SSA, port, instance, and width errors are sent back to the Repair Agent
without consuming this counter; those repairs continue until valid or until the
module time/token budget is exhausted. CompileOptions.max_simulation_attempts
provides the equivalent per-run override.
Python configuration overrides CPPL_AGENT_* environment variables. Reports
contain timing, token estimates, cache and diagnostic metadata, but never store
API keys or complete prompts/responses.
Tool-capable models can incrementally construct and repair JSON-IR through an
isolated module-local session. The always-available tools cover replacement,
JSON Patch edits, JSON Pointer queries, validation, pattern simulation, and
submission. If jq, rg, or diff are installed they are used as bounded
Unix enhancements; they are optional and are never invoked through a shell.
Tools operate only on temporary candidate snapshots and cannot modify project
files. In the default auto mode, models or custom backends without native tool
calling transparently use the existing structured/direct JSON generation path.
The corresponding environment controls are CPPL_AGENT_TOOL_MODE,
CPPL_AGENT_MAX_TOOL_ROUNDS, CPPL_AGENT_TOOL_TIMEOUT, and
CPPL_AGENT_MAX_TOOL_OUTPUT_CHARS.
Compilation logs are enabled by default and written to stderr, so JSON, MLIR,
and Verilog output on stdout remains clean:
[CPPL 0.02s] START — compiling 2 module(s)
[CPPL 0.05s] GENERATE Adder8 — generation call 1; next simulation attempt 1/3
[CPPL 1.24s] SUCCESS Adder8 — validated in 1.19s
[CPPL 2.12s] DONE — 2 module(s), 2 LLM call(s), 0 cache hit(s) in 2.10s
Use AgentConfig(log_enabled=False) or CPPL_AGENT_LOG_ENABLED=false for
silent compilation. Custom applications can receive the same structured event
stream through an observer:
from cppl import CompileEvent, Design
class MyObserver:
def on_event(self, event: CompileEvent) -> None:
save_event(event)
design = Design(observer=MyObserver())Install (editable / development mode):
pip install -e .Run the example as a normal Python script:
python examples/python/cppl_demo.pyThe intermediate representation is a JSON array of modules. Each module has name, ports, and a body of SSA operations:
[{
"name": "Adder8",
"ports": {
"a": {"dir": "input", "width": 8},
"b": {"dir": "input", "width": 8},
"sum": {"dir": "output", "width": 8}
},
"body": [
{"id": "result", "op": "add", "args": ["a", "b"]},
{"op": "output", "args": {"sum": "result"}}
]
}]Supported operations: constant, add, sub, mul, div, div_s, mod_u, mod_s, and, or, xor, not, neg, reverse, shl, shr_u, shr_s, eq, ne, lt_s, lt_u, ge_s, ge_u, gt_s, gt_u, le_s, le_u, or_reduce, and_reduce, xor_reduce, concat, extract, mux, sext, zext, reg, mem, instance, output.
The pure-Python interpreter supports the complete JSON-IR, including registers, memories, multiple clock domains, and hierarchical instances. Inputs retain their previous values when omitted; registers and uninitialized memories start at zero. A register or memory updates only when its clock input changes from 0 to 1.
from cppl import Interpreter
sim = Interpreter.from_json(
open("examples/json/register.json").read(), top="BasicReg"
)
print(sim.evaluate({"clk": 0, "d": 42})) # {"q": 0}
print(sim.evaluate({"clk": 1})) # {"q": 42}
print(sim.peek("q_reg")) # inspect an internal SSA valueFor DSL designs, use design.interpreter(top="ModuleName") after adding the
modules. peek_memory("instance.path.memory_name") returns an immutable memory
snapshot, and reset_state() restores initial register and memory contents.
The mem op creates register-array memories with configurable read/write ports:
{
"id": ["rdata"],
"op": "mem",
"width": 32,
"depth": 1024,
"clock": "clk",
"reset": "rst",
"reads": [{"addr": "raddr", "enable": "ren"}],
"writes": [
{"addr": "waddr", "data": "wdata", "enable": "wen", "mask": "wmask"}
]
}idhas one entry per read port; each output iswidthbits widereads: combinational read ports (latency 0);writes: synchronous write ports (latency 1)maskis optional andwidthbits wide;1updates the corresponding stored bit fromdata, while0preserves the old stored bit- Generates CIRCT
seq.hlmemops, lowered tosv.regarrays in Verilog
- LangChain — chat models and prompt pipelines
- LangGraph — agent workflow orchestration
- pycde — Python CIRCT bindings for MLIR/Verilog emission
- An LLM API key configured for your chosen provider
Create a root .env file for your provider credentials and model selection.
CircuitPPL reads LangChain model configuration from .env.
Recommended generic variables:
LLM_PROVIDER=openai
LLM_MODEL=openai/gpt-4.1
LLM_API_KEY=your_api_key
LLM_BASE_URL=https://api.openai.com/v1You can also use provider-specific variables when you prefer:
LLM_PROVIDER=deepseek
DEEPSEEK_MODEL=deepseek/deepseek-chat
DEEPSEEK_API_KEY=your_api_key
DEEPSEEK_BASE_URL=https://api.deepseek.comNotes:
LLM_PROVIDER,LLM_MODEL,LLM_API_KEY, andLLM_BASE_URLare the primary, provider-agnostic settings.- If
LLM_PROVIDERis set, CircuitPPL also checks${PROVIDER}_MODEL,${PROVIDER}_API_KEY, and${PROVIDER}_BASE_URL. - Existing fallbacks like
ANTHROPIC_API_KEY,OPENAI_API_KEY,ANTHROPIC_BASE_URL, andOPENAI_BASE_URLstill work. LLM_MODELis required for DSL compilation.anthropic/*models uselangchain-anthropic; OpenAI and OpenAI-compatible endpoints uselangchain-openai. Non-OpenAI compatible providers must set a base URL.- Optional generation overrides are available via
LLM_TEMPERATURE,LLM_TOP_P, andLLM_REASONING_EFFORT. By default CircuitPPL does not forcetemperature, which avoids compatibility issues with models such asgpt-5.
If you use CPPL in your research, please cite:
@misc{yin2026cpplcircuitpromptprogramming,
title={CPPL: A Circuit Prompt Programming Language},
author={Shuo Yin and Yihe Wang and Lancheng Zou and Xufeng Yao and Tinghuan Chen and Chen Bai and Zhengrong Wang and Tsung-Yi Ho and Bei Yu},
year={2026},
eprint={2605.17892},
archivePrefix={arXiv},
primaryClass={cs.AR},
url={https://arxiv.org/abs/2605.17892},
}cppl/
├── frontend/ # DSL types, @module decorator, LLM compiler, prompts
├── ir/ # JSON-IR models, parser, validator, width inference
├── codegen/ # MLIR/Verilog generation via CIRCT
examples/
├── json/ # JSON-IR examples
└── python/ # Python DSL examples
pyproject.toml # Package configuration