Skip to content

Latest commit

 

History

History
898 lines (731 loc) · 41.5 KB

File metadata and controls

898 lines (731 loc) · 41.5 KB

SystemVerilog モジュール間接続図ビューア 仕様書

1. 概要

1.1 本ドキュメントの目的

本書は、SystemVerilog のソースコードを入力として、モジュール間の接続関係を視覚的に表示・編集できる Web アプリケーション(以下、本アプリ)の開発仕様を定義する。

1.2 背景

  • SystemVerilog の大規模設計では、モジュール階層と信号接続が複雑化しやすく、コードのみでは全体像の把握が困難
  • 既存の商用 EDA ツール(Vivado RTL Viewer、Sigasi 等)は高機能だが、ライセンスや導入コストの制約がある
  • 軽量・OSS のみで構成され、Web ブラウザだけで動作するビューアが求められる

1.3 ゴール

  1. SystemVerilog ファイル(複数)を解析し、モジュール定義・インスタンス・ポート接続を抽出する
  2. モジュールを矩形、ポートを入出力端子、接続を線として描画する
  3. 接続線がノードと重ならないよう、直角配線で自動的にノードを迂回する
  4. ユーザーがノードをドラッグして配置を整えられる(編集機能)。ドロップ時は接続線のみが自動再ルーティングされる
  5. レイアウト・配置情報を JSON で保存・復元できる
  6. §6.5 に定義する「レイアウト・ルーティング要求仕様」(R-A1〜R-A14、F-17)を遵守する

1.4 非ゴール

  • 論理合成や回路図(ゲートレベル)生成は対象外
  • シミュレーション・タイミング解析は対象外
  • SystemVerilog の interface / modport / UVM コンポーネントの完全な表現は対象外(将来検討)

2. システム構成

2.1 全体アーキテクチャ

┌─────────────────────────────────────────────────────────────┐
│                       ブラウザ (Frontend)                    │
│  ┌────────────────────┐    ┌────────────────────────────┐   │
│  │  ファイル選択 UI    │    │  ダイアグラム表示 (JointJS) │   │
│  │  (input[type=file])│    │  - manhattan router        │   │
│  └─────────┬──────────┘    │  - ポート付きノード        │   │
│            │               │  - ドラッグ編集            │   │
│            ↓               └────────────────────────────┘   │
│  ┌────────────────────┐                ↑                    │
│  │  JSON エクスポート/  │                │                    │
│  │  インポート         │                │                    │
│  └────────────────────┘                │                    │
└─────────────────────────────────────────┼───────────────────┘
                                          │ (JSON: graph model)
                                          │
┌─────────────────────────────────────────┼───────────────────┐
│                  サーバ (Backend, Python/Node)               │
│  ┌────────────────────┐    ┌────────────────────────────┐   │
│  │  ファイル受信 API   │ →  │  Verible 呼び出し          │   │
│  │  (POST /parse)     │    │  (verible-verilog-syntax   │   │
│  └────────────────────┘    │   --export_json)           │   │
│                            └────────────┬───────────────┘   │
│                                         ↓                   │
│                            ┌────────────────────────────┐   │
│                            │  CST → 中間モデル変換       │   │
│                            │  (modules, ports, insts)   │   │
│                            └────────────┬───────────────┘   │
│                                         ↓                   │
│                            ┌────────────────────────────┐   │
│                            │  JointJS 用 JSON 整形       │   │
│                            └────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

2.2 技術スタック

レイヤ 技術 用途
SystemVerilog 解析 Verible (verible-verilog-syntax --export_json) CST 抽出
バックエンド Python 3.11+ / FastAPI API サーバ、Verible 呼び出し、CST 解析
Verible Python ラッパー verible_verilog_syntax.py(Verible 同梱) CST → Python オブジェクト変換
フロントエンド 素の TypeScript + Vite フレームワーク非依存
ダイアグラム描画 JointJS Core (@joint/core, MPL-2.0) ノード・リンク・自動回避配線
パッケージ管理 uv / pnpm Python / Node

2.3 ライセンス方針

  • 本アプリのソースコードは MIT または Apache-2.0 で公開
  • 依存ライブラリの主要ライセンスは以下を許容
    • Verible: Apache-2.0
    • JointJS Core: MPL-2.0(改変時は当該ファイルのみ MPL のまま再頒布)
    • FastAPI / Vite / TypeScript: MIT

3. 機能仕様

3.1 機能一覧

ID 機能 優先度
F-01 SystemVerilog ファイルのアップロード(複数) 必須
F-02 Verible によるパース、エラー表示 必須
F-03 モジュール・ポート・インスタンスの抽出 必須
F-04 トップモジュールの選択 必須
F-05 モジュール矩形の描画(インスタンス名 + モジュール名) 必須
F-06 入出力ポートの描画(左 = input、右 = output) 必須
F-07 ポート間接続線の描画(manhattan router で自動迂回) 必須
F-08 ノードのドラッグ移動 必須
F-09 キャンバスのパン・ズーム 必須
F-10 レイアウトの JSON 保存・復元 必須
F-11 信号名のラベル表示 必須
F-12 inout ポートの表現(中央配置 or 別色) 任意
F-13 バス幅の表示([7:0] 等) 任意
F-14 SVG / PNG エクスポート 任意
F-15 階層展開(インスタンス内部を別ビューで表示) 任意
F-16 自動初期レイアウト(ELK.js による)(§6.5 R-A6〜R-A12 を満たす) 必須
F-17 ノードドロップ時の接続線のみの自動再ルーティング(§6.5 R-A5)
※手動「再ルーティング」ボタンは設けない
必須

3.2 ユーザーフロー

  1. ユーザーがブラウザで本アプリを開く
  2. 「ファイルを選択」ボタンから .sv / .v ファイルを 1〜複数選択
  3. 「解析」ボタンをクリック → サーバが Verible でパース
  4. 解析成功時、トップモジュール候補がドロップダウンに表示される
  5. ユーザーがトップモジュールを選択 → ダイアグラムが描画される
  6. ユーザーは必要に応じてノードをドラッグして配置を整える
  7. 「保存」ボタンでレイアウト JSON をダウンロード
  8. 次回以降「読み込み」ボタンで JSON を読み込み、レイアウト復元

4. データモデル

4.1 中間モデル(バックエンド内部)

Verible CST から抽出する中間表現を以下とする。

from dataclasses import dataclass
from typing import Literal

@dataclass
class Port:
    name: str
    direction: Literal["input", "output", "inout"]
    width: int = 1          # ビット幅([7:0] なら 8)
    msb: int | None = None  # 例: 7
    lsb: int | None = None  # 例: 0

@dataclass
class ModuleDef:
    name: str               # モジュール名
    ports: list[Port]
    parameters: dict[str, str]  # パラメータ名 → デフォルト値

@dataclass
class PortConnection:
    port_name: str          # 接続先モジュールのポート名
    signal: str             # 接続される信号名(または式)

@dataclass
class ModuleInstance:
    instance_name: str      # 例: "u_cpu"
    module_name: str        # 例: "cpu_top"
    parameters: dict[str, str]
    connections: list[PortConnection]

@dataclass
class ParsedDesign:
    modules: dict[str, ModuleDef]     # モジュール名 → 定義
    top_candidates: list[str]         # トップモジュール候補
    instances_by_parent: dict[str, list[ModuleInstance]]
                                      # 親モジュール名 → インスタンス一覧

4.2 API レスポンス JSON

サーバ → フロントエンドへ返す JSON のスキーマ。

{
  "status": "ok",
  "errors": [],
  "design": {
    "modules": {
      "cpu_top": {
        "name": "cpu_top",
        "ports": [
          { "name": "clk",    "direction": "input",  "width": 1 },
          { "name": "rst_n",  "direction": "input",  "width": 1 },
          { "name": "data_o", "direction": "output", "width": 32, "msb": 31, "lsb": 0 }
        ],
        "parameters": { "WIDTH": "32" }
      }
    },
    "top_candidates": ["cpu_top"],
    "instances_by_parent": {
      "cpu_top": [
        {
          "instance_name": "u_alu",
          "module_name": "alu",
          "parameters": {},
          "connections": [
            { "port_name": "a", "signal": "reg_a" },
            { "port_name": "b", "signal": "reg_b" },
            { "port_name": "y", "signal": "alu_out" }
          ]
        }
      ]
    }
  }
}

4.3 JointJS グラフ JSON(保存形式)

JointJS の graph.toJSON() 形式をそのまま採用する。アプリ独自のメタ情報は attrs._meta に格納する。

{
  "version": "1.0",
  "topModule": "cpu_top",
  "graph": {
    "cells": [
      {
        "type": "standard.Rectangle",
        "id": "u_alu",
        "position": { "x": 100, "y": 100 },
        "size":     { "width": 180, "height": 120 },
        "attrs": {
          "label":  { "text": "u_alu\n(alu)" },
          "_meta":  { "moduleName": "alu", "instanceName": "u_alu" }
        },
        "ports": { "groups": { /* ... */ }, "items": [ /* ... */ ] }
      },
      {
        "type": "standard.Link",
        "source": { "id": "u_alu", "port": "port_y" },
        "target": { "id": "u_reg", "port": "port_d" },
        "router":    { "name": "manhattan", "args": { "padding": 20 } },
        "connector": { "name": "rounded" },
        "labels": [{ "attrs": { "text": { "text": "alu_out[31:0]" } } }]
      }
    ]
  }
}

5. バックエンド仕様

5.1 エンドポイント一覧

メソッド パス 用途
POST /api/parse SV ファイルを受け取り、解析結果 JSON を返す
GET /api/health ヘルスチェック

5.2 POST /api/parse

リクエスト: multipart/form-data

フィールド 必須 説明
files File[] .sv / .v ファイル群
top string × トップモジュール名のヒント(指定なければ自動推定)

レスポンス: application/json(セクション 4.2 のスキーマ)

エラー応答:

{
  "status": "error",
  "errors": [
    { "file": "cpu.sv", "line": 42, "column": 8, "message": "syntax error near 'endmodule'" }
  ]
}

5.3 Verible 呼び出し

verible-verilog-syntax --export_json --printtree path/to/file.sv

--export_json フラグを使用することで、CST を JSON 形式で取得できる。Verible には Python ラッパー verible_verilog_syntax.py が同梱されており、これを利用して CST を Python オブジェクトとして扱う。

5.4 CST から中間モデルへの変換

Verible CST のノードタグから以下を抽出する。

抽出対象 CST タグ
モジュール定義 kModuleDeclaration
モジュール名 kModuleHeader 直下の SymbolIdentifier
ポート宣言(ANSI スタイル) kPortDeclaration 配下の kPortDirection + SymbolIdentifier + kPackedDimensions
インスタンス kModuleInstantiation / kGateInstantiation
インスタンス名 kGateInstance 直下の SymbolIdentifier
名前付き接続 kActualNamedPort.port_name(signal) 形式)
順序接続 kActualPositionalPort

トップモジュール候補は「他のモジュールからインスタンス化されていないモジュール」とする。

5.5 エラーハンドリング

  • パースエラー時は HTTP 200 で status: "error" を返す(HTTP 4xx は使わない)
  • ファイル形式不正(.sv / .v 以外)は HTTP 400
  • Verible 未インストール時は HTTP 500 で { "error": "verible not found" }

6. フロントエンド仕様

6.1 画面構成

┌──────────────────────────────────────────────────────────┐
│ ヘッダ: アプリ名  [ファイル選択] [解析] [保存] [読込]     │
├──────┬───────────────────────────────────────────────────┤
│      │                                                   │
│ 左   │              ダイアグラム表示エリア                │
│ ペイン│              (JointJS Paper)                    │
│      │                                                   │
│ - モ │                                                   │
│ ジュ │                                                   │
│ ール │                                                   │
│ 一覧 │                                                   │
│ - ト │                                                   │
│ ップ │                                                   │
│ 選択 │                                                   │
│      │                                                   │
├──────┴───────────────────────────────────────────────────┤
│ ステータスバー: パース結果 / エラー / 選択中ノード情報    │
└──────────────────────────────────────────────────────────┘

6.2 ダイアグラム描画ルール

6.2.1 ノード(モジュールインスタンス)

  • 形状: 角丸矩形(standard.Rectangle
  • サイズ: 幅 = max(180, ポート最大ラベル幅 + 80)、高さ = max(80, max(入力数, 出力数) * 25 + 40)
  • 色: 種別ごとに固定パレット(後述)
  • ラベル: 上段にインスタンス名、下段にモジュール名(小さく)
  • 例:
    ┌────────────────┐
    │  u_alu         │ ← インスタンス名(大)
    │  (alu)         │ ← モジュール名(小)
    │ ●a        y● │
    │ ●b           │
    └────────────────┘
    

6.2.2 ポート

  • input: 左辺に配置、青色(#8ECAE6)、magnet: 'passive'(リンク終点になれる)
  • output: 右辺に配置、橙色(#FFB703)、magnet: true(リンク始点になれる)
  • inout: 左辺または右辺(R-A3 により上下辺は不可)に配置、緑色(#95D5B2)。原則として、信号の駆動方向が読み取れない場合は左辺扱いとする
  • ポートサイズ: 半径 6px の円
  • ラベル: ポート円の内側方向にテキスト、フォントサイズ 11px

6.2.3 リンク(接続線)

  • ルーター: manhattan(ノード自動回避)
  • コネクタ: rounded(角丸)
  • 線の太さ: バス幅で変える
    • 1 bit: 1.5px
    • 2〜8 bit: 2.5px
    • 9 bit 以上: 3.5px
  • 色:
    • 通常: #444
    • クロック信号(clk, clock を含む名前): #E63946
    • リセット信号(rst, reset を含む名前): #F4A261
  • 中央にラベル: 信号名 + ビット幅(例: data[31:0]

6.2.4 デフォルトのルーター設定

const paper = new joint.dia.Paper({
  // ...
  defaultRouter: {
    name: 'manhattan',
    args: {
      padding: 20,
      step: 10,
      maximumLoops: 2000,
      startDirections: ['right'],
      endDirections: ['left']
    }
  },
  defaultConnector: { name: 'rounded', args: { radius: 8 } },
  defaultLink: () => new joint.shapes.standard.Link()
});

6.3 接続の妥当性チェック

ユーザーが新規にリンクを引いた際の検証ルール。

  • 出力 → 入力 のみ許可
  • 同一モジュール内の自己ループは禁止
  • ポート以外の場所への接続禁止(linkPinning: false
validateConnection: (srcView, srcMag, tgtView, tgtMag, end, linkView) => {
  if (srcView === tgtView) return false;
  if (!srcMag || !tgtMag) return false;
  const srcGroup = srcMag.getAttribute('port-group');
  const tgtGroup = tgtMag.getAttribute('port-group');
  return srcGroup === 'out' && tgtGroup === 'in';
}

6.4 操作仕様

操作 効果
ノードをドラッグ ノードの移動。接続線は自動再ルーティング
キャンバスをドラッグ(空きスペース) パン
マウスホイール ズームイン・アウト
ノードをクリック 選択状態(青枠ハイライト)
出力ポートからドラッグ 新規リンクの作成(編集モード時のみ)
リンク上で右クリック コンテキストメニュー(削除のみ。Reroute 追加は F-17 により提供しない)
Ctrl+S レイアウト JSON 保存
Ctrl+O レイアウト JSON 読み込み

6.5 レイアウト・ルーティング要求仕様

初期レイアウトおよび再レイアウト時に遵守すべき要求仕様を 5 つの観点で定義する。これらは F-16(自動初期レイアウト)および F-17(自動再ルーティング)を実現するための必須要件である。詳細は .aiprj/AI_PRJ_REQUIREMENTS.md §2 / §7 を参照。

6.5.1 再レイアウト(ノードドラッグ時の挙動)

ID 要求
R-A5 ノードを移動した時は 接続線のみを再レイアウト する。ノードは動かさない。ドラッグされたノードはユーザーがドロップした位置にそのまま留まる。兄弟ノード・無関係なノードも位置を保つ。親コンテナは R-A4 によりリサイズされ得るが、左上座標は変化しない。

実装方針(JointJS):

  • element:pointerup イベントをハンドルし、当該ノードに接続するリンクのみ linkView.requestConnectionUpdate() を呼ぶ
  • 他のノードの position には触れない
  • 全リンク一括再計算が必要な場合のみ rerouteLinks() ヘルパー関数を呼ぶ(実装は §6.5.4 参照)

6.5.2 機能制約

ID 要求
F-17 手動の「再ルーティング」ボタンは設けない。再ルーティングはドロップ時に自動で行われ(R-A5)、専用ボタンは冗長のため廃止。

→ ツールバー仕様(§6.1)からは「再ルーティング」ボタンを除外する。

6.5.3 レイアウト仕様(初期レイアウト時の配置間隔)

ID 要求
R-A8 接続線が混雑する場合は、最初からノード間に十分な距離を確保する。混雑度(edges / nodes)に応じて ELK の spacing 系オプションを 最大 2.0 倍 までスケールする。
R-A12 混雑していない設計でもノードの左右方向に十分な余裕を確保する。elk.layered.spacing.nodeNodeBetweenLayers の基準値を 120 → 180 へ引き上げ、R-A8 の倍率と 加算的に作用 させる。

spacing スケール計算式:

// R-A8 + R-A12 を加算的に適用
function computeSpacingMultiplier(edgeCount: number, nodeCount: number): number {
  const density = nodeCount > 0 ? edgeCount / nodeCount : 0;
  // 混雑度 0 で 1.0、混雑度 3.0 で 2.0 にクランプ
  const congestionMultiplier = Math.min(1.0 + density / 3.0, 2.0);
  return congestionMultiplier;
}

const baseLayerSpacing = 180;  // R-A12: 旧 120 → 180
const baseNodeSpacing  = 80;
const mult = computeSpacingMultiplier(edges.length, nodes.length);

const layoutOptions = {
  'elk.algorithm': 'layered',
  'elk.direction': 'RIGHT',
  'elk.spacing.nodeNode': String(baseNodeSpacing * mult),
  'elk.layered.spacing.nodeNodeBetweenLayers': String(baseLayerSpacing * mult),
  'elk.spacing.edgeNode': String(40 * mult),
  'elk.spacing.edgeEdge': String(20 * mult)
};

6.5.4 ルーティング仕様(初期・再レイアウトの両方に適用)

ID 要求
R-A1 線同士の重複を禁止する。特に 縦線同士の重複は厳禁。各ネットは独自のチャネルでルーティングする。
R-A2 線はノードに重ならず、ノードの上を通過せず、必ず迂回 する。ソース・ターゲット以外のノードを完全に避けてルーティングする。
R-A3 線は必ず 左右方向でポートに接続 する。入出力ポートは左辺・右辺のみに配置し、上下辺には置かない。
R-A4 コンテナ内の子ノードを移動した場合、親コンテナは「現在の左上座標を原点として」リサイズする。左上は不変で、幅・高さのみが必要に応じて拡張される。階層は再帰的に適用される。
R-A9 上記の禁止事項を実際に満たすよう、ルーティング実装を継続的に最適化する(メタ要求)。違反ケースが見つかった場合は実装を更新する。

実装方針:

  • R-A1(線重複の回避): 各ネットに固有のチャネル(垂直走行位置)を割り当てる。同じ x 座標の縦線が重ならないよう、ネット ID をキーにオフセットを付与する。manhattan router の step をネットごとにずらすか、独自の router で実装する
  • R-A2(ノード回避): JointJS の manhattan router を採用(A* ベース、障害物回避が組込)。excludeEnds: [](全ノードを障害物として考慮)、padding: 20 * spacingMultiplier を設定
  • R-A3(左右接続のみ): ポートの position: { name: 'left' | 'right' } のみ使用。startDirections: ['right']endDirections: ['left'] を強制
  • R-A4(コンテナの左上不変リサイズ): 子ノード移動後、親要素の bounding box を再計算するが position は固定。size のみ更新する resizeContainerKeepingOrigin() ヘルパーを実装
  • R-A14(中央値トランク): 1-to-N のネットの場合、関与する全エンドポイントの x 座標の中央値を計算し、その x 位置に垂直トランクを置く
// R-A13 + R-A14 を満たす rerouteLinks() の擬似実装
function rerouteLinks(graph: joint.dia.Graph, paper: joint.dia.Paper): void {
  const links = graph.getLinks();
  
  // ネット ID(信号名)ごとにグルーピング
  const nets = groupBy(links, link => link.attr('_meta/netName'));
  
  for (const [netName, netLinks] of nets) {
    if (netLinks.length === 1) {
      // 1-to-1: R-A13 の straight-line shortcut を試行
      const link = netLinks[0];
      if (canDrawStraightLine(link, graph)) {
        link.router('normal');  // 直線
        continue;
      }
      link.router('manhattan', { padding: 20, startDirections: ['right'], endDirections: ['left'] });
    } else {
      // 1-to-N: R-A14 の中央値トランク
      const endpoints = collectEndpoints(netLinks);
      const trunkX = median(endpoints.map(e => e.x));
      netLinks.forEach(link => {
        link.router('manhattan', {
          padding: 20,
          startDirections: ['right'],
          endDirections: ['left'],
          // 中央値トランクへの誘導は vertices で実装
        });
        link.vertices([{ x: trunkX, y: link.getSourcePoint().y }]);
      });
    }
  }
}

6.5.5 ノード配置(初期レイアウト時の位置制約)

ID 要求
R-A6 トップレベル入力端子は 最左列 に配置する。ELK 子ノードに elk.layered.layering.layerConstraint: FIRST を付与する。
R-A7 トップレベル出力端子は 最右列 に配置する。同上、layerConstraint: LASTinout には制約を付けない。
R-A10 SystemVerilog interface の master 側 modport に接続するインスタンスは 左側 へ配置する。検出は接続信号の .master サフィックスに対する正規表現ヒューリスティック。完全な interface / modport 解析は T4-01 で将来対応。
R-A11 同様に slave 側のインスタンスは 右側 に配置する(.slave サフィックス)。

実装方針(ELK.js への制約付与):

function buildElkChildren(design: ParsedDesign, topModuleName: string): ElkNode[] {
  const top = design.modules[topModuleName];
  const instances = design.instances_by_parent[topModuleName] ?? [];
  const children: ElkNode[] = [];

  // R-A6: トップレベル入力端子を FIRST 層へ
  top.ports.filter(p => p.direction === 'input').forEach(p => {
    children.push({
      id: `top_in_${p.name}`,
      width: 80, height: 40,
      layoutOptions: { 'elk.layered.layering.layerConstraint': 'FIRST' }
    });
  });

  // R-A7: トップレベル出力端子を LAST 層へ
  top.ports.filter(p => p.direction === 'output').forEach(p => {
    children.push({
      id: `top_out_${p.name}`,
      width: 80, height: 40,
      layoutOptions: { 'elk.layered.layering.layerConstraint': 'LAST' }
    });
  });

  // inout は制約なし
  top.ports.filter(p => p.direction === 'inout').forEach(p => {
    children.push({ id: `top_io_${p.name}`, width: 80, height: 40 });
  });

  // R-A10/R-A11: master/slave サフィックスでインスタンス位置を寄せる
  for (const inst of instances) {
    const sigs = inst.connections.map(c => c.signal);
    const isMaster = sigs.some(s => /\.master$/.test(s));
    const isSlave  = sigs.some(s => /\.slave$/.test(s));

    const constraintOption: Record<string, string> = {};
    if (isMaster && !isSlave) {
      // 左寄せ: layerConstraint は FIRST/LAST のみで「左寄り」は無いため、
      // position constraint で代用するか、layerChoiceConstraint を利用
      constraintOption['elk.layered.layering.layerChoiceConstraint'] = '1';  // FIRST の次の層
    } else if (isSlave && !isMaster) {
      constraintOption['elk.layered.layering.layerChoiceConstraint'] = '-2'; // LAST の前の層
    }

    children.push({
      id: inst.instance_name,
      width: computeNodeWidth(design.modules[inst.module_name]),
      height: computeNodeHeight(design.modules[inst.module_name]),
      layoutOptions: constraintOption
    });
  }

  return children;
}

6.5.6 ELK.js レイアウト呼び出し(統合版)

§6.5.3〜§6.5.5 をまとめた初期レイアウトの呼び出しは以下のようになる。

import ELK from 'elkjs/lib/elk.bundled.js';

async function performInitialLayout(
  design: ParsedDesign,
  topModuleName: string,
  graph: joint.dia.Graph
): Promise<void> {
  const elk = new ELK();
  const children = buildElkChildren(design, topModuleName);              // R-A6, R-A7, R-A10, R-A11
  const edges = buildElkEdges(design, topModuleName);

  const mult = computeSpacingMultiplier(edges.length, children.length);  // R-A8

  const elkGraph: ElkNode = {
    id: 'root',
    layoutOptions: {
      'elk.algorithm': 'layered',
      'elk.direction': 'RIGHT',                                          // R-A3 の前提
      'elk.spacing.nodeNode':                          String(80 * mult),
      'elk.layered.spacing.nodeNodeBetweenLayers':     String(180 * mult), // R-A12
      'elk.spacing.edgeNode':                          String(40 * mult),
      'elk.spacing.edgeEdge':                          String(20 * mult), // R-A1
      'elk.layered.crossingMinimization.semiInteractive': 'true',
      'elk.portConstraints': 'FIXED_SIDE'                                // R-A3
    },
    children,
    edges
  };

  const layouted = await elk.layout(elkGraph);

  // ELK 結果を JointJS に反映
  layouted.children?.forEach(c => {
    const cell = graph.getCell(c.id);
    if (cell) cell.position(c.x ?? 0, c.y ?? 0);
  });

  // ルーティング適用(R-A1, R-A2, R-A13, R-A14)
  rerouteLinks(graph, paper);
}

6.5.7 ドラッグ&ドロップ時の処理フロー(R-A5 / R-A4 / F-17)

ユーザーがノードをドラッグ
    ↓
element:pointerdown → ドラッグ開始位置を記録
    ↓
element:pointermove → ノードの位置のみ更新(リンクは JointJS が自動追従、ただし router は再評価されない)
    ↓
element:pointerup(= ドロップ)
    ↓
1. ドロップされたノードの位置を確定(他ノードは触らない)       ← R-A5
2. 親コンテナがあれば左上を保ったまま bounding box を再計算   ← R-A4
3. rerouteLinks() を呼び出して接続線のみ再ルーティング        ← R-A5, F-17
4. 必要に応じて再帰的に上位コンテナのリサイズを伝播            ← R-A4

6.5.8 要求 ID とテストケースの対応

各要求 ID には少なくとも 1 つのテストケースを用意する。詳細は §10.1 のテストフィクスチャに以下を追加する。

要求 ID フィクスチャ / シナリオ
R-A1 同じ x 列に複数の縦走行線が必要な設計(並列接続 4 本のバス)
R-A2 ソース・ターゲットの直線経路上に障害物ノードが存在する設計
R-A3 ポートが上下に配置されないことを描画後の DOM で確認
R-A4 子ノードを右下に移動して親コンテナの左上が動かないことを確認
R-A5 ノード A をドラッグ後、無関係なノード B / C の座標が変化しないことを確認
R-A6 / R-A7 トップレベル input / output 端子が最左 / 最右に配置されることを確認
R-A8 高混雑設計(edges/nodes = 3.0)で spacing が 2.0 倍になることを確認
R-A10 / R-A11 .master / .slave サフィックスを含む信号名でインスタンスが寄ることを確認
R-A12 低混雑設計でも nodeNodeBetweenLayers が 180 以上であることを確認
R-A13 同 y 座標 + 障害物なしのリンクが単一直線であることを確認
R-A14 1-to-3 のネットでトランク x が 3 ターゲットの中央値であることを確認

7. ディレクトリ構成

sv-module-viewer/
├── README.md
├── docker-compose.yml          # 開発用
├── backend/
│   ├── pyproject.toml
│   ├── uv.lock
│   ├── app/
│   │   ├── __init__.py
│   │   ├── main.py             # FastAPI エントリ
│   │   ├── api/
│   │   │   └── parse.py        # POST /api/parse
│   │   ├── verible/
│   │   │   ├── runner.py       # Verible 呼び出し
│   │   │   └── cst_visitor.py  # CST → 中間モデル
│   │   └── models.py           # dataclass 定義
│   └── tests/
│       ├── fixtures/
│       │   ├── simple.sv
│       │   └── cpu_top.sv
│       └── test_parse.py
├── frontend/
│   ├── package.json
│   ├── tsconfig.json
│   ├── vite.config.ts
│   ├── index.html
│   ├── src/
│   │   ├── main.ts             # エントリ
│   │   ├── api/
│   │   │   └── client.ts       # バックエンド呼び出し
│   │   ├── diagram/
│   │   │   ├── paper.ts          # JointJS Paper 初期化
│   │   │   ├── nodes.ts          # モジュールノード生成
│   │   │   ├── ports.ts          # ポート定義(R-A3: 左右配置のみ)
│   │   │   ├── links.ts          # リンク生成
│   │   │   ├── layout.ts         # ELK.js 自動配置(R-A6〜R-A12)
│   │   │   ├── routing.ts        # rerouteLinks(R-A1, R-A2, R-A13, R-A14)
│   │   │   ├── container.ts      # 親コンテナの左上不変リサイズ(R-A4)
│   │   │   └── dragHandlers.ts   # ドロップ時の自動再ルーティング(R-A5, F-17)
│   │   ├── ui/
│   │   │   ├── toolbar.ts
│   │   │   ├── sidebar.ts
│   │   │   └── status.ts
│   │   └── types.ts            # 型定義(API レスポンス等)
│   └── tests/
└── docs/
    ├── architecture.md
    └── api.md

8. 開発フェーズと優先順位

Phase 1: MVP(最小実用版)

目標: 1 つの SV ファイルから、モジュールを矩形 + ポートで表示し、接続線を引く。

  • Verible 呼び出しと CST 取得
  • 単純なモジュール(パラメータなし、ANSI スタイル)の解析
  • JointJS による基本描画
  • manhattan router による自動回避配線(R-A2 の基本実装)
  • ポート左右配置(R-A3
  • 手動ドラッグ配置 + ドロップ時の接続線のみ自動再ルーティング(R-A5, F-17

Phase 2: レイアウト・ルーティング高度化

§6.5 の要求仕様を完全に満たす。

  • ELK.js による自動初期レイアウト(R-A6, R-A7
  • 混雑度に応じた spacing スケール(R-A8, R-A12
  • .master / .slave サフィックスによる位置寄せヒューリスティック(R-A10, R-A11
  • 縦線重複の回避とネット別チャネル割当(R-A1
  • 直線ショートカット(R-A13
  • 1-to-N ネットの中央値トランク(R-A14
  • コンテナの左上不変リサイズ(R-A4
  • 複数ファイル対応
  • バス幅の表示
  • レイアウト JSON 保存・復元
  • パースエラー UI

Phase 3: 機能拡充

  • ノン-ANSI スタイル(旧形式)のポート宣言対応
  • SVG / PNG エクスポート
  • パラメータ表示
  • inout の整然とした表現(左右辺いずれかに自動配置)
  • 階層展開(インスタンスをダブルクリックで内部を表示)
  • ルーティング違反ケースの収集と継続改善(R-A9

Phase 4: 将来検討

  • interface / modport の完全対応(T4-01。R-A10/R-A11 のヒューリスティックを正式解析に置き換え)
  • generate ブロック展開
  • 検索・フィルタ機能
  • ダーク/ライトテーマ切替
  • ブラウザ単体動作(WASM 版 Verible でフロントエンドのみで完結)

9. 非機能要件

9.1 性能

指標 目標値
100 モジュール程度のプロジェクトの解析時間 5 秒以内
50 ノード描画時のフレームレート 30 fps 以上
初回ページロード 3 秒以内(CDN 経由)

9.2 ブラウザ対応

  • Chrome / Edge: 最新版およびその 1 つ前のメジャーバージョン
  • Firefox: 最新版
  • Safari: 最新版(macOS 14 以降)
  • IE / レガシー Edge: 非対応

9.3 セキュリティ

  • アップロードされたファイルはサーバ側で一時ディレクトリに保存し、処理完了後に削除
  • Verible はサンドボックスとして subprocess で実行(タイムアウト 30 秒)
  • ファイルサイズ上限: 1 ファイル 10 MB、合計 50 MB
  • CORS: 開発時のみ全許可、本番は明示的なオリジン指定

9.4 配布

  • Docker Compose で docker compose up の 1 コマンドで起動可能とする
  • 各サービスを個別の Docker イメージで配布
    • sv-module-viewer-backend(Python + Verible 同梱)
    • sv-module-viewer-frontend(nginx + 静的ファイル)

10. テスト方針

10.1 バックエンド単体テスト

  • pytest を使用
  • テストフィクスチャ
    • simple.sv: 1 モジュール、ポート 2 本
    • hierarchy.sv: 親 + 子モジュール 2 個
    • bus_width.sv: 各種ビット幅のポート
    • non_ansi.sv: 旧形式のポート宣言
    • syntax_error.sv: 故意の構文エラー
  • 各フィクスチャで ParsedDesign の期待値を比較

10.2 フロントエンド単体テスト

  • Vitest を使用
  • JointJS の描画は jsdom 上で簡易検証(DOM 上の要素数、属性)
  • E2E は Playwright で「ファイルアップロード → 描画 → ドラッグ → 保存」のシナリオ

10.3 受け入れテスト

実プロジェクト(例: PicoRV32、ibex 等のオープンソース RISC-V コア)を読み込ませ、目視で接続図の妥当性を確認する。


11. リスクと対応

リスク 影響 対応
Verible が一部 SV 構文に未対応 既知の未対応構文をドキュメント化、エラー時はメッセージ表示
大規模設計でリンク数が増えると重い 仮想化(画面外ノードを非描画)、信号フィルタ機能を Phase 3 で導入
manhattan router が密集時に経路を見つけられない maximumLoops を増やす、代替で metro を試す、最終手段は折れ線フォールバック
Verible バイナリのビルド・配布 公式リリースバイナリをダウンロードして Docker イメージに同梱
JointJS の MPL ライセンス遵守 JointJS 由来ファイルを別ディレクトリに隔離、ライセンス表示を README に明記
R-A1(縦線重複禁止)の実装難度 ネット ID 別のチャネル割当アルゴリズムを Phase 2 で導入。違反ケースは E2E で自動検出
R-A2(ノード完全回避)が manhattan router で達成できないケース spacing スケール(R-A8)で予め間隔を確保。それでも違反する場合は迂回 vertices を強制挿入
R-A10/R-A11 のヒューリスティック(.master / .slave サフィックス)が実プロジェクトの命名規則と合わない サフィックスパターンを設定ファイルで上書き可能にする。完全対応は T4-01 で interface / modport の正式解析へ
R-A14 の中央値トランクが他ネットと衝突 チャネル割当(R-A1)と組み合わせ、衝突時はトランク x をオフセット
ドロップ時の自動再ルーティング(R-A5)がフレームレートを下げる デバウンス(ドロップ後 50ms 待機)と差分再ルーティング(移動ノードに接続するリンクのみ)で対応

12. 参考資料

12.1 Verible

12.2 JointJS

12.3 補助ライブラリ


13. 用語集

用語 意味
CST Concrete Syntax Tree。構文木のうち、ソース上の全トークンを保持するもの
AST Abstract Syntax Tree。CST から記号などを省いたもの
ANSI スタイル ポート宣言を module foo(input clk, output q); のように本体に書く形式
非 ANSI スタイル 古い形式で、module foo(clk, q); input clk; output q; のように分離する形式
インスタンス cpu_top u_cpu(.clk(clk), ...);u_cpu のような具体実体
トップモジュール 他から呼ばれない最上位モジュール
manhattan router JointJS の障害物回避を行う直角配線アルゴリズム(A* 探索ベース)
Reroute ノード 接続線の経路を手動で誘導するための中継ノード