Zuspec IR Checker¶
Overview¶
The Zuspec IR Checker is an extensible validation framework that checks your Zuspec code by analyzing the Intermediate Representation (IR). Unlike traditional type checkers that work at the AST level, the IR checker validates the semantic model of your code, ensuring it conforms to profile-specific rules.
Key Features:
IR-based validation - Checks compiled IR, not raw Python AST
Profile-aware - Different rules for different abstraction levels
Extensible - Plugins can register custom checkers via entrypoints
Flake8 integration - Works seamlessly with existing tools
VSCode support - Real-time error highlighting in the editor
Architecture¶
The checker uses a three-layer architecture:
Flake8 Plugin (
zuspec_flake8) - Entry point that hooks into flake8Core Checker (
IRChecker) - Builds IR and dispatches to profile checkersProfile Checkers - Validate IR nodes according to profile rules
Flake8 → ZuspecFlake8Plugin → IRChecker → Profile Checker
↓
IR Model
↓
Source Locations
Installation and Setup¶
Basic Installation¶
The checker is included with zuspec-dataclasses:
pip install zuspec-dataclasses
# or
uv pip install zuspec-dataclasses
The flake8 plugin is automatically registered via entrypoints.
Configuration¶
Configure in your pyproject.toml:
[tool.flake8]
max-line-length = 100
extend-ignore = ["E203", "W503"]
[tool.zuspec]
# Package roots for IR compilation
package_roots = [
"src",
"tests"
]
Configuration Options:
package_roots- List of directories containing your Zuspec packages. The checker needs to know where to find modules for import resolution during IR compilation.
VSCode Integration¶
Install the Python extension (if not already installed)
Install flake8 in your project environment:
uv pip install flake8
Configure VSCode settings in
.vscode/settings.json:{ "python.linting.enabled": true, "python.linting.flake8Enabled": true, "python.linting.flake8Path": "packages/python/bin/flake8", "python.linting.lintOnSave": true }
Reload VSCode window: Press
Ctrl+Shift+Pand select “Developer: Reload Window”
After setup, Zuspec errors will appear with red squiggles in the editor:
Error Codes¶
The checker produces errors with codes ZDC001 through ZDC006:
ZDC001: Width Annotation Required¶
Integer fields must have explicit width annotations in retargetable code.
Bad:
@zdc.dataclass
class Counter(zdc.Component):
count: int = zdc.field() # Error: infinite-width int
Good:
@zdc.dataclass
class Counter(zdc.Component):
count: zdc.uint32_t = zdc.field() # OK: explicit width
ZDC002: Non-Zuspec Type¶
Fields must use Zuspec types (Component, Struct, width-annotated integers).
Bad:
@zdc.dataclass
class MyComponent(zdc.Component):
data: object = zdc.field() # Error: not a Zuspec type
Good:
@zdc.dataclass
class MyComponent(zdc.Component):
data: zdc.uint8_t = zdc.field() # OK: Zuspec type
ZDC003: Unannotated Variable¶
Variables in process/sync/comb methods must have type annotations.
Bad:
@zdc.sync(clock=lambda s: s.clock)
def process(self):
x = 5 # Error: no type annotation
Good:
@zdc.sync(clock=lambda s: s.clock)
def process(self):
x: zdc.uint8_t = 5 # OK: annotated
ZDC004: Type Annotation Error¶
Type annotations must be valid and resolvable.
Bad:
@zdc.dataclass
class MyComponent(zdc.Component):
data: NonexistentType = zdc.field() # Error: undefined type
Good:
@zdc.dataclass
class MyComponent(zdc.Component):
data: zdc.uint16_t = zdc.field() # OK: valid type
ZDC005: Non-Zuspec Constructor Call¶
Constructors in retargetable code must be for Zuspec types.
Bad:
def build(self):
obj = object() # Error: Python object() not allowed
Good:
def build(self):
obj = MyComponent() # OK: Zuspec Component
ZDC006: Forbidden Function Call¶
Some Python functions are not allowed in retargetable code.
Bad:
def process(self):
if hasattr(self, 'optional'): # Error: dynamic introspection
pass
Good:
def process(self):
if self.enabled: # OK: static attribute access
pass
Built-in Profiles¶
PythonProfile¶
The most permissive profile, allows all Python constructs.
Use when:
Writing pure Python implementations
Prototyping before targeting hardware
Testing with maximum flexibility
Allows:
Infinite-width integers (
int)Dynamic operations (
hasattr,getattr,setattr)Unannotated variables
Anyandobjecttypes
Example:
@zdc.dataclass(profile=profiles.PythonProfile)
class FlexibleModel:
count: int = zdc.field(default=0) # OK in Python profile
def process(self):
x = 5 # Unannotated OK
if hasattr(self, 'optional'): # Dynamic access OK
return getattr(self, 'optional')
RetargetableProfile¶
The default profile for hardware-targetable code.
Use when:
Writing code for synthesis or compilation
Targeting multiple backends (Verilog, C++, etc.)
Need strong type safety
Requires:
Width-annotated integer types
Concrete types (no
Anyorobject)Type annotations on all variables
Static attribute access
Example:
@zdc.dataclass # Uses RetargetableProfile by default
class Counter(zdc.Component):
count: zdc.uint32_t = zdc.output()
@zdc.sync(clock=lambda s: s.clock)
def increment(self):
next_val: zdc.uint32_t = self.count + 1 # Annotation required
self.count = next_val
Profile Auto-Detection¶
The checker automatically detects which profile applies to each class by examining its type hierarchy:
@zdc.dataclass
class MyComponent(zdc.Component): # Auto-detected as Retargetable
data: zdc.uint8_t = zdc.field()
@zdc.dataclass(profile=profiles.PythonProfile)
class MyPythonClass: # Explicitly Python profile
data: int = zdc.field()
Creating Custom Checkers¶
You can extend the checker system with custom profile checkers.
Step 1: Create Profile and Checker Classes¶
# my_checker_plugin/profiles.py
from zuspec.dataclasses.profiles import Profile
from zuspec.dataclasses.ir_checker import ProfileChecker
from zuspec.dataclasses.ir.base import Field, DataTypeInt
class MyCustomProfile(Profile):
"""Custom profile with specific rules."""
pass
class MyCustomChecker(ProfileChecker):
"""Checker that validates MyCustomProfile rules."""
def check_field(self, field: Field, context) -> list:
"""Validate field types."""
errors = []
# Example: Only allow 32-bit integers
if isinstance(field.datatype, DataTypeInt):
if field.datatype.bits != 32:
errors.append({
'line': field.loc.line if field.loc else 1,
'col': field.loc.pos if field.loc else 0,
'code': 'ZDC101',
'message': f"Field '{field.name}' must be 32-bit"
})
return errors
Step 2: Register via Entrypoint¶
In your plugin’s pyproject.toml:
[project.entry-points."zuspec.checkers"]
my_custom = "my_checker_plugin:MyCustomProfile:MyCustomChecker"
The format is: name = "module:ProfileClass:CheckerClass"
Step 3: Install and Use¶
pip install my-checker-plugin
The checker is now automatically available:
from my_checker_plugin.profiles import MyCustomProfile
@zdc.dataclass(profile=MyCustomProfile)
class MyComponent(zdc.Component):
value: zdc.uint32_t = zdc.field() # OK
# bad: zdc.uint16_t = zdc.field() # Would fail with ZDC101
Checker Extension API¶
ProfileChecker Base Class¶
All custom checkers should inherit from ProfileChecker:
from zuspec.dataclasses.ir_checker import ProfileChecker
from zuspec.dataclasses.ir.base import (
Component, Field, Function, Statement
)
class MyChecker(ProfileChecker):
def check_component(self, component: Component) -> list:
"""Check a component definition."""
return [] # Return list of error dicts
def check_field(self, field: Field, context) -> list:
"""Check a field definition."""
return []
def check_function(self, function: Function, context) -> list:
"""Check a function/method definition."""
return []
def check_statement(self, stmt: Statement, context) -> list:
"""Check a statement in a function body."""
return []
Error Format¶
Errors should be dictionaries with these keys:
{
'line': int, # Source line number (1-indexed)
'col': int, # Column offset (0-indexed)
'code': str, # Error code (e.g., 'ZDC001')
'message': str # Human-readable description
}
Reusing Core Infrastructure¶
Custom checkers can leverage core utilities:
from zuspec.dataclasses.ir_checker import (
is_zuspec_type, # Check if type is Zuspec-compatible
get_type_width, # Extract width from annotated int
resolve_type, # Resolve type references
)
class MyChecker(ProfileChecker):
def check_field(self, field: Field, context) -> list:
if not is_zuspec_type(field.datatype):
return [{
'line': field.loc.line if field.loc else 1,
'col': field.loc.pos if field.loc else 0,
'code': 'ZDC102',
'message': f"Field '{field.name}' uses non-Zuspec type"
}]
return []
Command-Line Usage¶
Run the checker from the command line:
# Check a single file
flake8 src/my_module.py
# Check entire project
flake8 src/
# Show only Zuspec errors
flake8 src/ | grep ZDC
# Verbose output with line numbers
flake8 --format='%(path)s:%(row)d:%(col)d: %(code)s %(text)s' src/
Integration with CI/CD¶
Add to your CI pipeline:
# .github/workflows/lint.yml
- name: Check Zuspec code
run: |
pip install flake8 zuspec-dataclasses
flake8 src/ --select=ZDC
Troubleshooting¶
Errors Not Showing in VSCode¶
Check flake8 is enabled:
{ "python.linting.flake8Enabled": true }
Verify flake8 path points to environment with zuspec-dataclasses:
which flake8 flake8 --version
Reload VSCode window: Ctrl+Shift+P → “Developer: Reload Window”
Check Problems panel (Ctrl+Shift+M) for errors
Test from command line:
flake8 path/to/file.py
Errors at Line 1:1¶
If all errors show at line 1, column 1, the IR is missing source locations. This was fixed in version 2026.1+. Upgrade:
pip install --upgrade zuspec-dataclasses
“Module not found” Errors¶
The checker needs to import your code to build IR. Ensure:
Configure package_roots in
pyproject.toml:[tool.zuspec] package_roots = ["src"]
Install package in development mode:
pip install -e .
Check Python path includes your source directories
Slow Performance¶
IR compilation can be slow for large projects. To optimize:
Use .flake8cache to cache results
Run on changed files only in CI
Exclude unnecessary directories:
[tool.flake8] exclude = [".git", "__pycache__", "build", "dist"]
Comparison with Other Tools¶
vs MyPy¶
Feature |
MyPy Plugin |
IR Checker |
|---|---|---|
Analysis Level |
Python AST |
Zuspec IR |
Type Checking |
Yes (static) |
Yes (semantic) |
Profile Awareness |
Yes |
Yes |
Flake8 Integration |
No |
Yes |
VSCode Integration |
Via MyPy extension |
Via Flake8 |
Extensibility |
MyPy plugin API |
Entrypoints |
Error Location |
Excellent |
Excellent (2026.1+) |
Note: The mypy plugin is deprecated in favor of the IR checker.
vs Flake8 Style Checkers¶
Traditional flake8 plugins check code style (formatting, naming, imports). The Zuspec IR checker validates semantic correctness - whether your code can be compiled to the target platform.
vs PyLint¶
PyLint focuses on Python best practices and bug detection. The Zuspec IR checker enforces hardware synthesis constraints - ensuring code is retargetable to Verilog, VHDL, C++, etc.
Frequently Asked Questions¶
Q: Do I need to configure anything special for the checker to work?
A: Only if you have complex import structures. Add package_roots to your pyproject.toml pointing to your source directories.
Q: Can I disable specific error codes?
A: Yes, use flake8’s standard ignore mechanism:
flake8 --extend-ignore=ZDC001,ZDC003 src/
Or in pyproject.toml:
[tool.flake8]
extend-ignore = ["ZDC001", "ZDC003"]
Q: How do I create a checker for my own profile?
A: Create a ProfileChecker subclass and register it via entrypoints. See Creating Custom Checkers.
Q: Can I use this with pre-commit hooks?
A: Yes! Add to .pre-commit-config.yaml:
repos:
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies: [zuspec-dataclasses]
Q: Does the checker work with Python 3.8?
A: The checker requires Python 3.9+ for full AST support.
Q: Can I use the checker in Jupyter notebooks?
A: Not directly. Extract your Zuspec code to .py files for checking.
See Also¶
profiles - Profile system overview
Core Types - Zuspec type system
Components - Component and structure definitions
Profile Checker Design - Implementation details (historical, mypy-based)
Note
The mypy plugin is deprecated as of version 2026.1. Use the flake8-based IR checker instead.