diff --git a/.flake8 b/.flake8 deleted file mode 100644 index eae8f30..0000000 --- a/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -ignore = H101,H301,H304,H306,E221,E231,E241,E265,F403,F405,W503,W504 -max-line-length = 120 \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a110cad --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: Test Suite + +on: + push: + branches: [main, devel] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install iverilog + run: sudo apt-get install -y iverilog + + - name: Install dependencies + run: pip install -e . + + - name: Run test suite (HDL + Python sim) + run: timeout 300 python suite.py -f -j -P -n 4 diff --git a/.gitignore b/.gitignore index 2c47c26..70b3c75 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,33 @@ +# Hidden files and directories .gitignore .*/ .* -__pycache__ +!.github/ + +# Debugging debug* + +# Legacy output directory out/ + +# Suite ignore list .suite_ignores + +# Virtual environment (uv) +.venv/ + +# Python bytecode +__pycache__/ +*.pyc +*.pyo + +# Polyphony compiler output +polyphony_out/ + +# Python packaging +*.egg-info/ +dist/ +build/ + +# Testing +.pytest_cache/ diff --git a/.suite_ignores b/.suite_ignores index d0ad1cc..55bc9bc 100644 --- a/.suite_ignores +++ b/.suite_ignores @@ -1,3 +1,4 @@ +# IO: unimplemented features io/connect01.py io/connect02.py io/connect03.py @@ -8,3 +9,39 @@ io/flipped03.py io/flipped04.py io/thru01.py io/thru02.py +# timed: compile error +timed/fifo01.py +# module: compile hangs (also in v0.4.0) +module/module04.py +module/module05.py +module/module06.py +# error tests: wrong or missing error messages +error/callable.py +error/field_reference.py +error/global01.py +error/global03.py +error/io_conflict01.py +error/io_conflict02.py +error/io_pipeline_read_conflict01.py +error/io_pipeline_write_conflict01.py +error/io_write_conflict01.py +error/io_write_conflict02.py +error/io_write_conflict03.py +error/is_not_subscriptable02.py +error/is_not_subscriptable03.py +error/loop_var01.py +error/module_args01.py +error/must_be_x_type05.py +error/must_be_x_type06.py +error/reserved_port_name.py +error/return_type01.py +error/return_type02.py +error/sub.py +error/seq_capacity01.py +error/seq_capacity02.py +error/toomany_args01.py +error/toomany_args02.py +# warning tests: wrong or missing warnings +warning/pipeline_hazard01.py +warning/port_is_not_used01.py +warning/port_is_not_used02.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d740f64..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: python -sudo: false -python: - - "3.6" - - "nightly" -addons: - apt: - packages: - - iverilog -install: - - pip install -r requirements.txt -script: - - python suite.py -j -f diff --git a/README.md b/README.md new file mode 100644 index 0000000..162c04e --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +[![Test Suite](https://github.com/ktok07b6/polyphony/actions/workflows/test.yml/badge.svg?branch=devel)](https://github.com/ktok07b6/polyphony/actions/workflows/test.yml) +[![PyPI](https://badge.fury.io/py/polyphony.svg)](https://badge.fury.io/py/polyphony) +[![Python](https://img.shields.io/badge/python-%3E%3D3.12-blue)](https://github.com/ktok07b6/polyphony) +[![License](https://img.shields.io/github/license/ktok07b6/polyphony)](https://github.com/ktok07b6/polyphony/blob/main/LICENSE) + +# polyphony + +Polyphony is a Python-based High-Level Synthesis (HLS) compiler that generates synthesizable Verilog HDL from a Python subset. + +## Requirements + +- Python >= 3.12 +- Icarus Verilog (for HDL simulation) + +## Installation + +```bash +pip install polyphony +``` + +## Usage + +``` +polyphony [-h] [-o FILE] [-d DIR] [-c CONFIG] [-v] [-D] [-hd] [-q] + [-vd] [-vm] [-op PREFIX] [-t TARGETS [TARGETS ...]] [-V] + source +``` + +| Option | Description | +|--------|-------------| +| `-o FILE, --output FILE` | output filename (default is "polyphony_out") | +| `-d DIR, --dir DIR` | output directory | +| `-c CONFIG, --config CONFIG` | set configuration (JSON literal or file) | +| `-v, --verbose` | verbose output | +| `-D, --debug` | enable debug mode | +| `-hd, --hdl_debug` | enable HDL debug mode | +| `-q, --quiet` | suppress warning/error messages | +| `-vd, --verilog_dump` | output VCD file in testbench | +| `-vm, --verilog_monitor` | enable $monitor in testbench | +| `-V, --version` | print the Polyphony version number | + +## Examples + +See [tests](https://github.com/ktok07b6/polyphony/tree/main/tests) + +## License + +MIT License. See [LICENSE](LICENSE) for details. diff --git a/README.rst b/README.rst deleted file mode 100644 index 2ed589b..0000000 --- a/README.rst +++ /dev/null @@ -1,46 +0,0 @@ -.. image:: https://travis-ci.org/ktok07b6/polyphony.svg?branch=devel - :target: https://travis-ci.org/ktok07b6/polyphony -.. image:: https://badge.fury.io/py/polyphony.svg - :target: https://badge.fury.io/py/polyphony - -polyphony -========= -Polyphony is Python based High-Level Synthesis compiler. - -Requirements ------------- -Python 3.6 or later - -Installation ------------- -$ pip3 install polyphony - -Usage ------ -usage: polyphony [-h] [-o FILE] [-d DIR] [-c CONFIG] [-v] [-D] [-q] [-vd] - [-vm] [-V] - source - -positional arguments: - source Python source file - -optional arguments: - -h, --help show this help message and exit - -o FILE, --output FILE - output filename (default is "polyphony_out") - -d DIR, --dir DIR output directory - -c CONFIG, --config CONFIG - set configration(json literal or file) - -v, --verbose verbose output - -D, --debug enable debug mode - -q, --quiet suppress warning/error messages - -vd, --verilog_dump output vcd file in testbench - -vm, --verilog_monitor - enable $monitor in testbench - -V, --version print the Polyphony version number - -Examples --------- - -see https://github.com/ktok07b6/polyphony/tree/master/tests - diff --git a/error.py b/error.py index 3048acc..6584588 100644 --- a/error.py +++ b/error.py @@ -47,6 +47,8 @@ def make_compile_options(casename, casefile_path, err_options, quiet_level): options.debug_mode = err_options.debug_mode options.verilog_dump = False options.verilog_monitor = False + options.hdl_debug_mode = False + options.targets = [] return options diff --git a/polyphony/__init__.py b/polyphony/__init__.py index c60c4ff..e9711e7 100644 --- a/polyphony/__init__.py +++ b/polyphony/__init__.py @@ -1,4 +1,8 @@ import inspect +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from typing import Any + from . import version __version__ = version.__version__ @@ -71,6 +75,9 @@ def init_wrapper(self, *args, **kwargs): cls._is_module = True return cls +if TYPE_CHECKING: + module: Any # type: ignore[no-redef] + ''' A decorator to mark a testbench function. @@ -174,12 +181,17 @@ def __call__(self, **kwargs): def pipelined(seq, ii=-1): - pass + return seq def unroll(seq, factor='full'): + return seq + + +def append_worker(fn, *args, loop=False): pass + #class Reg: # pass @@ -187,25 +199,35 @@ def unroll(seq, factor='full'): # pass -@module -class Channel: - def __init__(self, dtype:type, capacity=4): - pass +if TYPE_CHECKING: + class Channel: + def __init__(self, dtype: type, capacity: int = 4) -> None: ... + def put(self, v: Any) -> None: ... + def get(self) -> Any: ... + def full(self) -> bool: ... + def empty(self) -> bool: ... + def will_full(self) -> bool: ... + def will_empty(self) -> bool: ... +else: + @module + class Channel: + def __init__(self, dtype:type, capacity=4): + pass - def put(self, v): - pass + def put(self, v): + pass - def get(self): - pass + def get(self): + pass - def full(self): - pass + def full(self): + pass - def empty(self): - pass + def empty(self): + pass - def will_full(self): - pass + def will_full(self): + pass - def will_empty(self): - pass + def will_empty(self): + pass diff --git a/polyphony/_internal/__init__.py b/polyphony/_internal/__init__.py index d98b0d7..53edb90 100644 --- a/polyphony/_internal/__init__.py +++ b/polyphony/_internal/__init__.py @@ -1,4 +1,4 @@ -__version__:str = '0.3.6' +__version__:str = '0.4.1' __python__:bool = False diff --git a/polyphony/_internal/_simulator.py b/polyphony/_internal/_simulator.py new file mode 100644 index 0000000..109a798 --- /dev/null +++ b/polyphony/_internal/_simulator.py @@ -0,0 +1,2 @@ +def watch(*signals, vcd=None, log=None) -> None: + pass diff --git a/polyphony/compiler/__main__.py b/polyphony/compiler/__main__.py index d6d4273..c05c5eb 100644 --- a/polyphony/compiler/__main__.py +++ b/polyphony/compiler/__main__.py @@ -890,6 +890,7 @@ def setup_options(options): env.quiet_level = options.quiet_level if options.quiet_level else 0 env.enable_verilog_dump = options.verilog_dump env.enable_verilog_monitor = options.verilog_monitor + env.watch_signals = getattr(options, 'watch_signals', '') env.targets = options.targets if options.config: try: diff --git a/polyphony/compiler/ahdl/hdlgen.py b/polyphony/compiler/ahdl/hdlgen.py index 81833c5..aee6478 100644 --- a/polyphony/compiler/ahdl/hdlgen.py +++ b/polyphony/compiler/ahdl/hdlgen.py @@ -220,6 +220,7 @@ def _build_module(self): if stm.dst.is_a(AHDL_VAR) and stm.dst.sig.is_net(): assign = AHDL_ASSIGN(stm.dst, stm.src) self.hdlmodule.add_static_assignment(assign, '') + self.hdlmodule.remove_sig(fsm.state_var) del self.hdlmodule.fsms[fsm.name] else: self._process_fsm(fsm) diff --git a/polyphony/compiler/ahdl/hdlmodule.py b/polyphony/compiler/ahdl/hdlmodule.py index c25561f..ce50660 100644 --- a/polyphony/compiler/ahdl/hdlmodule.py +++ b/polyphony/compiler/ahdl/hdlmodule.py @@ -56,46 +56,129 @@ def is_hdlmodule_scope(cls, scope): or scope.is_testbench()) def __str__(self): - s = '---------------------------------\n' - s += 'HDLModule {}\n'.format(self.name) + s = '=' * 60 + '\n' + s += f'HDLModule: {self.name}\n' + s += '=' * 60 + '\n' + + # Signals section s += self.str_signals() s += '\n' + + # I/O ports section s += self.str_ios() s += '\n' - s += '-- sub modules --\n' - for name, hdlmodule, connections, param_map in self.sub_modules.values(): - s += '{} \n'.format(name) - for sig, acc in connections: - s += ' connection : .{}({}) \n'.format(sig.name, acc.name) - s += '-- declarations --\n' - for decl in self.decls: - s += ' {}\n'.format(decl) - s += '\n' + + # Parameters section + if self.parameters: + s += '-- PARAMETERS --\n' + for sig, value in self.parameters.items(): + s += f' {sig.name} = {value}\n' + s += '\n' + + # Constants section + if self.constants: + s += '-- CONSTANTS --\n' + for sig, value in self.constants.items(): + s += f' {sig.name} = {value}\n' + s += '\n' + + # Sub modules section + if self.sub_modules: + s += '-- SUB MODULES --\n' + for name, hdlmodule, connections, param_map in self.sub_modules.values(): + s += f' Module: {name}\n' + if connections: + s += ' Connections:\n' + for sig, acc in connections: + s += f' .{sig.name}({acc.name})\n' + if param_map: + s += ' Parameters:\n' + for param, value in param_map.items(): + s += f' {param} = {value}\n' + s += '\n' + + # Declarations section + if self.decls: + s += '-- DECLARATIONS --\n' + for decl in self.decls: + s += f' {decl}\n' + s += '\n' + + # FSM section if self.fsms: - s += '-- fsm --\n' + s += '-- FINITE STATE MACHINES --\n' for name, fsm in self.fsms.items(): - s += '---------------------------------\n' - s += f'{name}\n' - s += '---------------------------------\n' - s += 'reset:\n' - for stm in fsm.reset_stms: - s += f'{stm}\n' - for stg in fsm.stgs: - for state in stg.states: - s += str(state) + s += f' FSM: {name}\n' + s += f' State Variable: {fsm.state_var.name}\n' + + if fsm.reset_stms: + s += ' Reset Statements:\n' + for stm in fsm.reset_stms: + s += f' {stm}\n' + + if fsm.outputs: + s += ' Outputs:\n' + for output in fsm.outputs: + s += f' {output.name}\n' + + if fsm.stgs: + s += ' State Transition Graphs:\n' + for i, stg in enumerate(fsm.stgs): + s += f' STG {i}: {len(stg.states)} states\n' + for state in stg.states: + state_str = str(state) + state_lines = state_str.split('\n') + for line in state_lines: + if line.strip(): + s += f' {line}\n' + else: + s += '\n' + s += '\n' + + # Tasks section if self.tasks: - s += '-- tasks --\n' + s += '-- TASKS --\n' for task in self.tasks: - s += f'{task}\n' - s += '\n' + s += f' {task}\n' + s += '\n' + + # Edge detectors section + if self.edge_detectors: + s += '-- EDGE DETECTORS --\n' + for var, old, new in self.edge_detectors: + s += f' {var.name}: {old} -> {new}\n' + s += '\n' + + # Resource summary + num_regs, num_nets, num_states = self.resources() + s += '-- RESOURCE SUMMARY --\n' + s += f' Registers: {num_regs} bits\n' + s += f' Nets: {num_nets} bits\n' + s += f' States: {num_states}\n' + + s += '=' * 60 + '\n' return s def str_ios(self): - s = '-- I/O ports --\n' - for var in self.inputs(): - s += f'{var.name} {var.sig}\n' - for var in self.outputs(): - s += f'{var.name} {var.sig}\n' + s = '-- I/O PORTS --\n' + + # Input ports + inputs = self.inputs() + if inputs: + s += ' Inputs:\n' + for var in inputs: + s += f' {var.name:<20} : {var.sig}\n' + else: + s += ' Inputs: None\n' + + # Output ports + outputs = self.outputs() + if outputs: + s += ' Outputs:\n' + for var in outputs: + s += f' {var.name:<20} : {var.sig}\n' + else: + s += ' Outputs: None\n' return s def clone(self): @@ -219,11 +302,13 @@ def resources(self): if sig.is_input() or sig.is_output(): continue if sig.is_reg(): - num_of_regs += sig.width + if sig.width > 0: + num_of_regs += sig.width elif sig.is_regarray(): num_of_regs += sig.width[0] * sig.width[1] elif sig.is_net(): - num_of_nets += sig.width + if sig.width > 0: + num_of_nets += sig.width elif sig.is_netarray(): num_of_nets += sig.width[0] * sig.width[1] num_of_states = 0 diff --git a/polyphony/compiler/ahdl/transformers/ahdlopt.py b/polyphony/compiler/ahdl/transformers/ahdlopt.py index af6d490..a6744d6 100644 --- a/polyphony/compiler/ahdl/transformers/ahdlopt.py +++ b/polyphony/compiler/ahdl/transformers/ahdlopt.py @@ -14,10 +14,7 @@ def process(self, hdlmodule): self.updated = False self.usedef = AHDLUseDefDetector().process(hdlmodule) super().process(hdlmodule) - # logger.debug(str(hdlmodule)) AHDLVarReducer().process(hdlmodule) - #logger.debug('!!! after reduce') - #logger.debug(str(hdlmodule)) def _is_ignore_case(self, target:AHDL_VAR) -> bool: if target.ctx != Ctx.LOAD: @@ -80,6 +77,8 @@ def _can_reduce(self, lvalue): return False if var.sig.is_connector(): return False + if var.sig.is_field(): + return False return True def visit_AHDL_MOVE(self, ahdl): diff --git a/polyphony/compiler/ahdl/transformers/iotransformer.py b/polyphony/compiler/ahdl/transformers/iotransformer.py index 89a2d70..fbda0df 100644 --- a/polyphony/compiler/ahdl/transformers/iotransformer.py +++ b/polyphony/compiler/ahdl/transformers/iotransformer.py @@ -94,7 +94,39 @@ def visit_State(self, state): else: return new_state + def _contains_meta_wait(self, ahdl): + if ahdl.is_a(AHDL_META_WAIT): + return True + if ahdl.is_a(AHDL_BLOCK): + return any(self._contains_meta_wait(c) for c in ahdl.codes) + if ahdl.is_a(AHDL_IF): + return any(self._contains_meta_wait(b) for b in ahdl.blocks) + return False + + def _sink_trailing_codes_into_wait_if(self, block): + codes = list(block.codes) + changed = True + while changed: + changed = False + for i, code in enumerate(codes): + if code.is_a(AHDL_IF) and self._contains_meta_wait(code) and i < len(codes) - 1: + trailing = tuple(codes[i + 1:]) + new_blocks = [] + new_conds = list(code.conds) + for blk in code.blocks: + new_blk = AHDL_BLOCK(blk.name, blk.codes + trailing) + new_blocks.append(new_blk) + if code.conds[-1] is not None: + new_conds.append(None) + new_blocks.append(AHDL_BLOCK('', trailing)) + new_if = AHDL_IF(tuple(new_conds), tuple(new_blocks)) + codes = codes[:i] + [new_if] + changed = True + break + return AHDL_BLOCK(block.name, tuple(codes)) + def visit_AHDL_BLOCK(self, ahdl): + ahdl = self._sink_trailing_codes_into_wait_if(ahdl) new_block = super().visit_AHDL_BLOCK(ahdl) meta_waits = [c for c in new_block.codes if c.is_a(AHDL_META_WAIT)] if meta_waits: diff --git a/polyphony/compiler/ahdl/transformers/statereducer.py b/polyphony/compiler/ahdl/transformers/statereducer.py index 8a59ba2..a1ef575 100644 --- a/polyphony/compiler/ahdl/transformers/statereducer.py +++ b/polyphony/compiler/ahdl/transformers/statereducer.py @@ -41,8 +41,9 @@ def _remove_unreached_state(self, hdlmodule): stg.remove_state(state) def _remove_empty_init_state(self, hdlmodule): - empty_stgs = [] + empty_fsms = [] for fsm in hdlmodule.fsms.values(): + empty_stgs = [] for stg in fsm.stgs: if is_empty_state(stg.states[0]): stg.remove_state(stg.states[0]) @@ -50,6 +51,11 @@ def _remove_empty_init_state(self, hdlmodule): empty_stgs.append(stg) for stg in empty_stgs: fsm.remove_stg(stg) + if not fsm.stgs: + empty_fsms.append(fsm) + for fsm in empty_fsms: + hdlmodule.remove_sig(fsm.state_var) + del hdlmodule.fsms[fsm.name] class StateGraph(Graph): def __str__(self): diff --git a/polyphony/compiler/common/env.py b/polyphony/compiler/common/env.py index 5a637ba..6460905 100644 --- a/polyphony/compiler/common/env.py +++ b/polyphony/compiler/common/env.py @@ -49,6 +49,7 @@ class Env(object): quiet_level = 0 enable_verilog_monitor = False enable_verilog_dump = False + watch_signals = '' sleep_sentinel_thredhold = 10 def __init__(self): @@ -68,6 +69,7 @@ def __init__(self): self.scope2output_hdlscope: dict[Scope, HDLScope] = {} self.targets = [] self.root_dir = '' + self.seq_id_to_array: dict = {} def load_config(self, config): for key, v in config.items(): diff --git a/polyphony/compiler/ir/analysis/regreducer.py b/polyphony/compiler/ir/analysis/regreducer.py index dfcdc70..2d22843 100644 --- a/polyphony/compiler/ir/analysis/regreducer.py +++ b/polyphony/compiler/ir/analysis/regreducer.py @@ -1,3 +1,4 @@ +from collections import deque from ..ir import * from ..irhelper import qualified_symbols from ..irvisitor import IRVisitor @@ -6,12 +7,69 @@ logger = getLogger(__name__) +def _is_clksleep(stm): + """Check if stm is a clksleep call (excludes wait_until and wait_*).""" + return (stm.is_a(EXPR) and stm.exp.is_a(SYSCALL) and + stm.exp.name == 'polyphony.timing.clksleep') + + class AliasVarDetector(IRVisitor): def process(self, scope): self.usedef = scope.usedef self.removes = [] super().process(scope) + def _has_clksleep_between(self, def_stm, use_stm): + """Check if there is a clksleep between def_stm and use_stm.""" + def_blk = def_stm.block + use_blk = use_stm.block + if def_blk is use_blk: + stms = def_blk.stms + in_range = False + for stm in stms: + if stm is def_stm: + in_range = True + continue + if stm is use_stm: + return False + if in_range and _is_clksleep(stm): + return True + return False + # Different blocks: check if any CFG path from def_blk to use_blk contains a clksleep + # First, check if def_blk has a clksleep after def_stm + stms = def_blk.stms + found_def = False + for stm in stms: + if stm is def_stm: + found_def = True + continue + if found_def and _is_clksleep(stm): + return True + # BFS from def_blk to use_blk + visited = set() + queue = deque(def_blk.succs) + while queue: + blk = queue.popleft() + if blk in visited: + continue + visited.add(blk) + if blk is use_blk: + # Check if use_blk has a clksleep before use_stm + for stm in blk.stms: + if stm is use_stm: + break + if _is_clksleep(stm): + return True + return False + # Intermediate block contains a clksleep + for stm in blk.stms: + if _is_clksleep(stm): + return True + for succ in blk.succs: + if succ not in visited: + queue.append(succ) + return False + def visit_CMOVE(self, ir): assert ir.dst.is_a(IRVariable) sym = qualified_symbols(ir.dst, self.scope)[-1] @@ -35,10 +93,14 @@ def visit_MOVE(self, ir): if self.scope.is_worker(): module = self.scope.worker_owner else: - # TODO: + # Walk up the parent chain to find the nearest enclosing module scope module = self.scope.parent + while module is not None and not module.is_module(): + module = module.parent if sym.typ.is_object(): return + if module is None or module.field_usedef is None: + return qsym = qualified_symbols(ir.dst, self.scope) defstms = module.field_usedef.get_def_stms(qsym) if len(defstms) == 1: @@ -91,11 +153,17 @@ def visit_MOVE(self, ir): return elif ir.src.is_a(ARRAY): return - stms = self.usedef.get_stms_defining(sym) - if len(stms) > 1: + def_stms = self.usedef.get_stms_defining(sym) + if len(def_stms) > 1: return - stms = self.usedef.get_stms_using(sym) - for stm in stms: + use_stms = self.usedef.get_stms_using(sym) + if sched == 'timed' and def_stms: + def_stm = next(iter(def_stms)) + for use_stm in use_stms: + if self._has_clksleep_between(def_stm, use_stm): + logger.debug(f'{sym} crosses clksleep, keeping as reg') + return + for stm in use_stms: if sched != 'pipeline' and stm.block.synth_params['scheduling'] == 'pipeline': return if sched != 'parallel' and stm.block.synth_params['scheduling'] == 'parallel': diff --git a/polyphony/compiler/ir/analysis/typecheck.py b/polyphony/compiler/ir/analysis/typecheck.py index a4948e1..8261f47 100644 --- a/polyphony/compiler/ir/analysis/typecheck.py +++ b/polyphony/compiler/ir/analysis/typecheck.py @@ -390,7 +390,7 @@ def visit_NEW(self, ir): for i, (_, arg) in enumerate(ir.args): if arg.is_a(IRVariable): arg_t = irexp_type(arg, self.scope) - if arg_t.is_scalar() or arg_t.is_class(): + if arg_t.is_scalar() or arg_t.is_class() or arg_t.is_function() or arg_t.is_seq(): continue fail(self.current_stm, Errors.MODULE_ARG_MUST_BE_X_TYPE, [arg_t]) if self.scope.is_global() and not callee_scope.is_module(): diff --git a/polyphony/compiler/ir/scope.py b/polyphony/compiler/ir/scope.py index 52fcecf..a9fabf7 100644 --- a/polyphony/compiler/ir/scope.py +++ b/polyphony/compiler/ir/scope.py @@ -236,6 +236,7 @@ class Scope(Tagged, SymbolTable): 'function_module', 'inlinelib', 'unflatten', 'package', 'directory', + 'superseded', } scope_id = 0 unnamed_ids = defaultdict(int) @@ -482,7 +483,8 @@ def clone_name(prefix: str, postfix: str, base_name: str) -> str: name = clone_name(prefix, postfix, self.base_name) parent = self.parent if parent is None else parent - s = Scope.create(parent, name, set(self.tags), self.lineno, origin=self) + cloned_tags = set(self.tags) - {'superseded'} + s = Scope.create(parent, name, cloned_tags, self.lineno, origin=self) self_sym = self.parent.find_sym(self.base_name) new_sym_typ = self_sym.typ.clone(scope=s) diff --git a/polyphony/compiler/ir/transformers/constopt.py b/polyphony/compiler/ir/transformers/constopt.py index 2f5889f..124d935 100644 --- a/polyphony/compiler/ir/transformers/constopt.py +++ b/polyphony/compiler/ir/transformers/constopt.py @@ -482,6 +482,13 @@ def visit_SYSCALL(self, ir): def visit_MREF(self, ir): if not ir.offset.is_a(CONST): return ir + if ir.mem.is_a(ARRAY): + offset = ir.offset.value + if 0 <= offset < len(ir.mem.items): + return ir.mem.items[offset] + return ir + if not isinstance(ir.mem, IRNameExp): + return ir qsym = qualified_symbols(ir.mem, self.scope) mem_sym = qsym[-1] assert isinstance(mem_sym, Symbol) @@ -725,8 +732,16 @@ def process_scopes(self, scopes): self.visit(stm) for sym, c in self.constant_table.items(): sym.scope.constants[sym] = c + if sym.scope.origin: + origin_scope = sym.scope.origin + if sym.name in origin_scope.symbols: + origin_scope.constants[origin_scope.symbols[sym.name]] = c for sym, c in self.constant_array_table.items(): sym.scope.constants[sym] = c + if sym.scope.origin: + origin_scope = sym.scope.origin + if sym.name in origin_scope.symbols: + origin_scope.constants[origin_scope.symbols[sym.name]] = c def collect_stms(self, scope): stms = [] diff --git a/polyphony/compiler/ir/transformers/copyopt.py b/polyphony/compiler/ir/transformers/copyopt.py index c0d5b24..8fbeec2 100755 --- a/polyphony/compiler/ir/transformers/copyopt.py +++ b/polyphony/compiler/ir/transformers/copyopt.py @@ -103,7 +103,13 @@ def _replace_copies(self, scope: Scope, udupdater: UseDefUpdater, copy_stm: MOVE copies.append(mv) return len(uses) > 0 - def _find_root_def(self, qsym: tuple[Symbol]) -> IR|None: + def _find_root_def(self, qsym: tuple[Symbol], _visited: set|None = None) -> IR|None: + if _visited is None: + _visited = set() + sym_key = id(qsym[-1]) + if sym_key in _visited: + return None + _visited.add(sym_key) defs = list(self.scope.usedef.get_stms_defining(qsym)) if len(defs) != 1: return None @@ -121,7 +127,7 @@ def _find_root_def(self, qsym: tuple[Symbol]) -> IR|None: src_t = src_sym.typ if src_t != dst_t: return None - orig = self._find_root_def(src_qsym) + orig = self._find_root_def(src_qsym, _visited) if orig: return orig else: @@ -133,7 +139,7 @@ def _find_root_def(self, qsym: tuple[Symbol]) -> IR|None: src_t = src_sym.typ if src_t != dst_t: return None - orig = self._find_root_def(src_qsym) + orig = self._find_root_def(src_qsym, _visited) if orig: return orig else: @@ -160,6 +166,8 @@ def visit_MOVE(self, ir): return if dst_sym.is_free(): return + if dst_t.is_function(): + return if ir.src.is_a(TEMP): src_sym = qualified_symbols(ir.src, self.scope)[-1] assert isinstance(src_sym, Symbol) diff --git a/polyphony/compiler/ir/transformers/instantiator.py b/polyphony/compiler/ir/transformers/instantiator.py index 5efb4b0..879f050 100644 --- a/polyphony/compiler/ir/transformers/instantiator.py +++ b/polyphony/compiler/ir/transformers/instantiator.py @@ -139,20 +139,43 @@ def process_scopes(self, scopes): call.args[1:] = args return next_scopes + def _resolve_seq_arg(self, arg: IRExp, caller_scope: Scope) -> IRExp: + """Resolve a seq-typed arg to its ARRAY when it was converted to an integer ID. + + objtransform._transform_seq_ctor converts seq definitions to integer symbol IDs + and stores them as CONST. We recover the original ARRAY either from env.seq_id_to_array + (for CONST(seq_id) from testbench) or from the caller's usedef (for TEMP vars). + """ + if arg.is_a(CONST) and isinstance(arg.value, int): + array = env.seq_id_to_array.get(arg.value) + if array is not None: + return array.clone() + elif arg.is_a(IRVariable) and hasattr(caller_scope, 'usedef') and caller_scope.usedef: + qsym = qualified_symbols(arg, caller_scope) + defs = list(caller_scope.usedef.get_stms_defining(qsym)) + if len(defs) == 1 and defs[0].is_a(MOVE) and defs[0].src.is_a(ARRAY): + return defs[0].src.clone() + return arg + def _bind_args(self, caller_scope: Scope, args: list[tuple[str, IRExp]], callee: Scope): binding: list[tuple[int, IRExp]] = [] module_param_vars: list[tuple[str, IRExp]] = [] param_names = callee.param_names() + param_syms = callee.param_symbols() for i, (_, arg) in enumerate(args): if isinstance(arg, IRExp): if param_names[i].isupper(): module_param_vars.append((param_names[i], arg)) else: + # Resolve seq-typed args (tuple/list) that were converted to integer IDs + if i < len(param_syms) and param_syms[i].typ.is_seq(): + arg = self._resolve_seq_arg(arg, caller_scope) binding.append((i, arg)) if binding: UseDefDetector().process(callee) for i, arg in binding: - VarReplacer.replace_uses(callee, TEMP(callee.param_symbols()[i].name), arg) + pname = callee.param_symbols()[i].name + VarReplacer.replace_uses(callee, TEMP(pname), arg) callee.remove_param([i for i, _ in binding]) for i, _ in reversed(binding): args.pop(i) diff --git a/polyphony/compiler/ir/transformers/objtransform.py b/polyphony/compiler/ir/transformers/objtransform.py index 3f4b869..79afe65 100644 --- a/polyphony/compiler/ir/transformers/objtransform.py +++ b/polyphony/compiler/ir/transformers/objtransform.py @@ -5,6 +5,7 @@ from ..types.type import Type from ..analysis.usedef import UseDefUpdater from ...common.utils import replace_item +from ...common.env import env from logging import getLogger logger = getLogger(__name__) @@ -286,6 +287,7 @@ def _transform_seq_ctor(self): assert defstm.src.is_a(ARRAY) seq_id = self.scope.add_sym(f'{seq_sym.name}{seq_sym.id}__id', tags=set(), typ=Type.int(16)) + env.seq_id_to_array[seq_id.id] = defstm.src.clone() mv = MOVE(TEMP(seq_id.name), CONST(seq_id.id), loc=defstm.loc) diff --git a/polyphony/compiler/ir/transformers/typeprop.py b/polyphony/compiler/ir/transformers/typeprop.py index fcdd2c0..7decf31 100644 --- a/polyphony/compiler/ir/transformers/typeprop.py +++ b/polyphony/compiler/ir/transformers/typeprop.py @@ -37,6 +37,7 @@ def process_all(self): def process_scopes(self, scopes): self._new_scopes = [] self._old_scopes = set() + self._indirect_old_scopes = set() self.typed = [] self.pure_type_inferrer = PureFuncTypeInferrer() self.worklist = deque(scopes) @@ -48,6 +49,9 @@ def process_scopes(self, scopes): continue if scope.is_directory(): continue + # Skip scopes that have been replaced by specialized versions + if scope in self._old_scopes or scope in self._indirect_old_scopes or scope.is_superseded(): + continue if scope.is_function() and scope.return_type is None: scope.return_type = Type.undef() try: @@ -64,10 +68,35 @@ def process_scopes(self, scopes): def _add_scope(self, scope): if scope.is_testbench() and not scope.parent.is_global(): return - if scope is not self.scope and scope not in self.typed and scope not in self.worklist: + if (scope is not self.scope and + scope not in self.typed and + scope not in self.worklist and + scope not in self._old_scopes and + scope not in self._indirect_old_scopes and + not scope.is_superseded()): self.worklist.appendleft(scope) logger.debug(f'add scope {scope.name}') + def _find_attr_type_from_specialized(self, scope, attr_name) -> 'Type': + """When a class scope's attribute type is undef, + look up the type from a specialized version of the scope.""" + parent = scope.parent + if parent is None: + logger.debug(f'_find_attr_type_from_specialized: {scope.name}.{attr_name} no parent') + return Type.undef() + base_name = scope.base_name + logger.debug(f'_find_attr_type_from_specialized: {scope.name}.{attr_name} base={base_name} children={[c.name for c in parent.children]}') + for child in parent.children: + if (child is not scope and + child.is_specialized() and + child.base_name.startswith(base_name + '_') and + child.has_sym(attr_name)): + sym = child.find_sym(attr_name) + logger.debug(f' found {child.name}.{attr_name} = {sym.typ}') + if not sym.typ.is_undef(): + return sym.typ + return Type.undef() + def visit(self, ir:IR) -> Type: method = 'visit_' + ir.__class__.__name__ visitor = getattr(self, method, None) @@ -208,6 +237,10 @@ def visit_ATTR(self, ir): symbol = attr_scope.find_sym(ir.name) attr_t = symbol.typ + if attr_t.is_undef() and attr_scope.is_class(): + attr_t = self._find_attr_type_from_specialized(attr_scope, ir.name) + if not attr_t.is_undef(): + symbol.typ = attr_t exp_sym = qualified_symbols(ir.exp, self.scope)[-1] assert isinstance(exp_sym, Symbol) assert exptyp == exp_sym.typ @@ -506,7 +539,18 @@ def visit_CALL(self, ir): fail(self.current_stm, Errors.UNSUPPORTED_FUNCTION_MODULE_PARAM_TYPE, [name, t]) self._new_scopes.append(new_scope) - self._old_scopes.add(callee_scope) + # Determine if this is a direct or indirect call. + # Direct call: func_sym is owned by callee_scope's parent (safe to replace the original). + # Indirect call: func_sym is a local variable (e.g., a function-typed parameter) + # whose type resolves to callee_scope. The original scope must be preserved + # because it is still referenced by callers (e.g., in NEW args), but we still + # need to prevent the infinite worklist loop. + owner = self.scope.find_owner_scope(func_sym) + if owner is not None and owner is not callee_scope.parent: + self._indirect_old_scopes.add(callee_scope) + callee_scope.add_tag('superseded') + else: + self._old_scopes.add(callee_scope) if is_new: new_scope_sym = callee_scope.parent.find_sym(new_scope.base_name) self._add_scope(new_scope) @@ -515,9 +559,7 @@ def visit_CALL(self, ir): ret_t = new_scope.return_type # Deal with imported scope asname = f'{ir.name}_{postfix}' - owner = self.scope.find_owner_scope(func_sym) - if owner and func_sym.scope is not owner: - # new_scope_sym is created at original scope so it must be imported + if owner and (func_sym.scope is not owner or owner is not callee_scope.parent): owner.import_sym(new_scope_sym, asname) # Replace name expression if ir.func.is_a(TEMP): diff --git a/polyphony/compiler/ir/types/functiontype.py b/polyphony/compiler/ir/types/functiontype.py index aa8161c..328ce89 100644 --- a/polyphony/compiler/ir/types/functiontype.py +++ b/polyphony/compiler/ir/types/functiontype.py @@ -13,7 +13,12 @@ def __post_init__(self): assert self.scope.is_function() or self.scope.is_method() or self.scope.is_object() def can_assign(self, rhs_t): - return self.name == rhs_t.name and self.scope.is_object() + if self.name != rhs_t.name: + return False + if self.scope.is_object(): + return True + # Also allow assigning between compatible function types (e.g. worker parameter binding) + return self.scope.is_assignable(rhs_t.scope) def propagate(self, rhs_t): lhs_t = self diff --git a/polyphony/compiler/ir/types/type.py b/polyphony/compiler/ir/types/type.py index 762d341..380a903 100644 --- a/polyphony/compiler/ir/types/type.py +++ b/polyphony/compiler/ir/types/type.py @@ -198,6 +198,9 @@ def mangled_names(cls, types): s = f'b' elif t.is_str(): s = f's' + elif t.is_function(): + name = t.scope.scope_id + s = f'f{name}' elif t.is_object(): name = t.scope.scope_id s = f'o{name}' diff --git a/polyphony/compiler/target/verilog/vericodegen.py b/polyphony/compiler/target/verilog/vericodegen.py index 58314bb..898301f 100644 --- a/polyphony/compiler/target/verilog/vericodegen.py +++ b/polyphony/compiler/target/verilog/vericodegen.py @@ -477,6 +477,7 @@ def _get_source_text(self, ahdl): if text[-1] == '\n': text = text[:-1] filename = os.path.basename(node.tag.loc.filename) + text = text.replace('%', '%%') return f'{filename}:{node.tag.loc.lineno} {text}' def _emit_source_text(self, ahdl): diff --git a/polyphony/compiler/target/verilog/veritestgen.py b/polyphony/compiler/target/verilog/veritestgen.py index 26d959b..7326397 100644 --- a/polyphony/compiler/target/verilog/veritestgen.py +++ b/polyphony/compiler/target/verilog/veritestgen.py @@ -35,6 +35,8 @@ def generate(self): self._generate_reset_task() if env.enable_verilog_dump: self._generate_dump_vcd_task() + if env.watch_signals: + self._generate_monitor_task(env.watch_signals) self.set_indent(-2) self.emit('endmodule\n') @@ -72,3 +74,35 @@ def _generate_dump_vcd_task(self): self.emit(f'$dumpvars(0, {self._safe_name(net.name)}[{i}]);') self.set_indent(-2) self.emit('end') + + def _generate_monitor_task(self, watch_signals): + signal_names = [s.strip() for s in watch_signals.split(',')] + # Resolve Python hierarchical names to Verilog testbench signal names + # Python name "m.i" -> look for signal with name "m_i" in testbench scope + verilog_names = [] + valid_names = [] + all_sigs = {sig.name: sig for sig in self.hdlmodule.get_signals( + {'reg', 'net', 'regarray', 'netarray'}, {'input', 'output'})} + # Also include input/output signals + for sig in self.hdlmodule.get_signals({'input', 'output'}): + all_sigs[sig.name] = sig + for py_name in signal_names: + v_name = py_name.replace('.', '_') + if v_name in all_sigs: + verilog_names.append(self._safe_name(v_name)) + valid_names.append(py_name) + else: + logger.warning(f"watch signal '{py_name}' not found as '{v_name}' in testbench") + if not valid_names: + return + fmt_parts = ["%5t:"] + args = ["$time"] + for py_name, v_name in zip(valid_names, verilog_names): + fmt_parts.append(f" {py_name}=%d") + args.append(v_name) + fmt_str = ''.join(fmt_parts) + self.emit('initial begin') + self.set_indent(2) + self.emit(f'$monitor("{fmt_str}", {", ".join(args)});') + self.set_indent(-2) + self.emit('end') diff --git a/polyphony/simulator.py b/polyphony/simulator.py index 17ebefe..47ade6d 100644 --- a/polyphony/simulator.py +++ b/polyphony/simulator.py @@ -19,12 +19,14 @@ class HDLAssertionError(AssertionError): def twos_comp(val, bits): """compute the 2's complement of int value val""" - if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255 - val = val - (1 << bits) # compute negative value - return val # return positive value as is + if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255 + val = val - (1 << bits) # compute negative value + return val # return positive value as is + class Value(Tagged): TAGS = Signal.TAGS + def __init__(self, val, width, sign, signal): if signal: super().__init__(signal.tags) @@ -33,7 +35,7 @@ def __init__(self, val, width, sign, signal): self.width = width self.sign = sign self.signal = signal - self.val = 'X' + self.val = "X" self.set(val) def set(self, v): @@ -65,11 +67,11 @@ def __str__(self): return str(self.val) def __repr__(self): - return f'Integer[{self.width}]={self.val}' + return f"Integer[{self.width}]={self.val}" def __bin_op__(self, op, rhs): - if self.val == 'X' or rhs.val == 'X': - return Integer('X', 0, False) + if self.val == "X" or rhs.val == "X": + return Integer("X", 0, False) width = self.width + rhs.width sign = max(self.sign, rhs.sign) value = op(self.val, rhs.val) @@ -91,8 +93,8 @@ def __mod__(self, rhs): return self.__bin_op__(operator.mod, rhs) def __bit_op__(self, op, rhs): - if self.val == 'X' or rhs.val == 'X': - return Integer('X', 0, False) + if self.val == "X" or rhs.val == "X": + return Integer("X", 0, False) width = max(self.width, rhs.width) value = op(self.val, rhs.val) return Integer(value, width, False) @@ -115,52 +117,52 @@ def __ne__(self, rhs): return Integer(int(b), 1, False) def __lt__(self, rhs): - if self.val == 'X' or rhs.val == 'X': - return Integer('X', 0, False) + if self.val == "X" or rhs.val == "X": + return Integer("X", 0, False) b = self.val < rhs.val return Integer(int(b), 1, False) def __le__(self, rhs): - if self.val == 'X' or rhs.val == 'X': - return Integer('X', 0, False) + if self.val == "X" or rhs.val == "X": + return Integer("X", 0, False) b = self.val <= rhs.val return Integer(int(b), 1, False) def __gt__(self, rhs): - if self.val == 'X' or rhs.val == 'X': - return Integer('X', 0, False) + if self.val == "X" or rhs.val == "X": + return Integer("X", 0, False) b = self.val > rhs.val return Integer(int(b), 1, False) def __ge__(self, rhs): - if self.val == 'X' or rhs.val == 'X': - return Integer('X', 0, False) + if self.val == "X" or rhs.val == "X": + return Integer("X", 0, False) b = self.val >= rhs.val return Integer(int(b), 1, False) def __lshift__(self, rhs): - if self.val == 'X' or rhs.val == 'X': - return Integer('X', 0, False) + if self.val == "X" or rhs.val == "X": + return Integer("X", 0, False) if rhs.val > self.width or rhs.val < 0: return Integer(0, self.width, self.sign) v = self.val << rhs.val return Integer(v, self.width, self.sign) def __rshift__(self, rhs): - if self.val == 'X' or rhs.val == 'X': - return Integer('X', 0, False) + if self.val == "X" or rhs.val == "X": + return Integer("X", 0, False) if rhs.val > self.width or rhs.val < 0: return Integer(0, self.width, self.sign) v = self.val >> rhs.val return Integer(v, self.width, self.sign) def __bool__(self): - if self.val == 'X': + if self.val == "X": return False return bool(self.val) def __int__(self): - if self.val == 'X': + if self.val == "X": return None return self.val @@ -168,15 +170,16 @@ def __pos__(self): return self def __neg__(self): - if self.val == 'X': + if self.val == "X": return self return Integer(-self.val, self.width, self.sign) def __invert__(self): - if self.val == 'X': + if self.val == "X": return self return Integer(~self.val, self.width, self.sign) + class Net(Value): def __init__(self, val, width, signal): super().__init__(val, width, signal.is_int(), signal) @@ -185,19 +188,20 @@ def __str__(self): return str(self.val) def __repr__(self): - return f'Net(\'{self.signal}\')={self.val}' + return f"Net('{self.signal}')={self.val}" + class Reg(Value): def __init__(self, val, width, signal): super().__init__(val, width, signal.is_int(), signal) - self.val = 'X' - self.prev_val = 'X' + self.val = "X" + self.prev_val = "X" def __str__(self): return str(self.val) def __repr__(self): - return f'Reg(\'{self.signal}\')={self.val}' + return f"Reg('{self.signal}')={self.val}" def set(self, v): if isinstance(v, int): @@ -218,23 +222,169 @@ def update(self): self.val = self.next +class SimulationObserver: + def __init__(self, vcd_file=None, log_file=None): + self.watched = [] # [(name, value_ref), ...] + self.prev_values = {} # name -> previous value + self._vcd_file_path = vcd_file + self._log_file_path = log_file + self._vcd_file = None + self._log_file = None + self._in_reset = False + self._vcd_header_written = False + if log_file: + os.makedirs(os.path.dirname(log_file) or '.', exist_ok=True) + self._log_file = open(log_file, 'w') + if vcd_file: + os.makedirs(os.path.dirname(vcd_file) or '.', exist_ok=True) + self._vcd_file = open(vcd_file, 'w') + + def add_watch(self, name, value): + """Add a signal to watch list. `value` is a Net or Reg object.""" + self.watched.append((name, value)) + self.prev_values[name] = None + + def on_reset_start(self): + self._in_reset = True + + def on_reset_done(self, clock_time): + self._in_reset = False + if not self.watched: + return + if self._log_file: + parts = [f"{name}={val.val}" for name, val in self.watched] + self._log_file.write(f"{clock_time:>4}: [reset] {', '.join(parts)}\n") + if self._vcd_file: + self._write_vcd_header() + self._write_vcd_values(clock_time) + for name, val in self.watched: + self.prev_values[name] = val.val + + def dump_initial(self, clock_time): + """Dump initial values after watches are registered (post-reset).""" + if not self.watched: + return + if self._log_file: + parts = [f"{name}={val.val}" for name, val in self.watched] + self._log_file.write(f"{clock_time:>4}: [init] {', '.join(parts)}\n") + if self._vcd_file and not self._vcd_header_written: + self._write_vcd_header() + self._write_vcd_values(clock_time) + for name, val in self.watched: + self.prev_values[name] = val.val + + def on_cycle(self, clock_time): + if self._in_reset: + return + if not self.watched: + return + if self._log_file: + changes = [] + for name, val in self.watched: + prev = self.prev_values.get(name) + cur = val.val + if cur != prev: + changes.append(f"{name}: {prev} -> {cur}") + if changes: + self._log_file.write(f"{clock_time:>4}: {', '.join(changes)}\n") + if self._vcd_file: + if not self._vcd_header_written: + self._write_vcd_header() + self._write_vcd_values(clock_time) + for name, val in self.watched: + self.prev_values[name] = val.val + + def close(self): + if self._log_file: + self._log_file.flush() + self._log_file.close() + self._log_file = None + if self._vcd_file: + self._vcd_file.flush() + self._vcd_file.close() + self._vcd_file = None + + def _write_vcd_header(self): + self._vcd_header_written = True + f = self._vcd_file + f.write("$timescale 1ns $end\n") + f.write("$scope module test $end\n") + for i, (name, val) in enumerate(self.watched): + width = val.width if isinstance(val.width, int) else 32 + sym = chr(33 + i) + f.write(f"$var wire {width} {sym} {name} $end\n") + f.write("$upscope $end\n") + f.write("$enddefinitions $end\n") + + def _write_vcd_values(self, clock_time): + f = self._vcd_file + f.write(f"#{clock_time}\n") + for i, (name, val) in enumerate(self.watched): + width = val.width if isinstance(val.width, int) else 32 + sym = chr(33 + i) + v = val.val + if isinstance(v, int): + bits = format(v & ((1 << width) - 1), f'0{width}b') + f.write(f"b{bits} {sym}\n") + else: + f.write(f"bx {sym}\n") + + +def _build_name_table(simulator): + """Walk core model tree, build {id(value): hierarchical_name} mapping.""" + table = {} + for core_model in simulator.models: + prefix = core_model.hdlmodule.name + _walk_core_model(core_model, prefix, table) + return table + + +def _walk_core_model(core_model, prefix, table): + """Walk SimpleNamespace model tree.""" + for attr_name, val in vars(core_model).items(): + if attr_name.startswith('_') or attr_name in ('hdlmodule', 'clk', 'rst'): + continue + if isinstance(val, (Net, Reg)): + table[id(val)] = f"{prefix}.{attr_name}" + elif isinstance(val, Port): + table[id(val.value)] = f"{prefix}.{attr_name}" + elif isinstance(val, Model): + sub_core = super(Model, val).__getattribute__("__model") + _walk_core_model(sub_core, f"{prefix}.{attr_name}", table) + + +def _resolve_hierarchical_name(simulator, port): + """Resolve a Port to its hierarchical name using the name table.""" + if not hasattr(simulator, '_name_table'): + simulator._name_table = _build_name_table(simulator) + val_id = id(port.value) + name = simulator._name_table.get(val_id) + if name is None: + name = port.value.signal.name if port.value.signal else '?' + return name + + current_simulator = None + def clkfence(): if current_simulator is None: raise RuntimeError() current_simulator._period() + def clksleep(n): if current_simulator is None: raise RuntimeError() current_simulator._period(n) + def clktime(): if current_simulator is None: raise RuntimeError() return current_simulator.clock_time + def clkrange(n): if current_simulator is None: raise RuntimeError() @@ -245,6 +395,33 @@ def clkrange(n): current_simulator._period() +def watch(*signals, vcd=None, log=None): + """Register Port signals for debug observation. + + Args: + *signals: Port objects to watch. + vcd: VCD output file path. None for default, False to disable. + log: Text log file path. None for default, False to disable. + """ + sim = current_simulator + if not sim: + return + if not sim.observer: + name = getattr(sim, 'case_name', 'test') + vcd_path = vcd if vcd is not None else f".tmp/{name}_py.vcd" + log_path = log if log is not None else f".tmp/{name}_py.log" + if vcd_path is False: + vcd_path = None + if log_path is False: + log_path = None + sim.observer = SimulationObserver(vcd_file=vcd_path, log_file=log_path) + for port in signals: + if not isinstance(port, Port): + continue + hier_name = _resolve_hierarchical_name(sim, port) + sim.observer.add_watch(hier_name, port.value) + + class Port(object): def __init__(self, dtype, direction, init=None, **kwargs): self.value = None @@ -273,12 +450,15 @@ def __str__(self): return str(self.value) def __repr__(self): - return f'Port(\'{repr(self.value)}\')' + return f"Port('{repr(self.value)}')" def edge(self, old_v, new_v): assert isinstance(self.value, Reg) return self.value.prev_val == old_v and self.value.val == new_v + def assign(self, func: callable): + pass + @property def signal(self): return self.value.signal @@ -287,14 +467,16 @@ def signal(self): class Simulator(object): def __init__(self, model): if isinstance(model, list): - self.models = [getattr(m, '__model') for m in model] + self.models = [getattr(m, "__model") for m in model] elif isinstance(model, Model): - self.models = [getattr(model, '__model')] + self.models = [getattr(model, "__model")] else: assert False self.evaluators = [ModelEvaluator(model) for model in self.models] self.clock_time = 0 + self.observer = None + self.case_name = '' def __enter__(self): self.begin() @@ -315,6 +497,8 @@ def end(self): global current_simulator if current_simulator is None: raise RuntimeError() + if self.observer: + self.observer.close() current_simulator = None def _period(self, count=1): @@ -326,14 +510,20 @@ def _period(self, count=1): self.clock_time += 1 for model in self.models: model.clk.val = 0 + if self.observer: + self.observer.on_cycle(self.clock_time) def _reset(self, count=1): + if self.observer: + self.observer.on_reset_start() for model in self.models: model.rst.val = 1 self._period(count) for model in self.models: model.rst.val = 0 self.clock_time = 0 + if self.observer: + self.observer.on_reset_done(self.clock_time) class ModelEvaluator(AHDLVisitor): @@ -342,7 +532,6 @@ def __init__(self, model): self.model = model self.updated_sigs = set() - def visit(self, ahdl): # if ahdl.is_a(AHDL_STM): # print('eval', ahdl) @@ -356,10 +545,16 @@ def eval(self): def _eval_decls(self): self.updated_sigs.add(None) + _max_iter = 1000 while self.updated_sigs: self.updated_sigs.clear() for decl in self.model._decls: self.visit(decl) + _max_iter -= 1 + if _max_iter <= 0: + import warnings + warnings.warn(f'_eval_decls: iteration limit reached for {self.model}') + break def _find_model(self, ahdl): assert isinstance(ahdl, AHDL_VAR) @@ -367,7 +562,7 @@ def _find_model(self, ahdl): for v in ahdl.vars[:-1]: if v.is_subscope(): user_model = getattr(model, v.name) - model = getattr(user_model, '__model') + model = getattr(user_model, "__model") assert isinstance(model, types.SimpleNamespace) return model @@ -375,7 +570,7 @@ def _collect_model(self, model): models = [model] for v in vars(model).values(): if isinstance(v, Model): - model_core = getattr(v, '__model') + model_core = getattr(v, "__model") models.extend(self._collect_model(model_core)) return models @@ -422,11 +617,11 @@ def visit_AHDL_SUBSCRIPT(self, ahdl): assert isinstance(mem, tuple) offs = self.visit(ahdl.offset) assert isinstance(offs, Integer) - if offs.get() == 'X': - return Integer('X', 1, False) + if offs.get() == "X": + return Integer("X", 1, False) mem_offs = offs.get() if mem_offs < 0 or mem_offs >= len(mem): - return Integer('X', 1, False) + return Integer("X", 1, False) v = mem[mem_offs] assert isinstance(v, (Reg, Net)) if ahdl.memvar.ctx is Ctx.LOAD: @@ -438,27 +633,27 @@ def eval_binop(self, op, left, right): r = self.visit(right) assert isinstance(l, Integer) assert isinstance(r, Integer) - if l.val == 'X' or r.val == 'X': - return Integer('X', 0, False) - if op == 'Add': + if l.val == "X" or r.val == "X": + return Integer("X", 0, False) + if op == "Add": return l + r - elif op == 'Sub': + elif op == "Sub": return l - r - elif op == 'Mult': + elif op == "Mult": return l * r - elif op == 'FloorDiv': + elif op == "FloorDiv": return l // r - elif op == 'Mod': + elif op == "Mod": return l % r - elif op == 'LShift': + elif op == "LShift": return l << r - elif op == 'RShift': + elif op == "RShift": return l >> r - elif op == 'BitOr': + elif op == "BitOr": return l | r - elif op == 'BitXor': + elif op == "BitXor": return l ^ r - elif op == 'BitAnd': + elif op == "BitAnd": return l & r else: assert False @@ -468,46 +663,45 @@ def eval_relop(self, op, left, right): r = self.visit(right) assert isinstance(l, Integer) assert isinstance(r, Integer) - if op == 'And': + if op == "And": if l.val and r.val: return Integer(1, 1, False) else: return Integer(0, 1, False) - elif op == 'Or': + elif op == "Or": if l.val or r.val: return Integer(1, 1, False) else: return Integer(0, 1, False) - elif op == 'Eq': + elif op == "Eq": return l == r - elif op == 'NotEq': + elif op == "NotEq": return l != r - elif op == 'Lt': + elif op == "Lt": return l < r - elif op == 'LtE': + elif op == "LtE": return l <= r - elif op == 'Gt': + elif op == "Gt": return l > r - elif op == 'GtE': + elif op == "GtE": return l >= r - elif op == 'Is': + elif op == "Is": return l == r - elif op == 'IsNot': + elif op == "IsNot": return l != r def eval_unop(self, op, arg): a = self.visit(arg) assert isinstance(a, Integer) - if op == 'USub': + if op == "USub": return -a - elif op == 'UAdd': + elif op == "UAdd": return a - elif op == 'Not': + elif op == "Not": return Integer(not a.get(), 1, False) - elif op == 'Invert': + elif op == "Invert": return ~a - def visit_AHDL_OP(self, ahdl): if ahdl.is_unop(): a = self.eval_unop(ahdl.op, ahdl.args[0]) @@ -532,7 +726,7 @@ def visit_AHDL_META_OP(self, ahdl): def visit_AHDL_SYMBOL(self, ahdl): if ahdl.name == "'bz": - return Integer('X', 1, False) + return Integer("X", 1, False) assert False def visit_AHDL_CONCAT(self, ahdl): @@ -568,7 +762,7 @@ def visit_AHDL_IO_WRITE(self, ahdl): self.visit(ahdl.src) def visit_AHDL_SEQ(self, ahdl): - method = 'visit_{}'.format(ahdl.factor.__class__.__name__) + method = "visit_{}".format(ahdl.factor.__class__.__name__) visitor = getattr(self, method, None) return visitor(ahdl.factor) @@ -579,7 +773,7 @@ def visit_AHDL_IF(self, ahdl): cv = self.visit(cond) assert isinstance(cv, Integer) # print(f'if {cond} == {cv}') - assert cv.val != 'X' + assert cv.val != "X" if int(cv.val): self.visit(blk) break @@ -590,7 +784,7 @@ def visit_AHDL_IF(self, ahdl): def visit_AHDL_IF_EXP(self, ahdl): cv = self.visit(ahdl.cond) assert isinstance(cv, Integer) - assert cv.val != 'X' + assert cv.val != "X" if int(cv.val): return self.visit(ahdl.lexp) else: @@ -636,18 +830,18 @@ def visit_AHDL_FUNCALL(self, ahdl): def visit_AHDL_PROCCALL(self, ahdl): args = [self.visit(arg) for arg in ahdl.args] - if ahdl.name == '!hdl_print': + if ahdl.name == "!hdl_print": argvs = [arg.get() for arg in args] print(*argvs) - elif ahdl.name == '!hdl_assert': + elif ahdl.name == "!hdl_assert": if not bool(args[0]): src_text = self._get_source_text(ahdl) raise HDLAssertionError(src_text) else: - raise RuntimeError('unknown function', ahdl.name) + raise RuntimeError("unknown function", ahdl.name) def visit_AHDL_META(self, ahdl): - method = 'visit_' + ahdl.metaid + method = "visit_" + ahdl.metaid visitor = getattr(self, method, None) if visitor: return visitor(ahdl) @@ -683,10 +877,10 @@ def visit_AHDL_EVENT_TASK(self, ahdl): triggered = False for v, e in ahdl.events: sig = getattr(self.model, v.name) - if e == 'rising' and sig.val == 1: + if e == "rising" and sig.val == 1: triggered = True break - elif e == 'falling' and sig.val == 0: + elif e == "falling" and sig.val == 0: triggered = True break if triggered: @@ -710,18 +904,18 @@ def _get_source_text(self, ahdl): text = text.strip() if not text: return - if text[-1] == '\n': + if text[-1] == "\n": text = text[:-1] filename = os.path.basename(node.tag.loc.filename) - return f'{filename} [{node.tag.loc.lineno}]: {text}' + return f"{filename} [{node.tag.loc.lineno}]: {text}" class Model(object): def __getattribute__(self, name): - model_core = super().__getattribute__('__model') - if name == '__model': + model_core = super().__getattribute__("__model") + if name == "__model": return model_core - if name == '__dict__': + if name == "__dict__": di = {} for name, v in vars(model_core).items(): if isinstance(v, Port): @@ -736,33 +930,34 @@ def __getattribute__(self, name): return attr.get() if callable(attr): return attr - if hasattr(attr, 'interface_tag'): + if hasattr(attr, "interface_tag"): return attr raise AttributeError() def __setattr__(self, name, value) -> None: - model_core = super().__getattribute__('__model') + model_core = super().__getattribute__("__model") attr = getattr(model_core, name) if not attr: raise AttributeError() if isinstance(attr, Value): return attr.set(value) if isinstance(attr, Port): - raise AttributeError('Cannot set to port') + raise AttributeError("Cannot set to port") raise AttributeError() def __call__(self, *args, **kwargs): - model_core = super().__getattribute__('__model') - if hasattr(model_core, '_call_body'): + model_core = super().__getattribute__("__model") + if hasattr(model_core, "_call_body"): return model_core._call_body(*args, **kwargs) else: return self + class SimulationModelBuilder(object): def build_model(self, hdlmodule: HDLScope, main_py_module, is_top=False): core_model = self.build_core(hdlmodule, main_py_module, is_top) user_model = Model() - super(Model, user_model).__setattr__('__model', core_model) + super(Model, user_model).__setattr__("__model", core_model) return user_model def build_core(self, hdlmodule: HDLScope, main_py_module, is_top): @@ -772,7 +967,7 @@ def build_core(self, hdlmodule: HDLScope, main_py_module, is_top): setattr(model, sig.name, sub) model.hdlmodule = hdlmodule - if is_top: # isinstance(hdlmodule, HDLModule): + if is_top: # isinstance(hdlmodule, HDLModule): model._tasks = hdlmodule.tasks model._decls = hdlmodule.decls else: @@ -799,7 +994,7 @@ def build_module(self, hdlscope: HDLScope, model, main_py_module, is_top): self._make_rom_function(hdlscope, model) def _add_signals(self, hdlscope: HDLScope, model): - for sig in hdlscope.get_signals({'constant', 'reg', 'net', 'regarray', 'netarray', 'rom'}, {'input', 'output'}): + for sig in hdlscope.get_signals({"constant", "reg", "net", "regarray", "netarray", "rom"}, {"input", "output"}): if sig.is_constant(): val = hdlscope.constants[sig] setattr(model, sig.name, Integer(val, sig.width, sign=sig.is_int())) @@ -826,17 +1021,17 @@ def _add_signals(self, hdlscope: HDLScope, model): assert False def get_input_signals(self, hdlscope: HDLScope): - return [sig for sig in hdlscope.get_signals({'input'})] + return [sig for sig in hdlscope.get_signals({"input"})] def get_output_signals(self, hdlscope: HDLScope): - return [sig for sig in hdlscope.get_signals({'output'})] + return [sig for sig in hdlscope.get_signals({"output"})] def _make_io_object_for_module(self, hdlscope: HDLScope, model): # add IO ports in_sigs = self.get_input_signals(hdlscope) out_sigs = self.get_output_signals(hdlscope) for sig in in_sigs: - if sig.name in ('clk', 'rst'): + if sig.name in ("clk", "rst"): continue input_value = Reg(0, sig.width, sig) iport = Port(None, None, None) @@ -853,15 +1048,15 @@ def _make_io_object_for_module(self, hdlscope: HDLScope, model): setattr(model, sig.name, oport) if isinstance(hdlscope, HDLModule): # add clk and rst - clksig = hdlscope.signal('clk') + clksig = hdlscope.signal("clk") assert isinstance(clksig, Signal) setattr(model, clksig.name, Reg(0, clksig.width, clksig)) - rstsig = hdlscope.signal('rst') + rstsig = hdlscope.signal("rst") assert isinstance(rstsig, Signal) setattr(model, rstsig.name, Reg(0, rstsig.width, rstsig)) def _make_io_object_for_function(self, hdlscope: HDLScope, model): - for sig in hdlscope.get_signals({'input', 'output'}): + for sig in hdlscope.get_signals({"input", "output"}): if sig.is_input(): assert sig.is_net() # Input is of type net, but generated as Reg for simulation @@ -884,6 +1079,7 @@ def find_origin_scope(scope): if scope.origin: return find_origin_scope(scope.origin) return scope + origin_scope = find_origin_scope(model.hdlmodule.scope) py_name = origin_scope.base_name if py_name in main_py_module.__dict__: @@ -913,17 +1109,17 @@ def convert_py_interface_to_hdl_interface(self): def _add_function_call(self, hdlscope: HDLScope, model): def _funcall(model, fn_name, args): - ready = getattr(model, f'{fn_name}_ready') - valid = getattr(model, f'{fn_name}_valid') - accept = getattr(model, f'{fn_name}_accept') - if hasattr(model, f'{fn_name}_out_0'): - out = getattr(model, f'{fn_name}_out_0') + ready = getattr(model, f"{fn_name}_ready") + valid = getattr(model, f"{fn_name}_valid") + accept = getattr(model, f"{fn_name}_accept") + if hasattr(model, f"{fn_name}_out_0"): + out = getattr(model, f"{fn_name}_out_0") else: out = None ready.set(1) for name, value in args: - i = getattr(model, f'{fn_name}_in_{name}') + i = getattr(model, f"{fn_name}_in_{name}") i.set(value) clkfence() @@ -950,11 +1146,11 @@ def call_body(*args, **kwargs): arg_and_names.append((param_names[i], v)) for k, v in kwargs.items(): arg_and_names.append((k, v)) - for param_name, defval in list(zip(param_names, default_values))[len(arg_and_names):]: + for param_name, defval in list(zip(param_names, default_values))[len(arg_and_names) :]: arg_and_names.append((param_name, defval.value)) return _funcall(model, hdlscope.name, arg_and_names) - setattr(model, '_call_body', call_body) + setattr(model, "_call_body", call_body) def _make_rom_function(self, hdlmodule: HDLModule, model): for fn in hdlmodule.functions: diff --git a/polyphony/typing.py b/polyphony/typing.py index 061ba12..268173b 100644 --- a/polyphony/typing.py +++ b/polyphony/typing.py @@ -24,7 +24,7 @@ class GenericMeta(abc.ABCMeta): def __getitem__(self, i): - return self.__class__(self.__name__, self.__bases__, dict(self.__dict__)) + return self.__class_getitem__(i) class Int(int, metaclass=GenericMeta): @@ -32,7 +32,21 @@ class Int(int, metaclass=GenericMeta): class List(list, metaclass=GenericMeta): - pass + @classmethod + def __class_getitem__(cls, param): + if not hasattr(cls, 'list_type'): + if not isinstance(param, type): + raise TypeError("List type parameter must be a type") + return type(f"List[{param}]", (cls,), { + "list_type": param + }) + else: + if not isinstance(param, int): + raise TypeError("List capacity parameter must be an integer") + return type(f"List[{cls.list_type}][{param}]", (cls,), { + "list_type": cls.list_type, + "list_capacity": param + }) class Tuple(tuple, metaclass=GenericMeta): @@ -43,6 +57,18 @@ class Type(type, metaclass=GenericMeta): pass +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from typing import Any + from typing import TypeAlias + # Polyphony uses custom subscript syntax (e.g. Tuple[int12], List[int8][16]) + # that is not compatible with Python's standard type system. + Tuple: TypeAlias = Any # type: ignore[no-redef] + List: TypeAlias = Any # type: ignore[no-redef] + Int: TypeAlias = Any # type: ignore[no-redef] + Type: TypeAlias = Any # type: ignore[no-redef] + + class int_base: base_type = int diff --git a/polyphony/version.py b/polyphony/version.py index abeeedb..f0ede3d 100644 --- a/polyphony/version.py +++ b/polyphony/version.py @@ -1 +1 @@ -__version__ = '0.4.0' +__version__ = '0.4.1' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a17026c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "polyphony" +version = "0.4.1" +description = "Python based High Level Synthesis compiler" +readme = "README.md" +license = { text = "MIT" } +authors = [{ name = "Hiroaki Kataoka", email = "ktok07b6@gmail.com" }] +keywords = ["HLS", "High Level Synthesis", "FPGA", "HDL", "Verilog"] +requires-python = ">=3.12" +dependencies = [] + +[project.scripts] +polyphony = "polyphony.compiler.__main__:main" + +[project.urls] +Homepage = "https://github.com/ktok07b6/polyphony" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "ruff", + "pytest", +] + +[tool.ruff] +line-length = 120 +target-version = "py312" + +[tool.ruff.lint] +ignore = ["E221", "E231", "E241", "E265", "F403", "F405"] + +[tool.ruff.format] +quote-style = "double" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/setup.py b/setup.py deleted file mode 100644 index d108b18..0000000 --- a/setup.py +++ /dev/null @@ -1,17 +0,0 @@ -import setuptools -from polyphony import __version__ - -setuptools.setup( - name='polyphony', - version=__version__, - packages=setuptools.find_packages(), - author="Hiroaki Kataoka", - author_email="kataoka@sinby.com", - description="Python based High Level Synthesis compiler", - long_description=open('README.rst').read(), - keywords="HLS High Level Synthesis FPGA HDL Verilog VHDL EDA", - license='MIT', - entry_points={'console_scripts':['polyphony=polyphony.compiler.__main__:main'],}, - url='https://github.com/ktok07b6/polyphony', -) -# scripts=['bin/polyphony'], \ No newline at end of file diff --git a/simu.py b/simu.py index bb8fc44..d0ca6f3 100755 --- a/simu.py +++ b/simu.py @@ -18,7 +18,7 @@ from polyphony.compiler.common.common import read_source from polyphony.compiler.__main__ import setup, compile_plan, output_hdl, output_plan from polyphony.compiler.__main__ import compile as compile_polyphony -from polyphony.simulator import Simulator, SimulationModelBuilder, HDLAssertionError +from polyphony.simulator import Simulator, SimulationModelBuilder, HDLAssertionError, SimulationObserver, Model, Net, Reg, Port def parse_options(): if not os.path.exists(TMP_DIR): @@ -39,6 +39,8 @@ def parse_options(): action='store_true', default=False, help='enable HDL debug mode') parser.add_argument('-p', dest='with_path_name', action='store_true') parser.add_argument('-t', '--targets', nargs='+', dest='targets', default=list()) + parser.add_argument('--watch', dest='watch_signals', default='', + help='comma-separated signal names to watch (e.g. "m.i,m.o")') parser.add_argument('source', help='Python source file') return parser.parse_args() @@ -103,6 +105,7 @@ def setup_compiler(casefile_path, casename, simu_options): compiler_options.hdl_debug_mode = simu_options.hdl_debug_mode compiler_options.verilog_dump = simu_options.verilog_dump compiler_options.verilog_monitor = simu_options.verilog_monitor + compiler_options.watch_signals = getattr(simu_options, 'watch_signals', '') setup(casefile_path, compiler_options) return compiler_options @@ -171,7 +174,7 @@ def model_selector_with_argv(models): def model_selector(*args, **kwargs): args_str = [] for a in args: - if type(a).__name__ == 'type': + if type(a).__name__ == 'type' or inspect.isfunction(a): args_str.append(a.__name__) else: args_str.append(str(a)) @@ -236,6 +239,88 @@ def model_selector(*args, **kwargs): from io import StringIO import contextlib + +def resolve_watch_signals(simulator, watch_str): + """Resolve --watch signal names to (name, value) pairs. + + Uses the name table built from the core model tree. Signal names like "m.i" + are matched by trying: + 1. Direct lookup by name in the name table (hierarchical match) + 2. Suffix match: look for any signal ending with the signal's leaf name, + where the prefix matches a subscope or instance name + """ + if not watch_str: + return + from polyphony.simulator import _build_name_table + name_table = _build_name_table(simulator) + # Reverse: name -> (id, value) + reverse_table = {} + for core_model in simulator.models: + _build_reverse_table(core_model, reverse_table) + signal_names = [s.strip() for s in watch_str.split(',')] + for sig_name in signal_names: + # Try exact match in name table values + found = False + for val_id, hier_name in name_table.items(): + if hier_name == sig_name: + value = reverse_table.get(val_id) + if value is not None: + simulator.observer.add_watch(sig_name, value) + found = True + break + if found: + continue + # Walk the dot-separated path on each core model + parts = sig_name.split('.') + # Skip first part (instance variable name in testbench, e.g. "s") + attr_path = parts[1:] if len(parts) > 1 else parts + for core_model in simulator.models: + obj = _walk_model_path(core_model, attr_path) + if obj is not None: + if isinstance(obj, Port): + simulator.observer.add_watch(sig_name, obj.value) + found = True + elif isinstance(obj, (Net, Reg)): + simulator.observer.add_watch(sig_name, obj) + found = True + break + if not found: + print(f"Warning: signal '{sig_name}' not found, skipping") + + +def _walk_model_path(core_model, path_parts): + """Walk a dot-path like ['i', 'tvalid'] on the core model tree. + + Intermediate Model objects are unwrapped to their core model. + Returns the final attribute (Port/Net/Reg) or None. + """ + obj = core_model + for part in path_parts: + attr = getattr(obj, part, None) + if attr is None: + return None + if isinstance(attr, Model): + obj = super(Model, attr).__getattribute__("__model") + else: + return attr + return None + + +def _build_reverse_table(core_model, table): + """Build {id(value): value} mapping for all signals in the model tree.""" + for attr_name, val in vars(core_model).items(): + if attr_name.startswith('_') or attr_name in ('hdlmodule', 'clk', 'rst'): + continue + if isinstance(val, (Net, Reg)): + table[id(val)] = val + elif isinstance(val, Port): + if val.value is not None: + table[id(val.value)] = val.value + elif isinstance(val, Model): + sub_core = super(Model, val).__getattribute__("__model") + _build_reverse_table(sub_core, table) + + def simulate_on_python(casefile_path, source_text, scopes, simu_options): finishes = [] casename = case_name_from_path(casefile_path) @@ -251,7 +336,9 @@ def simulate_on_python(casefile_path, source_text, scopes, simu_options): return finishes py_objects = [] for key, value in vars(main_py_module).items(): - if key.startswith(casename): + if inspect.isclass(value) and getattr(value, '_is_module', False): + py_objects.append(value) + elif inspect.isfunction(value) and key.startswith(casename): py_objects.append(value) for testbench in env.testbenches: @@ -277,7 +364,17 @@ def simulate_on_python(casefile_path, source_text, scopes, simu_options): try: simulate_models = [model for model, _ in models.values()] test._orig_func._execute_on_simu = True - with Simulator(simulate_models): + simulator = Simulator(simulate_models) + simulator.case_name = casename + watch_signals = getattr(simu_options, 'watch_signals', '') + if simu_options.verilog_dump or watch_signals: + vcd_path = f"{TMP_DIR}{os.sep}{casename}_py.vcd" + log_path = f"{TMP_DIR}{os.sep}{casename}_py.log" + simulator.observer = SimulationObserver(vcd_file=vcd_path, log_file=log_path) + with simulator: + if watch_signals and simulator.observer: + resolve_watch_signals(simulator, watch_signals) + simulator.observer.dump_initial(simulator.clock_time) test() finishes.append('OK') except HDLAssertionError as e: # from hdlmodule code diff --git a/suite.py b/suite.py index bee4e37..14e007a 100755 --- a/suite.py +++ b/suite.py @@ -7,8 +7,11 @@ import error import json import multiprocessing as mp +import time from pprint import pprint +TEST_TIMEOUT = 120 # seconds per test case + ROOT_DIR = './' TEST_DIR = ROOT_DIR + 'tests' @@ -60,7 +63,15 @@ 'config': '{ "perfect_inlining": false }', "ignores": ( 'pure/*', - 'module/*', + 'module/module01.py', 'module/module02.py', + 'module/module03.py', 'module/module03.new.py', + 'module/module07.py', 'module/module08.py', + 'module/module09.py', 'module/module10.py', + 'module/module11.py', 'module/module12.py', + 'module/field01.py', 'module/field02.py', 'module/field03.py', + 'module/nesting01.py', 'module/nesting02.py', + 'module/nesting03.py', 'module/nesting04.py', + 'module/parameter01.py', 'unroll/pipelined_unroll01.py', 'chstone/mips/pipelined_mips.py', 'error/pure01.py', 'error/pure02.py', @@ -77,17 +88,17 @@ def parse_options(): if not os.path.exists(TMP_DIR): os.mkdir(TMP_DIR) - parser = argparse.ArgumentParser(prog='suite') - parser.add_argument('-c', dest='compile_only', action='store_true') - parser.add_argument('-e', dest='error_test_only', action='store_true') - parser.add_argument('-w', dest='warn_test_only', action='store_true') - parser.add_argument('-j', dest='show_json', action='store_true') - parser.add_argument('-s', dest='silent', action='store_true') - parser.add_argument('-f', dest='full', action='store_true') - parser.add_argument('-n', '--num_cpu', dest='ncpu', type=int, default=1) + parser = argparse.ArgumentParser(prog='suite', description='Run the Polyphony test suite') + parser.add_argument('-c', dest='compile_only', action='store_true', help='compile only, skip simulation') + parser.add_argument('-e', dest='error_test_only', action='store_true', help='run error tests only (tests/error/)') + parser.add_argument('-w', dest='warn_test_only', action='store_true', help='run warning tests only (tests/warning/)') + parser.add_argument('-j', dest='show_json', action='store_true', help='display results as JSON') + parser.add_argument('-s', dest='silent', action='store_true', help='suppress output') + parser.add_argument('-f', dest='full', action='store_true', help='run with all config patterns') + parser.add_argument('-n', '--num_cpu', dest='ncpu', type=int, default=1, help='number of parallel processes (default: 1)') parser.add_argument('-P', '--python', dest='enable_python', action='store_true', default=False, help='enable python simulation') - parser.add_argument('dir', nargs='*') + parser.add_argument('dir', nargs='*', help='target test directories (all if omitted)') return parser.parse_args() @@ -101,14 +112,18 @@ def add_files(lst, patterns): def exec_test_entry(t, options, suite_results): if not options.silent: print(t) + start_time = time.time() try: hdl_finishes, py_finishes = simu.exec_test(t, options) if options.enable_python: - suite_results[t] = f'HDL Result: {','.join(hdl_finishes)} Python Result: {",".join(py_finishes)}' + suite_results[t] = f'HDL Result: {",".join(hdl_finishes)} Python Result: {",".join(py_finishes)}' else: suite_results[t] = f'{",".join(hdl_finishes)}' except Exception as e: suite_results[t] = 'Internal Error' + elapsed = time.time() - start_time + if elapsed > 30: + print(f'WARNING: {t} took {elapsed:.1f}s') def suite(options, ignores): tests = [] @@ -126,9 +141,21 @@ def suite(options, ignores): if t in tests: tests.remove(t) fails = 0 + async_results = [] for t in tests: - pool.apply_async(exec_test_entry, args=(t, options, suite_results)) + r = pool.apply_async(exec_test_entry, args=(t, options, suite_results)) + async_results.append((t, r)) pool.close() + for t, r in async_results: + try: + r.get(timeout=TEST_TIMEOUT) + except mp.TimeoutError: + print(f'TIMEOUT: {t} exceeded {TEST_TIMEOUT}s - skipping') + suite_results[t] = 'Timeout' + except Exception as e: + print(f'ERROR: {t} raised {e}') + suite_results[t] = 'Internal Error' + pool.terminate() pool.join() suite_results = dict(suite_results) fails = sum([res == 'FAIL' for res in suite_results.values()]) @@ -165,9 +192,19 @@ def abnormal_test(tests, proc, options, ignores): if t in tests: tests.remove(t) fails = 0 + async_results = [] for t in tests: - pool.apply_async(exec_abnormal_test_entry, args=(proc, t, options, error_results)) + r = pool.apply_async(exec_abnormal_test_entry, args=(proc, t, options, error_results)) + async_results.append((t, r)) pool.close() + for t, r in async_results: + try: + r.get(timeout=TEST_TIMEOUT) + except mp.TimeoutError: + print(f'TIMEOUT: {t} exceeded {TEST_TIMEOUT}s - skipping') + except Exception as e: + print(f'ERROR: {t} raised {e}') + pool.terminate() pool.join() fails = sum(error_results.values()) return fails diff --git a/tests/io/fifo01.py b/tests/io/fifo01.py new file mode 100644 index 0000000..9cafd27 --- /dev/null +++ b/tests/io/fifo01.py @@ -0,0 +1,93 @@ +from polyphony import testbench, module +from polyphony.io import connect, Port +from polyphony.timing import timed, wait_value, clkfence +from polyphony.typing import int8 + + + +@module +@timed +class Fifo: + def __init__(self, dtype, capacity): + self.read = Port(bool, 'in') + self.dout = Port(dtype, 'out') + self.empty = Port(bool, 'out') + self.outv = 0 + + self.write = Port(bool, 'in') + self.din = Port(dtype, 'in') + self.full = Port(bool, 'out') + + self.append_worker(self.worker, loop=True) + self.append_worker(self.update_flag, loop=True) + self.length = capacity + + self.mem = [0] * capacity + self.wp = 0 + self.rp = 0 + self._empty = 1 + self._full = 0 + + self.dout.assign(lambda:self.mem[self.rp]) + self.empty.assign(lambda:self._empty == 1) + self.full.assign(lambda:self._full == 1) + + def rd(self): + wait_value(False, self.empty) + self.read.wr(True) + clkfence() + self.read.wr(False) + self.outv = self.dout.rd() + clkfence() + return self.outv + + def wr(self, v): + wait_value(False, self.full) + self.write.wr(True) + self.din.wr(v) + clkfence() + self.write.wr(False) + + def _inc_rp(self): + self.rp = 0 if self.rp == (self.length - 1) else self.rp + 1 + + def _inc_wp(self): + self.wp = 0 if self.wp == (self.length - 1) else self.wp + 1 + + def worker(self): + if self.read.rd() and not self._empty: + self._inc_rp() + if self.write.rd() and not self._full: + self._inc_wp() + self.mem[self.wp] = self.din.rd() + + def update_flag(self): + if (self.write.rd() + and not self._full + and self.wp + 1 == self.rp): + self._full = 1 + elif self._full and self.wp == self.rp: + self._full = 1 + else: + self._full = 0 + if (self.read.rd() + and not self._empty + and self.rp + 1 == self.wp): + self._empty = 1 + elif self._empty and self.wp == self.rp: + self._empty = 1 + else: + self._empty = 0 +@timed +@testbench +def test(): + f = Fifo(int8, 4) + f.wr(1) + f.wr(2) + f.wr(3) + f.wr(4) + assert 1 == f.rd() + assert 2 == f.rd() + assert 3 == f.rd() + assert 4 == f.rd() + print('test passed') diff --git a/tests/io/port_edge02.py b/tests/io/port_edge02.py index ce07a80..22ade41 100644 --- a/tests/io/port_edge02.py +++ b/tests/io/port_edge02.py @@ -7,9 +7,9 @@ @module class port_edge02: def __init__(self): - self.sub_clk = Port(bit, 'out', 0) - self.sub_clk_posedge = Port(bit, 'out', 0) - self.sub_clk_posedge.assign(lambda:self.sub_clk.edge(0, 1)) + self.sub_clk = Port(bit, "out", 0) + self.sub_clk_posedge = Port(bit, "out", 0) + self.sub_clk_posedge.assign(lambda: self.sub_clk.edge(0, 1)) self.append_worker(self.clk_divider) @timed @@ -25,6 +25,7 @@ def clk_divider(self): def test(): m = port_edge02() for i in clkrange(15): + print(clktime()) wait_rising(m.sub_clk) print(clktime()) assert clktime() % 2 == 0 diff --git a/tests/module/ctor_func01.py b/tests/module/ctor_func01.py new file mode 100644 index 0000000..2dc945e --- /dev/null +++ b/tests/module/ctor_func01.py @@ -0,0 +1,24 @@ +from polyphony import module, testbench +from polyphony.io import Port +from polyphony.timing import wait_value + + +def compute(x): + return x * 2 + + +@module +class CtorFunc: + def __init__(self, fn): + self.o = Port(int, 'out') + self.append_worker(self.work, fn) + + def work(self, fn): + self.o.wr(fn(21)) + + +@testbench +def test0(): + m = CtorFunc(compute) + wait_value(42, m.o) + assert m.o.rd() == 42 diff --git a/tests/module/ctor_global01.py b/tests/module/ctor_global01.py new file mode 100644 index 0000000..891901a --- /dev/null +++ b/tests/module/ctor_global01.py @@ -0,0 +1,23 @@ +from polyphony import module, testbench +from polyphony.io import Port +from polyphony.timing import wait_value + +SCALE = 5 +OFFSET = 3 + + +@module +class CtorGlobal: + def __init__(self, factor, base): + self.o = Port(int, 'out') + self.append_worker(self.work, factor, base) + + def work(self, factor, base): + self.o.wr(6 * factor + base) + + +@testbench +def test0(): + m = CtorGlobal(SCALE, OFFSET) + wait_value(33, m.o) + assert m.o.rd() == 33 diff --git a/tests/module/ctor_multi01.py b/tests/module/ctor_multi01.py new file mode 100644 index 0000000..0ec954b --- /dev/null +++ b/tests/module/ctor_multi01.py @@ -0,0 +1,32 @@ +from polyphony import module, testbench +from polyphony.io import Port +from polyphony.timing import wait_value + +FACTOR = 3 + + +@module +class CtorMulti: + def __init__(self, base, scale, offsets): + self.o = Port(int, 'out') + self.append_worker(self.work, base, scale, offsets) + + def work(self, base, scale, offsets): + a, b = offsets + self.o.wr(base * scale + a + b) + + +@testbench +def test0(): + # literal + global + tuple + m = CtorMulti(10, FACTOR, (1, 2)) + wait_value(33, m.o) + assert m.o.rd() == 33 + + +@testbench +def test1(): + # different instances with different values + m = CtorMulti(5, 2, (10, 20)) + wait_value(40, m.o) + assert m.o.rd() == 40 diff --git a/tests/module/ctor_tuple01.py b/tests/module/ctor_tuple01.py new file mode 100644 index 0000000..8ecf4af --- /dev/null +++ b/tests/module/ctor_tuple01.py @@ -0,0 +1,21 @@ +from polyphony import module, testbench +from polyphony.io import Port +from polyphony.timing import wait_value + + +@module +class CtorTuple: + def __init__(self, params): + self.o = Port(int, 'out') + self.append_worker(self.work, params) + + def work(self, params): + a, b = params + self.o.wr(a + b) + + +@testbench +def test0(): + m = CtorTuple((10, 20)) + wait_value(30, m.o) + assert m.o.rd() == 30 diff --git a/tests/module/ctor_type01.py b/tests/module/ctor_type01.py new file mode 100644 index 0000000..76245cf --- /dev/null +++ b/tests/module/ctor_type01.py @@ -0,0 +1,28 @@ +from polyphony import module, testbench +from polyphony.io import Port +from polyphony.timing import wait_value +from polyphony.typing import int8, uint16 + + +@module +class CtorType: + def __init__(self, t): + self.o = Port(t, 'out') + self.append_worker(self.work) + + def work(self): + self.o.wr(42) + + +@testbench +def test_int8(): + m = CtorType(int8) + wait_value(42, m.o) + assert m.o.rd() == 42 + + +@testbench +def test_uint16(): + m = CtorType(uint16) + wait_value(42, m.o) + assert m.o.rd() == 42 diff --git a/tests/timed/timed_reg_test.py b/tests/timed/timed_reg_test.py new file mode 100644 index 0000000..23b678b --- /dev/null +++ b/tests/timed/timed_reg_test.py @@ -0,0 +1,40 @@ +from polyphony import module +from polyphony import testbench +from polyphony.io import Port +from polyphony.timing import timed, clkfence + + +@timed +@module +class timed_reg_test: + def __init__(self): + self.i = Port(int, 'in') + self.o = Port(int, 'out', 0) + self.append_worker(self.w) + + def w(self): + clkfence() + x = self.i.rd() + clkfence() + y = x + 10 + self.o.wr(y) + clkfence() + self.o.wr(x + 20) + clkfence() + + +@timed +@testbench +def test(): + m = timed_reg_test() + m.i.wr(5) + clkfence() + m.i.wr(99) + clkfence() + clkfence() + x = m.o.rd() + assert x == 15 + clkfence() + x = m.o.rd() + assert x == 25 + clkfence() diff --git a/tests/timed/watch_test.py b/tests/timed/watch_test.py new file mode 100644 index 0000000..de68586 --- /dev/null +++ b/tests/timed/watch_test.py @@ -0,0 +1,35 @@ +from polyphony import testbench, module +from polyphony.io import Port +from polyphony.typing import bit8 +from polyphony.timing import timed, clkfence + + +@module +class watch_test: + def __init__(self): + self.i = Port(bit8, 'in') + self.o = Port(bit8, 'out', 0) + self.append_worker(self.main) + + @timed + def main(self): + clkfence() + v = self.i.rd() + clkfence() + self.o.wr(v + 10) + clkfence() + + +# watch() test: run manually with `python simu.py -P tests/timed/watch_test.py` +# and add `from polyphony.simulator import watch; watch(m.i, m.o)` after instantiation + +@timed +@testbench +def test(): + m = watch_test() + m.i.wr(5) + clkfence() + clkfence() + clkfence() + x = m.o.rd() + assert x == 15 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..0cb7951 --- /dev/null +++ b/uv.lock @@ -0,0 +1,107 @@ +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "polyphony" +version = "0.4.0" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 }, +] + +[[package]] +name = "ruff" +version = "0.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333 }, + { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356 }, + { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434 }, + { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456 }, + { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772 }, + { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051 }, + { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494 }, + { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221 }, + { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459 }, + { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366 }, + { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887 }, + { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939 }, + { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471 }, + { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382 }, + { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664 }, + { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048 }, + { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776 }, +]