本書は、SystemVerilog のソースコードを入力として、モジュール間の接続関係を視覚的に表示・編集できる Web アプリケーション(以下、本アプリ)の開発仕様を定義する。
- SystemVerilog の大規模設計では、モジュール階層と信号接続が複雑化しやすく、コードのみでは全体像の把握が困難
- 既存の商用 EDA ツール(Vivado RTL Viewer、Sigasi 等)は高機能だが、ライセンスや導入コストの制約がある
- 軽量・OSS のみで構成され、Web ブラウザだけで動作するビューアが求められる
- SystemVerilog ファイル(複数)を解析し、モジュール定義・インスタンス・ポート接続を抽出する
- モジュールを矩形、ポートを入出力端子、接続を線として描画する
- 接続線がノードと重ならないよう、直角配線で自動的にノードを迂回する
- ユーザーがノードをドラッグして配置を整えられる(編集機能)。ドロップ時は接続線のみが自動再ルーティングされる
- レイアウト・配置情報を JSON で保存・復元できる
- §6.5 に定義する「レイアウト・ルーティング要求仕様」(R-A1〜R-A14、F-17)を遵守する
- 論理合成や回路図(ゲートレベル)生成は対象外
- シミュレーション・タイミング解析は対象外
- SystemVerilog の
interface/modport/ UVM コンポーネントの完全な表現は対象外(将来検討)
┌─────────────────────────────────────────────────────────────┐
│ ブラウザ (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 整形 │ │
│ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
| レイヤ | 技術 | 用途 |
|---|---|---|
| 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 |
- 本アプリのソースコードは MIT または Apache-2.0 で公開
- 依存ライブラリの主要ライセンスは以下を許容
- Verible: Apache-2.0
- JointJS Core: MPL-2.0(改変時は当該ファイルのみ MPL のまま再頒布)
- FastAPI / Vite / TypeScript: MIT
| 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) ※手動「再ルーティング」ボタンは設けない |
必須 |
- ユーザーがブラウザで本アプリを開く
- 「ファイルを選択」ボタンから
.sv/.vファイルを 1〜複数選択 - 「解析」ボタンをクリック → サーバが Verible でパース
- 解析成功時、トップモジュール候補がドロップダウンに表示される
- ユーザーがトップモジュールを選択 → ダイアグラムが描画される
- ユーザーは必要に応じてノードをドラッグして配置を整える
- 「保存」ボタンでレイアウト JSON をダウンロード
- 次回以降「読み込み」ボタンで JSON を読み込み、レイアウト復元
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]]
# 親モジュール名 → インスタンス一覧サーバ → フロントエンドへ返す 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" }
]
}
]
}
}
}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]" } } }]
}
]
}
}| メソッド | パス | 用途 |
|---|---|---|
POST |
/api/parse |
SV ファイルを受け取り、解析結果 JSON を返す |
GET |
/api/health |
ヘルスチェック |
リクエスト: 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'" }
]
}verible-verilog-syntax --export_json --printtree path/to/file.sv--export_json フラグを使用することで、CST を JSON 形式で取得できる。Verible には Python ラッパー verible_verilog_syntax.py が同梱されており、これを利用して CST を Python オブジェクトとして扱う。
Verible CST のノードタグから以下を抽出する。
| 抽出対象 | CST タグ |
|---|---|
| モジュール定義 | kModuleDeclaration |
| モジュール名 | kModuleHeader 直下の SymbolIdentifier |
| ポート宣言(ANSI スタイル) | kPortDeclaration 配下の kPortDirection + SymbolIdentifier + kPackedDimensions |
| インスタンス | kModuleInstantiation / kGateInstantiation |
| インスタンス名 | kGateInstance 直下の SymbolIdentifier |
| 名前付き接続 | kActualNamedPort (.port_name(signal) 形式) |
| 順序接続 | kActualPositionalPort |
トップモジュール候補は「他のモジュールからインスタンス化されていないモジュール」とする。
- パースエラー時は HTTP 200 で
status: "error"を返す(HTTP 4xx は使わない) - ファイル形式不正(
.sv/.v以外)は HTTP 400 - Verible 未インストール時は HTTP 500 で
{ "error": "verible not found" }
┌──────────────────────────────────────────────────────────┐
│ ヘッダ: アプリ名 [ファイル選択] [解析] [保存] [読込] │
├──────┬───────────────────────────────────────────────────┤
│ │ │
│ 左 │ ダイアグラム表示エリア │
│ ペイン│ (JointJS Paper) │
│ │ │
│ - モ │ │
│ ジュ │ │
│ ール │ │
│ 一覧 │ │
│ - ト │ │
│ ップ │ │
│ 選択 │ │
│ │ │
├──────┴───────────────────────────────────────────────────┤
│ ステータスバー: パース結果 / エラー / 選択中ノード情報 │
└──────────────────────────────────────────────────────────┘
- 形状: 角丸矩形(
standard.Rectangle) - サイズ: 幅 =
max(180, ポート最大ラベル幅 + 80)、高さ =max(80, max(入力数, 出力数) * 25 + 40) - 色: 種別ごとに固定パレット(後述)
- ラベル: 上段にインスタンス名、下段にモジュール名(小さく)
- 例:
┌────────────────┐ │ u_alu │ ← インスタンス名(大) │ (alu) │ ← モジュール名(小) │ ●a y● │ │ ●b │ └────────────────┘
input: 左辺に配置、青色(#8ECAE6)、magnet: 'passive'(リンク終点になれる)output: 右辺に配置、橙色(#FFB703)、magnet: true(リンク始点になれる)inout: 左辺または右辺(R-A3 により上下辺は不可)に配置、緑色(#95D5B2)。原則として、信号の駆動方向が読み取れない場合は左辺扱いとする- ポートサイズ: 半径 6px の円
- ラベル: ポート円の内側方向にテキスト、フォントサイズ 11px
- ルーター:
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])
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()
});ユーザーが新規にリンクを引いた際の検証ルール。
- 出力 → 入力 のみ許可
- 同一モジュール内の自己ループは禁止
- ポート以外の場所への接続禁止(
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';
}| 操作 | 効果 |
|---|---|
| ノードをドラッグ | ノードの移動。接続線は自動再ルーティング |
| キャンバスをドラッグ(空きスペース) | パン |
| マウスホイール | ズームイン・アウト |
| ノードをクリック | 選択状態(青枠ハイライト) |
| 出力ポートからドラッグ | 新規リンクの作成(編集モード時のみ) |
| リンク上で右クリック | コンテキストメニュー(削除のみ。Reroute 追加は F-17 により提供しない) |
Ctrl+S |
レイアウト JSON 保存 |
Ctrl+O |
レイアウト JSON 読み込み |
初期レイアウトおよび再レイアウト時に遵守すべき要求仕様を 5 つの観点で定義する。これらは F-16(自動初期レイアウト)および F-17(自動再ルーティング)を実現するための必須要件である。詳細は .aiprj/AI_PRJ_REQUIREMENTS.md §2 / §7 を参照。
| ID | 要求 |
|---|---|
| R-A5 | ノードを移動した時は 接続線のみを再レイアウト する。ノードは動かさない。ドラッグされたノードはユーザーがドロップした位置にそのまま留まる。兄弟ノード・無関係なノードも位置を保つ。親コンテナは R-A4 によりリサイズされ得るが、左上座標は変化しない。 |
実装方針(JointJS):
element:pointerupイベントをハンドルし、当該ノードに接続するリンクのみlinkView.requestConnectionUpdate()を呼ぶ- 他のノードの
positionには触れない - 全リンク一括再計算が必要な場合のみ
rerouteLinks()ヘルパー関数を呼ぶ(実装は §6.5.4 参照)
| ID | 要求 |
|---|---|
| F-17 | 手動の「再ルーティング」ボタンは設けない。再ルーティングはドロップ時に自動で行われ(R-A5)、専用ボタンは冗長のため廃止。 |
→ ツールバー仕様(§6.1)からは「再ルーティング」ボタンを除外する。
| 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)
};| ID | 要求 |
|---|---|
| R-A1 | 線同士の重複を禁止する。特に 縦線同士の重複は厳禁。各ネットは独自のチャネルでルーティングする。 |
| R-A2 | 線はノードに重ならず、ノードの上を通過せず、必ず迂回 する。ソース・ターゲット以外のノードを完全に避けてルーティングする。 |
| R-A3 | 線は必ず 左右方向でポートに接続 する。入出力ポートは左辺・右辺のみに配置し、上下辺には置かない。 |
| R-A4 | コンテナ内の子ノードを移動した場合、親コンテナは「現在の左上座標を原点として」リサイズする。左上は不変で、幅・高さのみが必要に応じて拡張される。階層は再帰的に適用される。 |
| R-A9 | 上記の禁止事項を実際に満たすよう、ルーティング実装を継続的に最適化する(メタ要求)。違反ケースが見つかった場合は実装を更新する。 |
実装方針:
- R-A1(線重複の回避): 各ネットに固有のチャネル(垂直走行位置)を割り当てる。同じ x 座標の縦線が重ならないよう、ネット ID をキーにオフセットを付与する。manhattan router の
stepをネットごとにずらすか、独自の router で実装する - R-A2(ノード回避): JointJS の
manhattanrouter を採用(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 }]);
});
}
}
}| ID | 要求 |
|---|---|
| R-A6 | トップレベル入力端子は 最左列 に配置する。ELK 子ノードに elk.layered.layering.layerConstraint: FIRST を付与する。 |
| R-A7 | トップレベル出力端子は 最右列 に配置する。同上、layerConstraint: LAST。inout には制約を付けない。 |
| 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.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);
}ユーザーがノードをドラッグ
↓
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
各要求 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 ターゲットの中央値であることを確認 |
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
目標: 1 つの SV ファイルから、モジュールを矩形 + ポートで表示し、接続線を引く。
- Verible 呼び出しと CST 取得
- 単純なモジュール(パラメータなし、ANSI スタイル)の解析
- JointJS による基本描画
- manhattan router による自動回避配線(R-A2 の基本実装)
- ポート左右配置(R-A3)
- 手動ドラッグ配置 + ドロップ時の接続線のみ自動再ルーティング(R-A5, F-17)
§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
- ノン-ANSI スタイル(旧形式)のポート宣言対応
- SVG / PNG エクスポート
- パラメータ表示
-
inoutの整然とした表現(左右辺いずれかに自動配置) - 階層展開(インスタンスをダブルクリックで内部を表示)
- ルーティング違反ケースの収集と継続改善(R-A9)
-
interface/modportの完全対応(T4-01。R-A10/R-A11 のヒューリスティックを正式解析に置き換え) -
generateブロック展開 - 検索・フィルタ機能
- ダーク/ライトテーマ切替
- ブラウザ単体動作(WASM 版 Verible でフロントエンドのみで完結)
| 指標 | 目標値 |
|---|---|
| 100 モジュール程度のプロジェクトの解析時間 | 5 秒以内 |
| 50 ノード描画時のフレームレート | 30 fps 以上 |
| 初回ページロード | 3 秒以内(CDN 経由) |
- Chrome / Edge: 最新版およびその 1 つ前のメジャーバージョン
- Firefox: 最新版
- Safari: 最新版(macOS 14 以降)
- IE / レガシー Edge: 非対応
- アップロードされたファイルはサーバ側で一時ディレクトリに保存し、処理完了後に削除
- Verible はサンドボックスとして
subprocessで実行(タイムアウト 30 秒) - ファイルサイズ上限: 1 ファイル 10 MB、合計 50 MB
- CORS: 開発時のみ全許可、本番は明示的なオリジン指定
- Docker Compose で
docker compose upの 1 コマンドで起動可能とする - 各サービスを個別の Docker イメージで配布
sv-module-viewer-backend(Python + Verible 同梱)sv-module-viewer-frontend(nginx + 静的ファイル)
- pytest を使用
- テストフィクスチャ
simple.sv: 1 モジュール、ポート 2 本hierarchy.sv: 親 + 子モジュール 2 個bus_width.sv: 各種ビット幅のポートnon_ansi.sv: 旧形式のポート宣言syntax_error.sv: 故意の構文エラー
- 各フィクスチャで
ParsedDesignの期待値を比較
- Vitest を使用
- JointJS の描画は jsdom 上で簡易検証(DOM 上の要素数、属性)
- E2E は Playwright で「ファイルアップロード → 描画 → ドラッグ → 保存」のシナリオ
実プロジェクト(例: PicoRV32、ibex 等のオープンソース RISC-V コア)を読み込ませ、目視で接続図の妥当性を確認する。
| リスク | 影響 | 対応 |
|---|---|---|
| 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 待機)と差分再ルーティング(移動ノードに接続するリンクのみ)で対応 |
- 公式: https://github.com/chipsalliance/verible
verible-verilog-syntaxドキュメント: https://chipsalliance.github.io/verible/verilog_syntax.html- CST JSON エクスポート例:
verilog/tools/syntax/export_json_examples/ - Python ラッパー:
verible_verilog_syntax.py
- 公式: https://www.jointjs.com/
- ドキュメント: https://docs.jointjs.com/
- API リファレンス: https://docs.jointjs.com/api/
- routers: https://docs.jointjs.com/api/routers/
- ポート設定: https://docs.jointjs.com/api/dia/Element/
- GitHub: https://github.com/clientIO/joint
- ライセンス: MPL-2.0
- ELK.js: https://github.com/kieler/elkjs(EPL-2.0、自動レイアウト用)
- FastAPI: https://fastapi.tiangolo.com/
- Vite: https://vitejs.dev/
| 用語 | 意味 |
|---|---|
| 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 ノード | 接続線の経路を手動で誘導するための中継ノード |