PyVerilog AST 변환
PyVerilog의 ast가 생각보다 정규화(표준화) 되어 있지 않아서 예외처리할 것이 너무 많아.(e.g. port가 IO 포함인지, 본문에 써있는지 등등) 좀 더 표준화된 형식으로 쉽게 traverse할 수 있는 형태로 conversion 하는 기능을 개발하려고 해. 들어가야할 사항들: - port(순서, 포트명, width(msb, lsb), in/out/inout) - 내부 instance(submodule, port-argument mapping) - 내부 nets(width(msb, lsb))
좋아요—PyVerilog AST의 들쭉날쭉함(ANSI/Non-ANSI 포트 선언, 포지셔널/네임드 인스턴스, width 표현식 등) 때문에 예외가 많죠. 아래처럼 표준화된 중간 표현(IR)로 변환하는 파이프라인을 제안합니다. 바로 쓸 수 있는 참조 구현 코드 도 넣었어요.
목표
-
모듈 단위로 일관된 JSON IR 제공
-
포트: 순서(index), 이름, 방향, 폭(msb/lsb, 정수 평가 가능하면 width), signed 여부
-
내부 인스턴스: 서브모듈명, 인스턴스명, 파라미터 override, 포트-아규먼트 매핑(포지셔널→네임드 자동 치환)
-
내부 nets: wire/reg 등, 이름, 폭(msb/lsb, width)
설계 (스텝별)
- IR 스키마 정의
{
"modules": {
"<modname>": {
"parameters": [{"name":"WIDTH","value_str":"8","value":8}],
"ports": [
{"index":0,"name":"clk","dir":"input","signed":false,
"msb_str":"0","lsb_str":"0","msb":0,"lsb":0,"width":1}
],
"nets": [
{"name":"data","kind":"wire","signed":false,
"msb_str":"WIDTH-1","lsb_str":"0","msb":7,"lsb":0,"width":8}
],
"instances": [
{"inst":"u0","module":"child","param_overrides":[{"name":"WIDTH","value_str":"4","value":4}],
"connections":[{"port":"a","expr_str":"data[3:0]"},{"port":"b","expr_str":"clk"}]}
]
}
}
}
-
*_str는 원본 표현식 문자열,msb/lsb/width/value는 가능하면 정수 평가 , 실패 시null. -
포지셔널 연결은 서브모듈 포트 순서 를 이용해 네임드로 변환.
- 두 단계 수집
-
1패스 : 모든 모듈의 포트 순서/기본 파라미터 테이블 생성(포지셔널 매핑용).
-
2패스 : 각 모듈에 대해 포트/넷/인스턴스 표준화.
- 포트 정규화
-
ANSI(
Ioport)와 Non-ANSI(Port+본문Input/Output/Inout선언) 모두 흡수. -
폭 미지정은
1비트(msb=lsb=0)로 통일. 방향 미상은 보수적으로inout대신 발견 실패 시 null(현실적으로 거의 없음).
- 인스턴스 정규화
-
InstanceList를 재귀 탐색(Generate 블록 내부 포함). -
포지셔널 연결은 1패스에서 모은 서브모듈 포트 순서 로 네임드 변환. 알 수 없는 외부 모듈은 인덱스 기반으로 남김(예:
__pos0,__pos1).
- 넷 정규화
-
wire/reg(필요하면integer)를 수집, 다중 선언은 개별 항목으로 분할. -
포트 선언에서 이미 등장한 식별자는 nets에서 중복 제외(요구사항이 “내부 nets”이므로).
- 표현식 평가기
- 파라미터 환경을 사용해
IntConst/Identifier/UnaryOp/BinaryOp중심으로 안전한 정수 평가(미지원 연산 시 값은null, 원 문자열은 유지).
레퍼런스 구현 (Python / PyVerilog)
pip:
pyverilog필요. 파일 모음 입력 → 위 IR JSON 반환. ```pythonvnorm.py
from future import annotations from typing import Any, Dict, List, Tuple, Optional import json from pyverilog.vparser.parser import parse from pyverilog.vparser import ast as vast from pyverilog.ast_code_generator.codegen import ASTCodeGenerator
codegen = ASTCodeGenerator()
———- utilities ———-
def to_str(node) -> Optional[str]: if node is None: return None try: return codegen.visit(node) except Exception: return None
def int_from_intconst(intconst: str) -> Optional[int]: # Handles decimal/hex/bin/octal Verilog ints like 8'hFF or 'd10. s = intconst.replace('_', '') try: # forms: 10, 8'hFF, 'd3 if "'" not in s: return int(s, 10) width_base, val = s.split("'") base = val[0].lower() digits = val[1:] if base == 'd': return int(digits, 10) if base == 'h': return int(digits, 16) if base == 'b': return int(digits, 2) if base == 'o': return int(digits, 8) # unknown base return int(digits, 10) except Exception: return None
def eval_expr(node, env: Dict[str, int]) -> Optional[int]: # conservative evaluator for width-ish integer expressions if node is None: return None if isinstance(node, vast.IntConst): return int_from_intconst(node.value) if isinstance(node, vast.Identifier): return env.get(node.name) if isinstance(node, vast.UnaryOperator): v = eval_expr(node.children()[0], env) if v is None: return None op = node.class.name if op in ("Uplus",): return +v if op in ("Uminus",): return -v if op in ("Ulnot","Unot"): return 0 if v else 1 return None if isinstance(node, vast.Partselect) or isinstance(node, vast.Pointer): # treat as unknown integer return None if isinstance(node, vast.Concat) or isinstance(node, vast.Repeat): return None # Binary operators if isinstance(node, vast.BinaryOperator): a = eval_expr(node.left, env) b = eval_expr(node.right, env) if a is None or b is None: return None t = node.class.name try: if t == "Plus": return a + b if t == "Minus": return a - b if t == "Times": return a * b if t == "Div": return a // b if b != 0 else None if t == "Power": return a ** b if t == "Mod": return a % b if b != 0 else None if t == "Sll": return a « b if t == "Srl": return a » b if t == "And": return a & b if t == "Or": return a | b if t == "Xor": return a ^ b except Exception: return None return None return None
def width_tuple(width_node, env: Dict[str,int]): # returns (msb_str, lsb_str, msb_val, lsb_val, width_val) if width_node is None: return "0", "0", 0, 0, 1 msb_n, lsb_n = width_node.msb, width_node.lsb msb_s, lsb_s = to_str(msb_n), to_str(lsb_n) msb_v, lsb_v = eval_expr(msb_n, env), eval_expr(lsb_n, env) width_v = None if msb_v is not None and lsb_v is not None: width_v = abs(msb_v - lsb_v) + 1 return msb_s, lsb_s, msb_v, lsb_v, width_v
———- 1st pass: collect module port order & param defaults ———-
def collect_modules(ast_root) -> Dict[str, vast.ModuleDef]: mods = {} for d in ast_root.description.definitions: if isinstance(d, vast.ModuleDef): mods[d.name] = d return mods
def collect_param_env(mod: vast.ModuleDef) -> Dict[str,int]: env = {} # parameters in header if getattr(mod, "paramlist", None): for p in mod.paramlist.params: if isinstance(p, vast.Parameter): v = eval_expr(p.value, env) # parameters can depend on earlier ones if v is not None: env[p.name] = v # parameters in body declarations, if any for item in getattr(mod, "items", []) or []: if isinstance(item, vast.Decl): for e in item.list: if isinstance(e, vast.Parameter): v = eval_expr(e.value, env) if v is not None: env[e.name] = v return env
def ordered_port_names(mod: vast.ModuleDef) -> List[str]: names = [] if mod.portlist is None: return names for p in mod.portlist.ports: # Ioport(first=Identifier, second=Input/Output/Inout) if isinstance(p, vast.Ioport): names.append(p.first.name) elif isinstance(p, vast.Port): # Port(name, arg=None) in non-ANSI names.append(p.name) return names
———- helpers: declaration maps ———-
def build_io_decl_map(mod: vast.ModuleDef, env: Dict[str,int]): # name -> (dir, signed, width_info) io_map = {} for item in getattr(mod, "items", []) or []: if not isinstance(item, vast.Decl): continue for e in item.list: if isinstance(e, (vast.Input, vast.Output, vast.Inout)): dir_ = "input" if isinstance(e, vast.Input) else ("output" if isinstance(e, vast.Output) else "inout") signed = bool(getattr(e, "signed", False)) w = getattr(e, "width", None) msb_s, lsb_s, msb_v, lsb_v, width_v = width_tuple(w, env) for name in [n.name for n in e.children() if isinstance(n, vast.Identifier)]: io_map[name] = (dir_, signed, (msb_s, lsb_s, msb_v, lsb_v, width_v)) return io_map
def build_net_decl_list(mod: vast.ModuleDef, env: Dict[str,int]): nets = [] for item in getattr(mod, "items", []) or []: if not isinstance(item, vast.Decl): continue for e in item.list: if isinstance(e, (vast.Wire, vast.Reg, vast.Integer)): kind = "wire" if isinstance(e, vast.Wire) else ("reg" if isinstance(e, vast.Reg) else "integer") signed = bool(getattr(e, "signed", False)) w = getattr(e, "width", None) msb_s, lsb_s, msb_v, lsb_v, width_v = width_tuple(w, env) for n in e.children(): if isinstance(n, vast.Identifier): nets.append({ "name": n.name, "kind": kind, "signed": signed, "msb_str": msb_s, "lsb_str": lsb_s, "msb": msb_v, "lsb": lsb_v, "width": width_v }) return nets
———- recursive walker for InstanceList ———-
def iter_instancelists(node): # yield vast.InstanceList under any nesting (e.g., Generate blocks) if isinstance(node, vast.InstanceList): yield node for ch in getattr(node, "children", lambda: [])(): yield from iter_instancelists(ch)
———- 2nd pass: build IR per module ———-
def normalize_module(mod: vast.ModuleDef, modlib_ports: Dict[str, List[str]]) -> Dict[str,Any]: env = collect_param_env(mod) # parameters params = [] # header params if getattr(mod, "paramlist", None): for p in mod.paramlist.params: if isinstance(p, vast.Parameter): params.append({ "name": p.name, "value_str": to_str(p.value), "value": eval_expr(p.value, env) }) # body params (avoid duplicates) body_params = {} for item in getattr(mod, "items", []) or []: if isinstance(item, vast.Decl): for e in item.list: if isinstance(e, vast.Parameter) and e.name not in {x["name"] for x in params}: body_params[e.name] = { "name": e.name, "value_str": to_str(e.value), "value": eval_expr(e.value, env) } params.extend(body_params.values())
# ports
io_map = build_io_decl_map(mod, env)
ordered_names = ordered_port_names(mod)
ports = []
for idx, pname in enumerate(ordered_names):
dir_, signed, winfo = None, False, None
# ANSI Ioport case
ansi_hit = False
if mod.portlist and isinstance(mod.portlist.ports[idx], vast.Ioport):
io = mod.portlist.ports[idx]
decl = io.second # Input/Output/Inout
dir_ = "input" if isinstance(decl, vast.Input) else ("output" if isinstance(decl, vast.Output) else "inout")
signed = bool(getattr(decl, "signed", False))
w = getattr(decl, "width", None)
msb_s, lsb_s, msb_v, lsb_v, width_v = width_tuple(w, env)
winfo = (msb_s, lsb_s, msb_v, lsb_v, width_v)
ansi_hit = True
# Non-ANSI fallback via body decl map
if not ansi_hit and pname in io_map:
dir_, signed, winfo = io_map[pname]
if winfo is None:
winfo = width_tuple(None, env)
msb_s, lsb_s, msb_v, lsb_v, width_v = winfo
ports.append({
"index": idx, "name": pname, "dir": dir_, "signed": signed,
"msb_str": msb_s, "lsb_str": lsb_s, "msb": msb_v, "lsb": lsb_v, "width": width_v
})
# nets (exclude names that are top-level ports)
nets_raw = build_net_decl_list(mod, env)
port_names_set = set(ordered_names)
nets = [n for n in nets_raw if n["name"] not in port_names_set]
# instances
instances = []
for il in iter_instancelists(mod):
submod = il.module
# parameter overrides (common across instances in the list)
param_overrides = []
if getattr(il, "paramlist", None):
for pa in il.paramlist.params:
# pa: vast.ParamArg(name, argname/value)
name = pa.paramname if hasattr(pa, "paramname") else getattr(pa, "name", None)
value_node = pa.argname if hasattr(pa, "argname") else getattr(pa, "value", None)
param_overrides.append({
"name": name,
"value_str": to_str(value_node),
"value": eval_expr(value_node, env)
})
for inst in il.instances:
conns = []
# PortArg(portname, argname)
portargs = getattr(inst, "portlist", []) or []
# decide positional vs named
positional = any(getattr(pa, "portname", None) in (None, "") for pa in portargs)
if positional:
# map by index using lib
lib_ports = modlib_ports.get(submod, [])
for i, pa in enumerate(portargs):
pname = lib_ports[i] if i < len(lib_ports) else f"__pos{i}"
conns.append({"port": pname, "expr_str": to_str(getattr(pa, "argname", None))})
else:
for pa in portargs:
conns.append({"port": pa.portname, "expr_str": to_str(pa.argname)})
instances.append({
"inst": inst.name,
"module": submod,
"param_overrides": param_overrides,
"connections": conns
})
return {"parameters": params, "ports": ports, "nets": nets, "instances": instances}
def convert_to_vnorm(files: List[str]) -> Dict[str,Any]: ast, _ = parse(files) mods = collect_modules(ast) modlib_ports = {name: ordered_port_names(m) for name, m in mods.items()} result = {"modules": {}} for name, m in mods.items(): result["modules"][name] = normalize_module(m, modlib_ports) return result
if name == "main": import argparse, sys ap = argparse.ArgumentParser() ap.add_argument("verilog", nargs="+", help="Verilog source files") ap.add_argument("-o", "–out", default="-", help="Output JSON path (default: stdout)") args = ap.parse_args() ir = convert_to_vnorm(args.verilog) s = json.dumps(ir, indent=2) if args.out == "-" or args.out == "/dev/stdout": print(s) else: with open(args.out, "w") as f: f.write(s)
* * *
# 사용 예시
**예제 Verilog**
```verilog
module child #(parameter WIDTH=8) (input clk, input [WIDTH-1:0] a, output y);
assign y = a[0];
endmodule
module top (clk, rst, out);
input clk, rst;
output out;
wire [7:0] data;
child #(.WIDTH(8)) u0 (.clk(clk), .a(data), .y(out));
endmodule
실행
python vnorm.py top.v -o ir.json
핵심 결과(발췌)
{
"modules": {
"top": {
"ports": [
{"index":0,"name":"clk","dir":"input","width":1,...},
{"index":1,"name":"rst","dir":"input","width":1,...},
{"index":2,"name":"out","dir":"output","width":1,...}
],
"nets":[
{"name":"data","kind":"wire","msb_str":"7","lsb_str":"0","msb":7,"lsb":0,"width":8}
],
"instances":[
{"inst":"u0","module":"child",
"param_overrides":[{"name":"WIDTH","value_str":"8","value":8}],
"connections":[
{"port":"clk","expr_str":"clk"},
{"port":"a","expr_str":"data"},
{"port":"y","expr_str":"out"}
]}
]
}
}
}
구현 포인트 & 엣지케이스 처리
-
ANSI/Non-ANSI 포트 : 둘 다 커버. Non-ANSI는 본문
Decl탐색으로 방향/폭 채움. -
폭 미지정 :
msb=lsb=0,width=1로 통일. -
표현식 평가 실패 :
*_str만 보존하고 정수 필드는null. -
포지셔널 연결 : 1패스에서 수집한 서브모듈 포트 순서 로 네임드화.
-
Generate 내부 인스턴스 : 재귀 탐색으로 수집.
-
내부 nets : 포트 이름과 중복 제거.
-
파라미터 : 헤더/본문 모두 수집, 가능한 경우 정수 평가.
다음 확장 아이디어
-
SystemVerilog 일부 지원(가능 범위에서):
logic, packed/unpacked 배열, interface 포트는 별 레이어로. -
폭/파라미터 완전 평가 를 위해 PyVerilog Dataflow 연계.
-
기본 nettype(
default_nettype none미사용 시) 암시적 net 감지 옵션. -
소스 위치 정보(filename, lineno) 추가로 디버깅 편의성↑.
-
스키마 검증(pydantic/jsonschema)로 다운스트림 안정성 확보.
필요하시면 이 IR로부터 Graphviz 모듈-인스턴스 다이어그램 이나 포트/넷 테이블 뽑는 코드도 바로 붙여드릴게요.
JSON 말고 Python class로 변환하고 싶어
좋지! JSON 대신 Python 클래스(IR)로 바로 다루게 만들자.
아래는 (1) 타입 설계 → (2) 변환 파이프라인 → (3) 전체 구현 코드 → (4) 사용 예시 순서로 차근차근 정리했어.
1) 목표 재정의 (Step-by-step)
-
PyVerilog AST → 일관된 Python dataclass IR 반환
-
포함 정보
-
포트 : 순서(index), 이름, 방향(in/out/inout), signed, 폭(msb/lsb/width)
-
인스턴스 : 서브모듈명, 인스턴스명, 파라미터 override, 포트-아규먼트 매핑(포지셔널 → 네임드 자동 변환)
-
내부 nets : wire/reg/integer, signed, 폭(msb/lsb/width)
-
-
표현식은 가능한 한 정수 평가 , 실패하면 원문 문자열 유지
2) 타입 설계 (dataclass + Enum)
-
DesignIR(루트) → 모듈명 →ModuleIR -
ModuleIR안에Parameter,Port,Net,Instance(Connection[]) -
방향/넷 종류는 Enum으로 안정화
3) 변환 파이프라인 (두 패스, Step-by-step)
-
1패스 : 모든 모듈의
-
포트 순서 수집(포지셔널 매핑용)
-
파라미터 기본값 환경(env) 구성(폭 평가용)
-
-
2패스(모듈 단위) :
-
포트: ANSI/Non-ANSI 통합 정규화
-
넷: 포트와 중복 제거, 다중 선언 분리
-
인스턴스: Generate 내부 포함 재귀 수집, 포지셔널→네임드
-
4) 전체 구현 코드 (복붙해서 vnorm_classes.py 로 저장)
# vnorm_classes.py
from __future__ import annotations
from dataclasses import dataclass, field, asdict
from enum import Enum
from typing import Dict, List, Optional
import json
from pyverilog.vparser.parser import parse
from pyverilog.vparser import ast as vast
from pyverilog.ast_code_generator.codegen import ASTCodeGenerator
codegen = ASTCodeGenerator()
# -------------------- Typed IR --------------------
class Direction(Enum):
INPUT = "input"
OUTPUT = "output"
INOUT = "inout"
class NetKind(Enum):
WIRE = "wire"
REG = "reg"
INTEGER = "integer"
@dataclass
class Parameter:
name: str
value_str: Optional[str]
value: Optional[int]
@dataclass
class Port:
index: int
name: str
dir: Optional[Direction] # Non-ANSI에서 드물게 못찾으면 None
signed: bool
msb_str: Optional[str]
lsb_str: Optional[str]
msb: Optional[int]
lsb: Optional[int]
width: Optional[int] # msb/lsb 둘 다 평가되면 abs(msb-lsb)+1
@dataclass
class Net:
name: str
kind: NetKind
signed: bool
msb_str: Optional[str]
lsb_str: Optional[str]
msb: Optional[int]
lsb: Optional[int]
width: Optional[int]
@dataclass
class Connection:
port: str
expr_str: Optional[str]
@dataclass
class Instance:
inst: str
module: str
param_overrides: List[Parameter] = field(default_factory=list)
connections: List[Connection] = field(default_factory=list)
@dataclass
class ModuleIR:
parameters: List[Parameter] = field(default_factory=list)
ports: List[Port] = field(default_factory=list)
nets: List[Net] = field(default_factory=list)
instances: List[Instance] = field(default_factory=list)
# 편의 메서드
def port_by_name(self, name: str) -> Optional[Port]:
for p in self.ports:
if p.name == name:
return p
return None
def ports_by_dir(self, dir_: Direction) -> List[Port]:
return [p for p in self.ports if p.dir == dir_]
@dataclass
class DesignIR:
modules: Dict[str, ModuleIR] = field(default_factory=dict)
def to_json(self, indent: int = 2) -> str:
return json.dumps(asdict(self), indent=indent)
# -------------------- Helpers --------------------
def to_str(node) -> Optional[str]:
if node is None:
return None
try:
return codegen.visit(node)
except Exception:
return None
def int_from_intconst(intconst: str) -> Optional[int]:
s = intconst.replace('_', '')
try:
if "'" not in s:
return int(s, 10)
width_base, val = s.split("'")
base = val[0].lower()
digits = val[1:]
if base == 'd': return int(digits, 10)
if base == 'h': return int(digits, 16)
if base == 'b': return int(digits, 2)
if base == 'o': return int(digits, 8)
return int(digits, 10)
except Exception:
return None
def eval_expr(node, env: Dict[str, int]) -> Optional[int]:
if node is None:
return None
if isinstance(node, vast.IntConst):
return int_from_intconst(node.value)
if isinstance(node, vast.Identifier):
return env.get(node.name)
if isinstance(node, vast.UnaryOperator):
v = eval_expr(node.children()[0], env)
if v is None: return None
t = node.__class__.__name__
if t == "Uplus": return +v
if t == "Uminus": return -v
if t in ("Ulnot", "Unot"): return 0 if v else 1
return None
if isinstance(node, vast.BinaryOperator):
a = eval_expr(node.left, env)
b = eval_expr(node.right, env)
if a is None or b is None: return None
t = node.__class__.__name__
try:
if t == "Plus": return a + b
if t == "Minus": return a - b
if t == "Times": return a * b
if t == "Div": return a // b if b != 0 else None
if t == "Power": return a ** b
if t == "Mod": return a % b if b != 0 else None
if t == "Sll": return a << b
if t == "Srl": return a >> b
if t == "And": return a & b
if t == "Or": return a | b
if t == "Xor": return a ^ b
except Exception:
return None
return None
# 기타 복합표현(Concat/Repeat/Partselect/Pointer)은 평가 보류
return None
def width_tuple(width_node, env: Dict[str, int]):
# (msb_str, lsb_str, msb_val, lsb_val, width_val)
if width_node is None:
return "0", "0", 0, 0, 1
msb_n, lsb_n = width_node.msb, width_node.lsb
msb_s, lsb_s = to_str(msb_n), to_str(lsb_n)
msb_v, lsb_v = eval_expr(msb_n, env), eval_expr(lsb_n, env)
width_v = None
if msb_v is not None and lsb_v is not None:
width_v = abs(msb_v - lsb_v) + 1
return msb_s, lsb_s, msb_v, lsb_v, width_v
# -------------------- 1st pass: module tables --------------------
def collect_modules(ast_root) -> Dict[str, vast.ModuleDef]:
mods = {}
for d in ast_root.description.definitions:
if isinstance(d, vast.ModuleDef):
mods[d.name] = d
return mods
def collect_param_env(mod: vast.ModuleDef) -> Dict[str, int]:
env: Dict[str, int] = {}
# 헤더 파라미터
if getattr(mod, "paramlist", None):
for p in mod.paramlist.params:
if isinstance(p, vast.Parameter):
v = eval_expr(p.value, env)
if v is not None:
env[p.name] = v
# 바디 파라미터
for item in getattr(mod, "items", []) or []:
if isinstance(item, vast.Decl):
for e in item.list:
if isinstance(e, vast.Parameter):
v = eval_expr(e.value, env)
if v is not None:
env[e.name] = v
return env
def ordered_port_names(mod: vast.ModuleDef) -> List[str]:
names: List[str] = []
if mod.portlist is None:
return names
for p in mod.portlist.ports:
if isinstance(p, vast.Ioport):
names.append(p.first.name)
elif isinstance(p, vast.Port):
names.append(p.name)
return names
# -------------------- Decl scans --------------------
def build_io_decl_map(mod: vast.ModuleDef, env: Dict[str, int]):
# name -> (Direction|None, signed, width_info)
io_map = {}
for item in getattr(mod, "items", []) or []:
if not isinstance(item, vast.Decl):
continue
for e in item.list:
if isinstance(e, (vast.Input, vast.Output, vast.Inout)):
if isinstance(e, vast.Input): dir_ = Direction.INPUT
elif isinstance(e, vast.Output): dir_ = Direction.OUTPUT
else: dir_ = Direction.INOUT
signed = bool(getattr(e, "signed", False))
w = getattr(e, "width", None)
msb_s, lsb_s, msb_v, lsb_v, width_v = width_tuple(w, env)
# e.children() contains Identifiers
for n in e.children():
if isinstance(n, vast.Identifier):
io_map[n.name] = (dir_, signed, (msb_s, lsb_s, msb_v, lsb_v, width_v))
return io_map
def build_net_decl_list(mod: vast.ModuleDef, env: Dict[str, int]) -> List[Net]:
nets: List[Net] = []
for item in getattr(mod, "items", []) or []:
if not isinstance(item, vast.Decl):
continue
for e in item.list:
kind: Optional[NetKind] = None
if isinstance(e, vast.Wire): kind = NetKind.WIRE
elif isinstance(e, vast.Reg): kind = NetKind.REG
elif isinstance(e, vast.Integer): kind = NetKind.INTEGER
if kind is None:
continue
signed = bool(getattr(e, "signed", False))
w = getattr(e, "width", None)
msb_s, lsb_s, msb_v, lsb_v, width_v = width_tuple(w, env)
for n in e.children():
if isinstance(n, vast.Identifier):
nets.append(Net(
name=n.name, kind=kind, signed=signed,
msb_str=msb_s, lsb_str=lsb_s, msb=msb_v, lsb=lsb_v, width=width_v
))
return nets
# -------------------- Instance walker --------------------
def iter_instancelists(node):
if isinstance(node, vast.InstanceList):
yield node
for ch in getattr(node, "children", lambda: [])():
yield from iter_instancelists(ch)
# -------------------- 2nd pass: normalize module --------------------
def normalize_module(mod: vast.ModuleDef, modlib_ports: Dict[str, List[str]]) -> ModuleIR:
env = collect_param_env(mod)
# parameters
params: List[Parameter] = []
if getattr(mod, "paramlist", None):
for p in mod.paramlist.params:
if isinstance(p, vast.Parameter):
params.append(Parameter(
name=p.name,
value_str=to_str(p.value),
value=eval_expr(p.value, env),
))
# body params (dedupe)
header_names = {p.name for p in params}
for item in getattr(mod, "items", []) or []:
if isinstance(item, vast.Decl):
for e in item.list:
if isinstance(e, vast.Parameter) and e.name not in header_names:
params.append(Parameter(
name=e.name,
value_str=to_str(e.value),
value=eval_expr(e.value, env),
))
# ports
io_map = build_io_decl_map(mod, env)
ordered_names = ordered_port_names(mod)
ports: List[Port] = []
for idx, pname in enumerate(ordered_names):
dir_: Optional[Direction] = None
signed = False
msb_s = lsb_s = None
msb_v = lsb_v = width_v = None
ansi_hit = False
if mod.portlist and isinstance(mod.portlist.ports[idx], vast.Ioport):
io = mod.portlist.ports[idx]
decl = io.second
if isinstance(decl, vast.Input): dir_ = Direction.INPUT
elif isinstance(decl, vast.Output): dir_ = Direction.OUTPUT
elif isinstance(decl, vast.Inout): dir_ = Direction.INOUT
signed = bool(getattr(decl, "signed", False))
msb_s, lsb_s, msb_v, lsb_v, width_v = width_tuple(getattr(decl, "width", None), env)
ansi_hit = True
if not ansi_hit and pname in io_map:
dir_, signed, (msb_s, lsb_s, msb_v, lsb_v, width_v) = io_map[pname]
if msb_s is None and lsb_s is None and msb_v is None and lsb_v is None and width_v is None:
# width unspecified -> 1-bit
msb_s, lsb_s, msb_v, lsb_v, width_v = "0", "0", 0, 0, 1
ports.append(Port(
index=idx, name=pname, dir=dir_, signed=signed,
msb_str=msb_s, lsb_str=lsb_s, msb=msb_v, lsb=lsb_v, width=width_v
))
# nets (exclude top ports)
nets_raw = build_net_decl_list(mod, env)
port_names = {p.name for p in ports}
nets = [n for n in nets_raw if n.name not in port_names]
# instances
instances: List[Instance] = []
for il in iter_instancelists(mod):
submod = il.module
shared_param_ovrs: List[Parameter] = []
if getattr(il, "paramlist", None):
for pa in il.paramlist.params:
name = getattr(pa, "paramname", None) or getattr(pa, "name", None)
value_node = getattr(pa, "argname", None) or getattr(pa, "value", None)
shared_param_ovrs.append(Parameter(
name=name,
value_str=to_str(value_node),
value=eval_expr(value_node, env)
))
for inst in il.instances:
conns: List[Connection] = []
portargs = getattr(inst, "portlist", []) or []
positional = any(getattr(pa, "portname", None) in (None, "") for pa in portargs)
if positional:
lib_ports = modlib_ports.get(submod, [])
for i, pa in enumerate(portargs):
pname = lib_ports[i] if i < len(lib_ports) else f"__pos{i}"
conns.append(Connection(port=pname, expr_str=to_str(getattr(pa, "argname", None))))
else:
for pa in portargs:
conns.append(Connection(port=pa.portname, expr_str=to_str(pa.argname)))
instances.append(Instance(
inst=inst.name, module=submod,
param_overrides=list(shared_param_ovrs),
connections=conns
))
return ModuleIR(parameters=params, ports=ports, nets=nets, instances=instances)
# -------------------- Public API --------------------
def convert_to_design_ir(files: List[str]) -> DesignIR:
ast, _ = parse(files)
mods = collect_modules(ast)
modlib_ports = {name: ordered_port_names(m) for name, m in mods.items()}
design = DesignIR()
for name, m in mods.items():
design.modules[name] = normalize_module(m, modlib_ports)
return design
# CLI (optional)
if __name__ == "__main__":
import argparse, sys
ap = argparse.ArgumentParser()
ap.add_argument("verilog", nargs="+", help="Verilog sources")
ap.add_argument("-o", "--out", help="Dump as JSON (for debug)")
args = ap.parse_args()
ir = convert_to_design_ir(args.verilog)
if args.out:
with open(args.out, "w") as f:
f.write(ir.to_json())
else:
# Pretty print summary
for mname, m in ir.modules.items():
print(f"[module] {mname}")
for p in m.ports:
print(f" port[{p.index}] {p.dir and p.dir.value or 'None'} {p.name} [{p.msb_str}:{p.lsb_str}] -> width={p.width}")
for n in m.nets:
print(f" net {n.kind.value} {n.name} [{n.msb_str}:{n.lsb_str}] -> width={n.width}")
for i in m.instances:
print(f" inst {i.inst}:{i.module}")
for c in i.connections:
print(f" .{c.port}({c.expr_str})")
5) 사용 예시 (Step-by-step)
python vnorm_classes.py top.v # 요약 출력
python vnorm_classes.py top.v -o ir.json # 디버그용 JSON 덤프(옵션)
# 라이브러리처럼 사용
from vnorm_classes import convert_to_design_ir, Direction
ir = convert_to_design_ir(["top.v", "child.v"])
top = ir.modules["top"]
# 포트 순회
for p in top.ports:
print(p.index, p.name, p.dir and p.dir.value, p.width)
# 인스턴스/연결 순회
for inst in top.instances:
print("inst", inst.inst, ":", inst.module)
for c in inst.connections:
print(" ", c.port, "=>", c.expr_str)
# 필요 시 JSON 직렬화도 가능 (테스트/로깅용)
print(ir.to_json())
6) 주의/확장 (Step-by-step)
-
표현식 평가 실패 :
value/width/msb/lsb는None이 될 수 있음. 원문은*_str에 보존. -
포지셔널 연결 : 라이브러리에 해당 서브모듈이 없으면
__pos0처럼 보존. -
SystemVerilog 확장 :
logic, packed/unpacked, interface 포트 등은 별 레이어로 확장 추천. -
위치 정보 : 필요하면
lineno/coord를 dataclass에 추가 가능. -
검증 : 큰 설계에서 스키마 보장 원하면
pydantic으로 교체 가능.
원하는 대로 직접 탐색 가능한 Python 클래스 IR 로 변환되도록 구성해 두었어.
원하면 Connection에 좌표/소스파일 정보나, ModuleIR에 빠른 인덱스(예: port_dict)를 추가해주는 최적화 버전도 바로 만들어줄게.
IR이 뭐야
좋은 질문! IR(Intermediate Representation, 중간 표현)은 소스 코드(또는 설계)를 한층 표준화된 형태로 담아두는 중간 단계의 데이터 구조 예요. 파서가 만든 “문법 그대로”의 트리(AST)와, 그걸 써서 분석·변환·코드생성 같은 후처리 사이에 놓입니다.
아래처럼 차근차근 이해하면 편해요:
-
정의
- IR = 분석·최적화·변환을 하기 쉽도록 만든 표준화된 내부 데이터 모델.
-
왜 필요한가
-
언어 문법 차이/스타일(예: Verilog ANSI vs Non-ANSI 포트 선언)을 한 가지 규칙 으로 통일.
-
트래버스(순회) 쉬움 : 필요한 정보가 같은 자리/형태에 있음.
-
모듈성 : 프런트엔드(AST)와 백엔드(분석/생성)를 느슨하게 결합.
-
-
AST와의 차이
-
AST : 원문 문법을 거의 그대로 반영(케이스가 많고 예외 처리 필요).
-
IR : 의미 중심으로 정규화/정리(예: 포지셔널 인스턴스 → 네임드로 변환, 폭 미지정→1비트로 통일, 상수 가능하면 평가).
-
-
일반적인 IR의 형태
-
컴파일러: SSA, 3-address code, CFG/DFG 등
-
하드웨어: 넷리스트-유사 구조 , 파라미터 평가 결과, 모듈/인스턴스/연결 관계 등
-
-
당신의 프로젝트에서의 IR
-
우리가 설계한 Python dataclass IR 이 곧 IR:
-
ModuleIR안에ports,nets,instances,parameters -
포트 방향/폭, 내부 넷 폭, 인스턴스의 포트 매핑을 일관된 스키마 로 제공
-
가능한 범위에서 정수 평가(
width,msb/lsb) 적용, 실패해도 원문(*_str) 보존
-
-
-
얻는 이점
-
이후 단계(예: 연결 검사, 자동 배선, 문서화, 코드 생성, 시각화)를 예외처리 거의 없이 구현 가능.
-
도구 간 재사용 ·확장 용이.
-
한 줄 요약: IR은 “다루기 쉬운 표준형 설계 스냅샷”이고, 지금 만들고 있는 Python 클래스들이 바로 그 IR이에요.
decl = io.first 이야. 근데 Enum을 Serialize 못해서 json 변환 부분에서 에러가 나
좋은 포인트 두 가지!
-
(A)
Ioport에서 선언/이름의 위치가 환경마다 달라decl = io.first가 되는 케이스가 있어요. ⇒ 양쪽 모두 자동 감지 하도록 바꿉니다. -
(B)
Enum은 기본json.dumps로 직렬화가 안 됩니다. ⇒ custom default/encoder 로.value를 쓰게 하면 해결됩니다.
아래를 그대로 반영하세요. (필요한 부분만 교체/추가)
1) Ioport 양식 차이를 자동 감지 (first/second 스왑 대응)
ordered_port_names 교체
def ordered_port_names(mod: vast.ModuleDef) -> List[str]:
names: List[str] = []
if mod.portlist is None:
return names
for p in mod.portlist.ports:
if isinstance(p, vast.Ioport):
# Robust: 이름이 어느 쪽(first/second)에 있든 잡아낸다
name_node = None
if isinstance(p.first, vast.Identifier):
name_node = p.first
elif isinstance(p.second, vast.Identifier):
name_node = p.second
if name_node is not None:
names.append(name_node.name)
elif isinstance(p, vast.Port):
names.append(p.name)
return names
normalize_module 내부의 포트 정규화 부분 수정
아래처럼 decl(방향/폭을 가진 선언 노드)를 어느 쪽(first/second)이든 잡도록 바꿉니다.
# ...
for idx, pname in enumerate(ordered_names):
dir_: Optional[Direction] = None
signed = False
msb_s = lsb_s = None
msb_v = lsb_v = width_v = None
ansi_hit = False
if mod.portlist and isinstance(mod.portlist.ports[idx], vast.Ioport):
io = mod.portlist.ports[idx]
# 어느 쪽이 선언 노드인지 감지
def _is_decl(x):
return isinstance(x, (vast.Input, vast.Output, vast.Inout))
decl = None
if _is_decl(io.first):
decl = io.first
elif _is_decl(io.second):
decl = io.second
if decl is not None:
if isinstance(decl, vast.Input): dir_ = Direction.INPUT
elif isinstance(decl, vast.Output): dir_ = Direction.OUTPUT
else: dir_ = Direction.INOUT
signed = bool(getattr(decl, "signed", False))
msb_s, lsb_s, msb_v, lsb_v, width_v = width_tuple(getattr(decl, "width", None), env)
ansi_hit = True
# Non-ANSI fallback
if not ansi_hit and pname in io_map:
dir_, signed, (msb_s, lsb_s, msb_v, lsb_v, width_v) = io_map[pname]
# 폭 미지정 → 1비트
if all(v is None for v in (msb_s, lsb_s, msb_v, lsb_v, width_v)):
msb_s, lsb_s, msb_v, lsb_v, width_v = "0", "0", 0, 0, 1
ports.append(Port(
index=idx, name=pname, dir=dir_, signed=signed,
msb_str=msb_s, lsb_str=lsb_s, msb=msb_v, lsb=lsb_v, width=width_v
))
이렇게 하면
decl = io.first인 케이스도, 반대로decl = io.second인 케이스도 모두 정상 처리돼요.
2) Enum JSON 직렬화 에러 해결
json.dumps에 default를 주거나, 커스텀 JSONEncoder를 써서 Enum을 .value로 바꿔 주세요.
간단 버전: default 함수 추가
import json
from enum import Enum
from dataclasses import asdict
def _json_default(o):
# Enum -> 그 값으로 직렬화
if isinstance(o, Enum):
return o.value
# 필요시 추가 핸들링 가능
return str(o)
@dataclass
class DesignIR:
modules: Dict[str, ModuleIR] = field(default_factory=dict)
def to_json(self, indent: int = 2) -> str:
return json.dumps(asdict(self), indent=indent, default=_json_default)
대안: Encoder 클래스로
class EnumEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Enum):
return o.value
return super().default(o)
# 사용
json.dumps(asdict(self), indent=2, cls=EnumEncoder)
어느 쪽이든 결과는 동일:
Direction.INPUT→"input",NetKind.WIRE→"wire"처럼 직렬화됩니다.
3) 빠른 체크리스트 (step-by-step)
-
ordered_port_names를 위 코드로 교체 -
normalize_module의 포트 처리 블록 을 위 코드로 교체 -
DesignIR.to_json에서default=_json_default또는cls=EnumEncoder를 적용 -
다시 실행해서
Enum직렬화 에러가 사라지는지, ANSI/Non-ANSI 모두에서 포트 방향/폭이 제대로 나오는지 확인
필요하면 Port.dir 타입을 일시적으로 Optional[str]로 바꿔도 되지만, 위처럼 Enum 유지 + 직렬화 커스터마이즈 가 더 안정적이에요.
다른 부분도 막히는 게 있으면 오류 메시지랑 함께 알려줘—거기에 맞춰 추가 패치 줄게!
1. Normalize 말고 표준화/정규화 라는 느낌으로 rename 하는 걸 제안해. 2. Port 방향이 non ansi 타입으로 되어있을 경우에 찾으면 업데이트하도록 해줘
좋아—요청한 두 가지를 차근차근 반영할게.
1) “Normalize” 이름을 “표준화/정규화” 느낌으로 변경 제안
영어 식별자는 그대로 쓰되 의미를 “표준화/정규화”로 바꿔 읽히도록 추천해.
이름 매핑(권장)
-
normalize_module→standardize_module(표준화) -
convert_to_design_ir→build_standard_ir(표준 IR 빌드) -
파일명
vnorm_classes.py→std_ir.py(standardized IR)
호환용 래퍼(선택)
기존 호출부 안 깨지게 아래처럼 유지해도 좋아.
def convert_to_design_ir(files):
# deprecated alias for backward-compat
return build_standard_ir(files)
2) Non-ANSI 선언에서 포트 방향/속성 발견 시 업데이트 로직 추가
요지는 “헤더(ANSI)가 비어 있거나(non-ANSI 스타일) 또는 충돌 시 본문 선언(Decl) 을 신뢰해 덮어쓰기”다.
아래 2개 조각만 교체/추가하면 됨.
(A) 포트 표준화 함수: standardize_module (기존 normalize_module 대체)
차이점:
-
Ioport에서 선언 노드(first/second) 자동 감지 -
일단 포트를 구성한 뒤 본문 선언으로 후처리 덮어쓰기 수행
def standardize_module(mod: vast.ModuleDef, modlib_ports: Dict[str, List[str]]) -> ModuleIR:
env = collect_param_env(mod)
# 1) parameters (동일)
params: List[Parameter] = []
if getattr(mod, "paramlist", None):
for p in mod.paramlist.params:
if isinstance(p, vast.Parameter):
params.append(Parameter(
name=p.name, value_str=to_str(p.value), value=eval_expr(p.value, env)
))
header_names = {p.name for p in params}
for item in getattr(mod, "items", []) or []:
if isinstance(item, vast.Decl):
for e in item.list:
if isinstance(e, vast.Parameter) and e.name not in header_names:
params.append(Parameter(
name=e.name, value_str=to_str(e.value), value=eval_expr(e.value, env)
))
# 2) 포트 초안(헤더 기반)
ordered_names = ordered_port_names(mod)
ports: List[Port] = []
for idx, pname in enumerate(ordered_names):
dir_, signed = None, False
msb_s = lsb_s = None
msb_v = lsb_v = width_v = None
# Ioport에서 first/second 어느 쪽이든 선언 노드 감지
decl = None
if mod.portlist and isinstance(mod.portlist.ports[idx], vast.Ioport):
io = mod.portlist.ports[idx]
def _is_decl(x): return isinstance(x, (vast.Input, vast.Output, vast.Inout))
if _is_decl(io.first): decl = io.first
elif _is_decl(io.second): decl = io.second
if decl is not None:
if isinstance(decl, vast.Input): dir_ = Direction.INPUT
elif isinstance(decl, vast.Output): dir_ = Direction.OUTPUT
else: dir_ = Direction.INOUT
signed = bool(getattr(decl, "signed", False))
msb_s, lsb_s, msb_v, lsb_v, width_v = width_tuple(getattr(decl, "width", None), env)
# 폭 미지정 → 1비트 기본
if all(v is None for v in (msb_s, lsb_s, msb_v, lsb_v, width_v)):
msb_s, lsb_s, msb_v, lsb_v, width_v = "0", "0", 0, 0, 1
ports.append(Port(
index=idx, name=pname, dir=dir_, signed=signed,
msb_str=msb_s, lsb_str=lsb_s, msb=msb_v, lsb=lsb_v, width=width_v
))
# 3) 본문 선언(Non-ANSI)로 **후처리 덮어쓰기**
io_map = build_io_decl_map(mod, env)
apply_nonansi_overrides(ports, io_map, prefer_body=True)
# 4) nets (포트명 제외)
nets_raw = build_net_decl_list(mod, env)
port_names = {p.name for p in ports}
nets = [n for n in nets_raw if n.name not in port_names]
# 5) instances (기존과 동일)
instances: List[Instance] = []
for il in iter_instancelists(mod):
submod = il.module
shared_param_ovrs: List[Parameter] = []
if getattr(il, "paramlist", None):
for pa in il.paramlist.params:
name = getattr(pa, "paramname", None) or getattr(pa, "name", None)
value_node = getattr(pa, "argname", None) or getattr(pa, "value", None)
shared_param_ovrs.append(Parameter(
name=name, value_str=to_str(value_node), value=eval_expr(value_node, env)
))
for inst in il.instances:
conns: List[Connection] = []
pas = getattr(inst, "portlist", []) or []
positional = any(getattr(pa, "portname", None) in (None, "") for pa in pas)
if positional:
lib_ports = modlib_ports.get(submod, [])
for i, pa in enumerate(pas):
pname = lib_ports[i] if i < len(lib_ports) else f"__pos{i}"
conns.append(Connection(port=pname, expr_str=to_str(getattr(pa, "argname", None))))
else:
for pa in pas:
conns.append(Connection(port=pa.portname, expr_str=to_str(pa.argname)))
instances.append(Instance(inst=inst.name, module=submod,
param_overrides=list(shared_param_ovrs),
connections=conns))
return ModuleIR(parameters=params, ports=ports, nets=nets, instances=instances)
(B) 본문 선언 덮어쓰기 헬퍼 추가: apply_nonansi_overrides
-
prefer_body=True면 항상 본문 선언으로 덮어씀(권장) -
prefer_body=False면 비어있는 값만 채움
def apply_nonansi_overrides(ports: List[Port], io_map: Dict[str, tuple], prefer_body: bool = True):
for p in ports:
if p.name not in io_map:
continue
dir2, signed2, (msb_s2, lsb_s2, msb_v2, lsb_v2, width_v2) = io_map[p.name]
def use_body(existing, new):
return new if (prefer_body or existing is None) else existing
# 방향/서명/폭 모두 업데이트 규칙 적용
p.dir = use_body(p.dir, dir2)
p.signed = use_body(p.signed, signed2)
p.msb_str = use_body(p.msb_str, msb_s2)
p.lsb_str = use_body(p.lsb_str, lsb_s2)
p.msb = use_body(p.msb, msb_v2)
p.lsb = use_body(p.lsb, lsb_v2)
p.width = use_body(p.width, width_v2)
(C) 엔트리 포인트 이름 교체 (build_standard_ir)
def build_standard_ir(files: List[str]) -> DesignIR:
ast, _ = parse(files)
mods = collect_modules(ast)
modlib_ports = {name: ordered_port_names(m) for name, m in mods.items()}
design = DesignIR()
for name, m in mods.items():
design.modules[name] = standardize_module(m, modlib_ports)
return design
# (선택) 하위호환
def convert_to_design_ir(files: List[str]) -> DesignIR:
return build_standard_ir(files)
동작 요약 (step-by-step)
-
헤더에서 포트 초안 생성 (ANSI면 그 값 사용, 아니면 1비트 기본)
-
본문
Decl(Input/Output/Inout)스캔해서 io_map 생성 -
apply_nonansi_overrides(..., prefer_body=True)로-
non-ANSI에서 찾은 방향/폭/서명 을 항상 덮어쓰기
-
결과적으로 “헤더가 비어 있고 본문에만 방향이 있는” 경우도 정확히 채워짐
-
원하면 prefer_body=False로 바꿔 “헤더 우선, 빈 값만 보충” 정책도 쉽게 전환 가능해.
필요 시 포트에 source="ansi|body" 같은 필드 추가해서 최종 값의 출처를 트래킹하는 것도 추천!