Skip to content

Latest commit

 

History

History
225 lines (187 loc) · 12.4 KB

File metadata and controls

225 lines (187 loc) · 12.4 KB

Architecture

日本語版はこちら

This document describes how ModView, the SystemVerilog module interconnection viewer, is put together. It complements .aiprj/AI_PRJ_DESIGN.md.

Overview

┌────────────────────────────── Browser ──────────────────────────────┐
│  index.html                                                          │
│  main.ts ── orchestration                                            │
│    ├── ui/toolbar.ts   file pick, parse, save/load, zoom, edit lock,  │
│    │                   re-layout, keyboard shortcuts                  │
│    ├── ui/sidebar.ts   module list + top-module selector             │
│    ├── ui/status.ts    status / error bar (+ "View errors" link)     │
│    ├── ui/errors.ts    parse-error overlay panel                     │
│    ├── api/client.ts   fetch POST /api/parse, GET /api/health         │
│    └── diagram/                                                       │
│          paper.ts     JointJS Paper: pan, zoom, selection, edit lock  │
│          nodes.ts     module-instance + boundary-I/O elements         │
│          ports.ts     port groups (in / out / inout / interface)      │
│          links.ts     net-based link inference, trunks + styling      │
│          hierarchy.ts nested-module (container) diagram building      │
│          layout.ts    ELK layered layout, drop re-route, re-layout    │
│          routing.ts   ELK edge sections → JointJS link vertices       │
│          container.ts drop-time container refit                       │
└───────────────────────────────┬──────────────────────────────────────┘
                                 │ multipart upload / JSON
┌───────────────────────────────┴──────────────────────────────────────┐
│                          Backend (FastAPI)                            │
│  app/main.py            app + CORS + / + /api/health                  │
│  app/api/parse.py       POST /api/parse (upload limits, ok/partial)   │
│  app/verible/runner.py  invoke verible-verilog-syntax                 │
│  app/verible/cst_visitor.py  CST → intermediate model                 │
│  app/models.py          Port / ModuleDef / ModuleInstance / interface │
│  app/verible/verible_verilog_syntax.py  (vendored Verible wrapper)     │
└────────────────────────────────────────────────────────────────────────┘

Parsing pipeline (backend)

  1. POST /api/parse reads the multipart form with an elevated max_files, validates extensions and size limits, and writes the uploads to a temporary directory.
  2. runner.parse_files calls verible-verilog-syntax --export_json --printtree through the vendored Python wrapper, yielding a concrete syntax tree (CST).
  3. cst_visitor.build_design walks the CST:
    • kModuleDeclarationModuleDef
    • ANSI ports from kPortDeclaration; non-ANSI ports from header kPort names cross-referenced with body kModulePortDeclaration directions
    • interface-typed ANSI ports (pcie_avst_if.slave tx) from kInterfacePortHeaderdirection: "interface" plus interface_name and modport
    • kInterfaceDeclarationInterfaceDef, with body signals and kModportDeclarationModport views
    • instantiations from kInstantiationBase / kGateInstance; an instantiation whose type is a known interface becomes an InterfaceInstance of the parent instead of a module instance
    • named connections from kActualNamedPort; a .bus(axi_bus.master) signal is split into interface_instance + modport
  4. Top-module candidates are modules never instantiated elsewhere.
  5. The temporary directory is removed and the design is returned as JSON.

The design is built from whatever Verible managed to parse. Files with syntax errors do not block the rest: the response carries both the extracted design and the per-file error list (see status: "partial" in api.md).

Verible CST quirks

In .port(signal) named connections Verible sometimes tags the port-name token by its literal text instead of SymbolIdentifier. cst_visitor._dot_name therefore locates the name positionally — the child immediately after the . token — rather than by tag.

For interface-typed ports, iter_find_all's traversal order is not stable between the nested and the direct identifier of a kInterfacePortHeader, so _interface_port_info walks the subtree with an explicit pre-order iteration instead.

Rendering pipeline (frontend)

  1. client.parseFiles posts the files and receives the design JSON.
  2. sidebar.setDesign fills the top-module dropdown (grouped into Top candidates / Other modules) and the click-selectable module list — any module in the design may be rendered as the top, not only a candidate.
  3. For the selected top module, hierarchy.buildHierarchy:
    • creates one boundary-I/O node per top-level port (createIoNode)
    • creates one node per instance, with ports from the instantiated module's definition (createModuleNode)
    • infers links by matching signal names (analyzeTopModule)
    • recurses into instances that themselves contain instances, embedding them as nested containers
    • emits the ELK graph that mirrors the JointJS graph
  4. layout.applyHierarchicalLayout runs ELK over that graph and writes the result back: node positions and sizes, ELK-computed port coordinates, and the edge routes (routing.applyElkRoutingToLinks).

Layout and routing

ELK.js does both node placement and orthogonal edge routing (applyHierarchicalLayout, elk.algorithm: layered, elk.direction: RIGHT, elk.hierarchyHandling: INCLUDE_CHILDREN, elk.edgeRouting: ORTHOGONAL). Because ELK computes every edge in the same pass, it can keep the lines from overlapping each other as well as the node bodies. Its bend points become the JointJS link vertices, and each link's own router is set to normal so JointJS draws exactly the path ELK produced. Spacing scales with the design's congestion (edges per node), so denser diagrams get wider channels.

Two events re-compute the wiring afterwards, and they behave differently:

  • Dropping a dragged node (automatic). The node stays exactly where it was released and no other node moves. The parent container refits around its children on all four edges (container.resizeContainerToFit), then layout.rerouteAll re-routes every link in the graph — not just the ones touching the dragged node — using JointJS' manhattan router. Routing each link independently keeps the drop position exact and every segment orthogonal, at the cost of ELK's cross-link channel separation.
  • The Re-layout button (explicit). layout.fullRelayout re-runs the whole ELK pipeline over the current graph, restoring initial-grade wiring. ELK chooses the node positions again, so a manual arrangement is intentionally discarded — that is the trade-off the button exists to offer.

.aiprj/AI_PRJ_REQUIREMENTS.md §7.9 records why the drop path cannot use ELK: elk.layered treats a node's supplied position as advisory and reflows it, so routing a drop through ELK would move the node the user just placed.

Edit lock

The diagram starts locked (paper.ts Diagram, editLocked = true). Locking calls paper.setInteractivity(false), which disables JointJS' built-in interactions — element drag, link creation, vertex and label editing. Custom handlers are not covered by that call, so the selection click, the trunk expand/collapse click and all drag-tracking listeners are individually gated on the flag as well; drag tracking staying dormant is what guarantees a locked pan never rewrites a link's vertices. While locked, pointerdown on a node or a link pans the paper just like pointerdown on blank canvas.

Link inference

Links are inferred per net by links.analyzeTopModule:

  1. Each connection's signal string is reduced to a base identifier — bit-selects (data_o[0]data_o), interface member access (bus.cmdbus) and leading unary operators (~rstrst) are stripped; numeric / sized literals such as 8'h01 are ignored.
  2. Every port wired to the same base signal forms a net. One driver is chosen (a known instance output or a top-level input); links are drawn from it to every other port on the net.

Link appearance encodes the signal: stroke width grows with bus width, clock and reset nets get their own colours, and the centre label shows the signal name with its bit range (data[31:0]).

Interface trunks

A connection that binds an interface — either explicitly (.bus(axi_bus.master)) or through a child port declared as <interface>.<modport> <name> whose bound signal resolves to an interface instance of the parent — carries the interface instance, interface name and modport on the resulting link. All links sharing an interface instance between the same two nodes are collapsed into a single trunk: twice the normal stroke width, labelled interface.modport, and coloured by hashing the instance name so the same bus always gets the same colour.

Clicking a trunk (while unlocked) expands it into faded per-signal links with the trunk kept as a dotted backdrop; clicking again collapses it. Expanding is purely visual — it never triggers a re-route.

The modport also decides which side of the node the port sits on: a master-like modport drives, so its port is placed on the right edge; a slave-like modport receives and is placed on the left.

Black-box modules

If a sub-module's definition was not uploaded, its port directions are unknown. The analyser then synthesises a definition from the instance's own connections and infers each port's direction from how its net is driven (a port on a net with a known driver becomes an input; the sole unknown port on an otherwise-undriven net becomes the driver/output). This lets a design be viewed even when only the top-level file is provided.

Instances with no connections (e.g. SystemVerilog interface instances) are not drawn as nodes; their names act as nets that wire other instances together.

Nested-module display

hierarchy.ts builds a nested diagram: an instance whose module contains further instances is drawn as a container with those instances embedded inside it (JointJS embed), recursively, with a depth/cycle guard. Each module level's connectivity is computed by reusing analyzeTopModule, and its cell ids are remapped to hierarchy-qualified ids (parent/child). ELK lays the whole hierarchy out in one pass (elk.hierarchyHandling: INCLUDE_CHILDREN, which also sizes each container to fit its children plus its ports and labels), and routing.applyElkRoutingToLinks walks the result recursively, adding each container's absolute offset to the bend points it contains — JointJS works in absolute paper coordinates while ELK emits parent-relative ones.

An instance that looks expandable but ends up with no rendered children (for example, one whose sub-instances are all connection-less interface instances) is demoted to a leaf node, because ELK would otherwise size the empty container as 0×0 and it would render as nothing at all.

Persistence

The save format wraps JointJS' native graph.toJSON():

{ "version": "1.0", "topModule": "cpu_top", "graph": { "cells": [ ... ] } }

version here is the save-format schema version, not the application version (which is reported by GET /api/health and recorded in ../CHANGELOG.md). The two are deliberately independent.

The custom svmv.Module element is registered into the JointJS cell namespace so that graph.fromJSON() can reconstruct saved diagrams. A file saved after a drop stores router: manhattan with no vertices — the path is recomputed on render — while one saved after an initial layout or a Re-layout stores router: normal plus the ELK-computed vertices.