Skip to content

Commit

Permalink
Feature: Formal Verification - Assertion Cells (#49)
Browse files Browse the repository at this point in the history
* Assertion Cells
* AssertionCell can only be used in Formal verification
* Testcases for Assertion Cells

* Update `magia-flow` dependency version 

CI:
* Switching image version
* Install OSS-CAD for formal verification
* Check Verilator version in Poetry Env
  • Loading branch information
khwong-c authored May 13, 2024
1 parent 368424f commit b7debcd
Show file tree
Hide file tree
Showing 8 changed files with 577 additions and 133 deletions.
24 changes: 11 additions & 13 deletions .github/workflows/ci-general.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,28 @@ jobs:
python-version: [ "3.10", "3.11", "3.12" ]

runs-on: ubuntu-latest
container:
image: verilator/verilator:v5.018

steps:
- name: Checkout Source Code
uses: actions/checkout@v4

# Installation of Binary Dependencies
- name: Install Poetry
run: |
apt-get update; apt-get install -y curl sudo
curl -sSL https://install.python-poetry.org | python3
ln -sf "${HOME}/.local/bin/poetry" /usr/bin/poetry
- name: Install poetry
run: pipx install poetry

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'

- name: Install dependencies
run: |
poetry install --no-interaction --with=dev
- name: Install OSS-CAD
run: |
poetry run python -m magia_flow install oss-cad --verilator
# Verify version of installed binaries
- name: List Poetry Environment
run: |
Expand All @@ -57,18 +59,14 @@ jobs:
poetry env info
- name: Print Verilator version
run: |
verilator --version
poetry run verilator --version
- name: Python Version
run: |
python --version
- name: Python with Poetry Version
run: |
poetry run python --version
- name: Install dependencies
run: |
poetry install --no-interaction --with=dev
- name: Run Pytest
env:
pytest_github_report: true
Expand Down
115 changes: 115 additions & 0 deletions magia/assertions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Assertion cells used by formal verification tools."""
from contextlib import contextmanager
from dataclasses import dataclass
from enum import Enum
from string import Template

from .signals import CodeSectionType, Signal, Synthesizable
from .utils import ModuleContext


class AssertionType(Enum):
ASSERT = "assert"
ASSUME = "assume"
COVER = "cover"


ASSERTION_TEMPLATE = Template(
"// $name_and_desc\n"
"always @(posedge $clk) begin\n"
" $name: $type ($cond);\n"
"end"
)
ASSERTION_EN_TEMPLATE = Template(
"// $name_and_desc\n"
"always @(posedge $clk) begin\n"
" if($en) begin\n"
" $name: $type ($cond);\n"
" end\n"
"end"
)


@dataclass
class AssertionCellConfig:
name: str
cond: Signal
clk: Signal
assertion_type: AssertionType
en: None | Signal = None
desc: None | str = None


class AssertionCell(Synthesizable):
"""Assertion Cells for Formal Verification."""

assertion_counter = 0

def __init__(
self,
name: None | str,
cond: Signal,
clk: Signal,
en: None | Signal = None,
desc: None | str = None,
assertion_type: AssertionType | str = AssertionType.ASSERT,
**kwargs,
):
if self.current_code_section != CodeSectionType.FORMAL:
raise RuntimeError("AssertionCell can only be used inside a Formal code section.")
super().__init__(**kwargs)
if cond.width != 1:
raise ValueError("Assertion condition must be 1-bit wide")
if clk.width != 1:
raise ValueError("Assertion clock must be 1-bit wide")
if en is not None and en.width != 1:
raise ValueError("Assertion enable must be 1-bit wide")
if isinstance(assertion_type, str):
assertion_type = AssertionType(assertion_type)
if name is None:
name = f"{assertion_type.value}_autogen_{AssertionCell.assertion_counter}"
AssertionCell.assertion_counter += 1

self.config = AssertionCellConfig(
name=name,
cond=cond,
clk=clk,
en=en,
desc=desc,
assertion_type=assertion_type,
)
if (module_context := ModuleContext().current) is not None:
if self.name in module_context.assertions_collected:
raise ValueError(f"Assertion name '{self.name}' already exists")
module_context.assertions_collected[self.name] = self

@property
def name(self) -> str:
"""Return the name of the assertion."""
return self.config.name

@property
def drivers(self) -> list[Signal]:
"""Return the driving signals of the assertion."""
if self.config.en is not None:
return [self.config.clk, self.config.cond, self.config.en]
return [self.config.clk, self.config.cond]

@classmethod
@contextmanager
def code_section(cls):
with Synthesizable.code_section(CodeSectionType.FORMAL):
yield

def elaborate(self) -> str:
"""Elaborate the assertion into SystemVerilog code."""
template = ASSERTION_EN_TEMPLATE if self.config.en is not None else ASSERTION_TEMPLATE

return template.substitute(
name=self.name,
name_and_desc=f"{self.name}: {self.config.desc}" if self.config.desc is not None else self.name,
type=self.config.assertion_type.value,
cond=self.config.cond.name,
clk=self.config.clk.name,
en=self.config.en.name if self.config.en is not None else "",
)
46 changes: 36 additions & 10 deletions magia/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import logging
from collections import Counter, OrderedDict
from dataclasses import dataclass
from functools import cached_property, partial
from functools import cached_property
from itertools import count
from os import PathLike
from string import Template
from typing import TYPE_CHECKING

from .assertions import AssertionCell
from .data_struct import SignalDict
from .io_ports import IOPorts
from .io_signal import Input, Output
Expand Down Expand Up @@ -39,6 +40,11 @@ class ModuleInstanceConfig:
MOD_DECL_TEMPLATE = Template("module $name (\n$io\n);")
INST_TEMPLATE = Template("$module_name $inst_name (\n$io\n);")
IO_TEMPLATE = Template(".$port_name($signal_name)")
FORMAL_SECTION_TEMPLATE = Template(
"`ifdef FORMAL\n"
"$code\n"
"`endif"
)


class _ModuleMetaClass(type):
Expand Down Expand Up @@ -79,8 +85,6 @@ def implement(self):
_new_module_counter = count(0)
output_file: None | PathLike = None

formal_code = partial(Synthesizable.code_section, CodeSectionType.FORMAL)

def __init__(self, name: None | str = None, **kwargs):
super().__init__(**kwargs)
ModuleContext().push(self) # Push current module to the context stack
Expand All @@ -106,6 +110,7 @@ def __init__(self, name: None | str = None, **kwargs):
)
self.io = IOPorts()
self.manual_sva_collected = []
self.assertions_collected = {}

def validate(self) -> list[Exception]:
undriven_outputs = [
Expand Down Expand Up @@ -145,8 +150,18 @@ def elaborate(self) -> tuple[str, set[Module]]:

trace_from = self.io.outputs
trace_from += self.manual_sva_collected
trace_from += list(self.assertions_collected.values())
synth_objs, insts = self.trace(trace_from)

formal_objs = [
obj for obj in synth_objs
if obj._code_section == CodeSectionType.FORMAL
]
synth_objs = [
obj for obj in synth_objs
if obj._code_section != CodeSectionType.FORMAL
]

signal_decl = [
signal.signal_decl()
for signal in synth_objs
Expand All @@ -173,6 +188,22 @@ def elaborate(self) -> tuple[str, set[Module]]:
for output in self.io.outputs
)

formal_signal_decl = [
signal.signal_decl()
for signal in formal_objs
if isinstance(signal, Signal) and not (signal.is_input or signal.is_output)
]
formal_signal_decl = "\n".join(formal_signal_decl)
formal_impl = [
obj.elaborate()
for obj in formal_objs
]
formal_impl = "\n".join(formal_impl)

formal_section = FORMAL_SECTION_TEMPLATE.substitute(
code=f"{formal_signal_decl}\n{formal_impl}"
)

extra_code = self.post_elaborate()

mod_end = "endmodule"
Expand All @@ -181,6 +212,7 @@ def elaborate(self) -> tuple[str, set[Module]]:
mod_decl,
signal_decl,
mod_impl,
formal_section,
mod_output_assignment,
extra_code,
mod_end,
Expand Down Expand Up @@ -267,13 +299,7 @@ def trace(trace_from: list[Synthesizable]) -> tuple[list[Synthesizable], list[In
if (id_sig := id(sig)) not in traced_obj_id
}

case Signal():
next_trace |= {
id_sig: sig for sig in obj.drivers
if (id_sig := id(sig)) not in traced_obj_id
}

case SVAManual():
case Signal() | SVAManual() | AssertionCell():
next_trace |= {
id_sig: sig for sig in obj.drivers
if (id_sig := id(sig)) not in traced_obj_id
Expand Down
3 changes: 2 additions & 1 deletion magia/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,8 @@ def signal_decl(self) -> str:
case CodeSectionType.VERILOG:
template = SIGNAL_DECL_VERILOG_TEMPLATE
case CodeSectionType.FORMAL:
template = SIGNAL_DECL_FORMAL_TEMPLATE
template = SIGNAL_DECL_FORMAL_TEMPLATE \
if self.signal_config.op_type == OPType.REG else SIGNAL_DECL_TEMPLATE
case CodeSectionType.SVA_MANUAL:
template = SIGNAL_DECL_TEMPLATE

Expand Down
Loading

0 comments on commit b7debcd

Please sign in to comment.