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)


설계 (스텝별)

  1. 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. 두 단계 수집
  • 1패스 : 모든 모듈의 포트 순서/기본 파라미터 테이블 생성(포지셔널 매핑용).

  • 2패스 : 각 모듈에 대해 포트/넷/인스턴스 표준화.

  1. 포트 정규화
  • ANSI(Ioport)와 Non-ANSI(Port+본문 Input/Output/Inout 선언) 모두 흡수.

  • 폭 미지정은 1비트(msb=lsb=0)로 통일. 방향 미상은 보수적으로 inout 대신 발견 실패 시 null(현실적으로 거의 없음).

  1. 인스턴스 정규화
  • InstanceList를 재귀 탐색(Generate 블록 내부 포함).

  • 포지셔널 연결은 1패스에서 모은 서브모듈 포트 순서 로 네임드 변환. 알 수 없는 외부 모듈은 인덱스 기반으로 남김(예: __pos0, __pos1).

  1. 넷 정규화
  • wire/reg(필요하면 integer)를 수집, 다중 선언은 개별 항목으로 분할.

  • 포트 선언에서 이미 등장한 식별자는 nets에서 중복 제외(요구사항이 “내부 nets”이므로).

  1. 표현식 평가기
  • 파라미터 환경을 사용해 IntConst/Identifier/UnaryOp/BinaryOp 중심으로 안전한 정수 평가(미지원 연산 시 값은 null, 원 문자열은 유지).

레퍼런스 구현 (Python / PyVerilog)

pip: pyverilog 필요. 파일 모음 입력 → 위 IR JSON 반환. ```python

vnorm.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. 1패스 : 모든 모듈의

    • 포트 순서 수집(포지셔널 매핑용)

    • 파라미터 기본값 환경(env) 구성(폭 평가용)

  2. 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/lsbNone이 될 수 있음. 원문은 *_str에 보존.

  • 포지셔널 연결 : 라이브러리에 해당 서브모듈이 없으면 __pos0처럼 보존.

  • SystemVerilog 확장 : logic, packed/unpacked, interface 포트 등은 별 레이어로 확장 추천.

  • 위치 정보 : 필요하면 lineno/coord를 dataclass에 추가 가능.

  • 검증 : 큰 설계에서 스키마 보장 원하면 pydantic으로 교체 가능.

원하는 대로 직접 탐색 가능한 Python 클래스 IR 로 변환되도록 구성해 두었어.
원하면 Connection에 좌표/소스파일 정보나, ModuleIR에 빠른 인덱스(예: port_dict)를 추가해주는 최적화 버전도 바로 만들어줄게.

IR이 뭐야

좋은 질문! IR(Intermediate Representation, 중간 표현)소스 코드(또는 설계)를 한층 표준화된 형태로 담아두는 중간 단계의 데이터 구조 예요. 파서가 만든 “문법 그대로”의 트리(AST)와, 그걸 써서 분석·변환·코드생성 같은 후처리 사이에 놓입니다.

아래처럼 차근차근 이해하면 편해요:

  1. 정의

    • IR = 분석·최적화·변환을 하기 쉽도록 만든 표준화된 내부 데이터 모델.
  2. 왜 필요한가

    • 언어 문법 차이/스타일(예: Verilog ANSI vs Non-ANSI 포트 선언)을 한 가지 규칙 으로 통일.

    • 트래버스(순회) 쉬움 : 필요한 정보가 같은 자리/형태에 있음.

    • 모듈성 : 프런트엔드(AST)와 백엔드(분석/생성)를 느슨하게 결합.

  3. AST와의 차이

    • AST : 원문 문법을 거의 그대로 반영(케이스가 많고 예외 처리 필요).

    • IR : 의미 중심으로 정규화/정리(예: 포지셔널 인스턴스 → 네임드로 변환, 폭 미지정→1비트로 통일, 상수 가능하면 평가).

  4. 일반적인 IR의 형태

    • 컴파일러: SSA, 3-address code, CFG/DFG 등

    • 하드웨어: 넷리스트-유사 구조 , 파라미터 평가 결과, 모듈/인스턴스/연결 관계 등

  5. 당신의 프로젝트에서의 IR

    • 우리가 설계한 Python dataclass IR 이 곧 IR:

      • ModuleIR 안에 ports, nets, instances, parameters

      • 포트 방향/폭, 내부 넷 폭, 인스턴스의 포트 매핑을 일관된 스키마 로 제공

      • 가능한 범위에서 정수 평가(width, msb/lsb) 적용, 실패해도 원문(*_str) 보존

  6. 얻는 이점

    • 이후 단계(예: 연결 검사, 자동 배선, 문서화, 코드 생성, 시각화)를 예외처리 거의 없이 구현 가능.

    • 도구 간 재사용 ·확장 용이.

한 줄 요약: 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.dumpsdefault를 주거나, 커스텀 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)

  1. ordered_port_names를 위 코드로 교체

  2. normalize_module포트 처리 블록 을 위 코드로 교체

  3. DesignIR.to_json에서 default=_json_default 또는 cls=EnumEncoder적용

  4. 다시 실행해서 Enum 직렬화 에러가 사라지는지, ANSI/Non-ANSI 모두에서 포트 방향/폭이 제대로 나오는지 확인

필요하면 Port.dir 타입을 일시적으로 Optional[str]로 바꿔도 되지만, 위처럼 Enum 유지 + 직렬화 커스터마이즈 가 더 안정적이에요.
다른 부분도 막히는 게 있으면 오류 메시지랑 함께 알려줘—거기에 맞춰 추가 패치 줄게!

1. Normalize 말고 표준화/정규화 라는 느낌으로 rename 하는 걸 제안해. 2. Port 방향이 non ansi 타입으로 되어있을 경우에 찾으면 업데이트하도록 해줘

좋아—요청한 두 가지를 차근차근 반영할게.


1) “Normalize” 이름을 “표준화/정규화” 느낌으로 변경 제안

영어 식별자는 그대로 쓰되 의미를 “표준화/정규화”로 바꿔 읽히도록 추천해.

이름 매핑(권장)

  • normalize_modulestandardize_module (표준화)

  • convert_to_design_irbuild_standard_ir (표준 IR 빌드)

  • 파일명 vnorm_classes.pystd_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)

  1. 헤더에서 포트 초안 생성 (ANSI면 그 값 사용, 아니면 1비트 기본)

  2. 본문 Decl(Input/Output/Inout) 스캔해서 io_map 생성

  3. apply_nonansi_overrides(..., prefer_body=True)

    • non-ANSI에서 찾은 방향/폭/서명항상 덮어쓰기

    • 결과적으로 “헤더가 비어 있고 본문에만 방향이 있는” 경우도 정확히 채워짐

원하면 prefer_body=False로 바꿔 “헤더 우선, 빈 값만 보충” 정책도 쉽게 전환 가능해.
필요 시 포트에 source="ansi|body" 같은 필드 추가해서 최종 값의 출처를 트래킹하는 것도 추천!

Categories:

Updated: