Skip to content

Commit

Permalink
Improved error handling (#165)
Browse files Browse the repository at this point in the history
- Type inference now more efficient (will only look at first value if
generator is not used)
- Better exceptions
- Hopefully removes matplotlib dependency
  • Loading branch information
WorldofKerry authored Oct 12, 2023
1 parent c5f72bf commit 7a49690
Show file tree
Hide file tree
Showing 14 changed files with 205 additions and 112 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/packaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Install package
run: |
mkdir build -p && cd build && python3 -m pip install .. && cd ..
bash ./build.sh
- name: Install requirements for examples
working-directory: examples
Expand Down
2 changes: 2 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#! /bin/bash
mkdir build -p && cd build && python3 -m pip install .. && cd ..
2 changes: 1 addition & 1 deletion examples/notebook.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
],
"source": [
"from python2verilog import verilogify, namespace_to_verilog, get_namespace\n",
"from python2verilog.utils import make_visual\n",
"from python2verilog.utils.visualization import make_visual\n",
"\n",
"ns = get_namespace(\"./notebook\")\n",
"\n",
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "python2verilog"
version = "0.2.8"
version = "0.2.9"
authors = [{ name = "Kerry Wang", email = "[email protected]" }]
description = "Converts a subset of python generator functions into synthesizable sequential SystemVerilog"
readme = "README.md"
Expand All @@ -14,6 +14,9 @@ classifiers = [
"Homepage" = "https://github.com/WorldofKerry/Python2Verilog/"
"Bug Tracker" = "https://github.com/WorldofKerry/Python2Verilog/issues"

[build-system]
requires = ["setuptools", "typing-extensions"]

[project.optional-dependencies]
full = ["matplotlib", "numpy", "dash_cytoscape", "dash"]

Expand Down
62 changes: 51 additions & 11 deletions python2verilog/api/verilogify.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
import inspect
import logging
import textwrap
import warnings
from functools import wraps
from types import FunctionType
from typing import Iterator, Optional, Union
from typing import Iterator, Optional, Union, cast

import __main__ as main

Expand Down Expand Up @@ -79,31 +80,70 @@ def verilogify(
def generator_wrapper(*args, **kwargs):
nonlocal context
if kwargs:
raise RuntimeError(
warnings.warn(
"Keyword arguments not yet supported, use positional arguments only"
)
for arg in args:
assert guard(arg, int)

context.test_cases.append(args)

# Input inference
if not context.input_types:
context.input_types = [type(arg) for arg in args]
else:
context.check_input_types(args)

for result in func(*args, **kwargs):
if not isinstance(result, tuple):
result = (result,)

def tuplefy(either: Union[int, tuple[int]]) -> tuple[int]:
"""
Converts int to tuple, otherwise returns input
"""
if isinstance(either, int):
ret = (either,)
else:
ret = either

for value in ret:
try:
assert guard(value, int)
except Exception as e:
raise TypeError("Expected `int` type inputs and outputs") from e
return ret

# Always get output one-ahead of what func user sees
# For output type inference even if user doesn't use generator
instance = func(*args)
try:
result = cast(Union[int, tuple[int]], next(instance))
tupled_result = tuplefy(result)
if not context.output_types:
logging.info(
"Using input `%s` as reference for %s's I/O types",
result,
tupled_result,
func.__name__,
)
context.output_types = [type(arg) for arg in result]
context.output_types = [type(arg) for arg in tupled_result]
context.default_output_vars()
else:
context.check_output_types(result)

return func(*args, **kwargs)
context.check_output_types(tupled_result)
except StopIteration:
pass
except Exception as e:
raise e

@wraps(func)
def inside():
nonlocal result
for i, new_result in enumerate(instance):
tupled_new_result = tuplefy(new_result)
context.check_output_types(tupled_new_result)
yield result
result = new_result
if i > 10000:
raise RuntimeError(f"{func.__name__} yields more than 10000 values")
yield result

return inside()

@wraps(func)
def function_wrapper(*_0, **_1):
Expand Down
43 changes: 43 additions & 0 deletions python2verilog/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Exceptions
"""
import ast


class UnknownValueError(Exception):
"""
An unexpected 'x' or 'z' was encountered in simulation
"""


class UnsupportedSyntaxError(Exception):
"""
Python syntax was not within the supported subset
"""

def __init__(self, *args: object) -> None:
super().__init__(
"Python syntax was not within the supported subset",
*args,
)

@classmethod
def from_pyast(cls, node: ast.AST):
"""
Based on AST error
"""
inst = cls(f"Unsupported Python syntax {ast.dump(node)}")
return inst


class TypeInferenceError(Exception):
"""
Type inferrence failed, either use the function in code or provide type hints
"""

def __init__(self, *args: object) -> None:
super().__init__(
"Input/output type inferrence failed, "
"either use the function in Python code or provide type hints",
*args,
)
64 changes: 30 additions & 34 deletions python2verilog/frontend/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing_extensions import TypeAlias

from python2verilog import ir
from python2verilog.exceptions import UnsupportedSyntaxError
from python2verilog.utils.typed import guard, typed_list, typed_strict


Expand All @@ -22,7 +23,6 @@ class GeneratorFunc:

def __init__(self, context: ir.Context) -> None:
self._context = copy.deepcopy(context)
print(self._context)

def create_root(self) -> tuple[ir.Node, ir.Context]:
"""
Expand Down Expand Up @@ -133,7 +133,7 @@ def _parse_stmt(
assert guard(nothing.child, ir.Edge)
continues.append(nothing.child)
return nothing, []
raise TypeError(f"Unparseable stmt {pyast.dump(stmt)}")
raise TypeError(f"Unexpected statement {pyast.dump(stmt)}")

def _parse_stmts(
self,
Expand Down Expand Up @@ -274,6 +274,7 @@ def _parse_for(self, stmt: pyast.For, prefix: str) -> ParseResult:
# pylint: disable=too-many-locals
breaks: list[ir.Edge] = []
continues: list[ir.Edge] = []
assert not stmt.orelse, "for-else statements not supported"
target = stmt.iter
assert isinstance(target, pyast.Name)
if target.id not in self._context.instances:
Expand Down Expand Up @@ -377,6 +378,7 @@ def create_capture_output_nodes():
return head, [to_ready_and_done, *breaks]

def _parse_while(self, whil: pyast.While, prefix: str) -> ParseResult:
assert not whil.orelse, "while-else statements not supported"
breaks: list[ir.Edge] = []
continues: list[ir.Edge] = []
body_head, ends = self._parse_stmts(
Expand Down Expand Up @@ -577,32 +579,30 @@ def _parse_expression(self, expr: pyast.AST) -> ir.Expression:
"""
<expression> (e.g. constant, name, subscript, etc., those that return a value)
"""
if isinstance(expr, pyast.Constant):
return ir.Int(expr.value)
if isinstance(expr, pyast.Name):
return ir.Var(py_name=expr.id)
if isinstance(expr, pyast.Subscript):
return self._parse_subscript(expr)
if isinstance(expr, pyast.BinOp):
return self._parse_binop(expr)
if isinstance(expr, pyast.UnaryOp):
if isinstance(expr.op, pyast.USub):
return ir.UnaryOp("-", self._parse_expression(expr.operand))
raise TypeError(
"Error: unexpected unaryop type", type(expr.op), pyast.dump(expr.op)
)
if isinstance(expr, pyast.Compare):
return self._parse_compare(expr)
if isinstance(expr, pyast.BoolOp):
if isinstance(expr.op, pyast.And):
return ir.UBinOp(
self._parse_expression(expr.values[0]),
"&&",
self._parse_expression(expr.values[1]),
)
raise TypeError(
"Error: unexpected expression type", type(expr), pyast.dump(expr)
)
try:
if isinstance(expr, pyast.Constant):
return ir.Int(expr.value)
if isinstance(expr, pyast.Name):
return ir.Var(py_name=expr.id)
if isinstance(expr, pyast.Subscript):
return self._parse_subscript(expr)
if isinstance(expr, pyast.BinOp):
return self._parse_binop(expr)
if isinstance(expr, pyast.UnaryOp):
if isinstance(expr.op, pyast.USub):
return ir.UnaryOp("-", self._parse_expression(expr.operand))
if isinstance(expr, pyast.Compare):
return self._parse_compare(expr)
if isinstance(expr, pyast.BoolOp):
if isinstance(expr.op, pyast.And):
return ir.UBinOp(
self._parse_expression(expr.values[0]),
"&&",
self._parse_expression(expr.values[1]),
)
except Exception as e:
raise UnsupportedSyntaxError.from_pyast(expr) from e
raise UnsupportedSyntaxError.from_pyast(expr)

def _parse_subscript(self, node: pyast.Subscript) -> ir.Expression:
"""
Expand Down Expand Up @@ -637,9 +637,7 @@ def _parse_compare(self, node: pyast.Compare) -> ir.UBinOp:
elif isinstance(node.ops[0], pyast.Eq):
operator = "==="
else:
raise TypeError(
"Error: unknown operator", type(node.ops[0]), pyast.dump(node.ops[0])
)
raise UnsupportedSyntaxError(f"Unknown operator {pyast.dump(node.ops[0])}")
return ir.UBinOp(
left=self._parse_expression(node.left),
oper=operator,
Expand Down Expand Up @@ -674,6 +672,4 @@ def _parse_binop(self, expr: pyast.BinOp) -> ir.Expression:
left = self._parse_expression(expr.left)
right = self._parse_expression(expr.right)
return ir.Mod(left, right)
raise TypeError(
"Error: unexpected binop type", type(expr.op), pyast.dump(expr.op)
)
raise UnsupportedSyntaxError(f"Unexpected binop type {pyast.dump(expr.op)}")
Loading

0 comments on commit 7a49690

Please sign in to comment.