diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index c104738..012b292 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -76,13 +76,15 @@ jobs: test: needs: build runs-on: ${{ matrix.os }} + # A request that errors leaves some tests waiting for a reply: fail, don't hang. + timeout-minutes: 20 strategy: fail-fast: false matrix: os: [ ubuntu-latest, macos-latest ] python-version: [ "3.10", "3.13" ] # Oldest najaeda the package declares support for, and the newest. - najaeda: [ "==0.7.29", "" ] + najaeda: [ "==0.7.34", "" ] steps: - name: Checkout code uses: actions/checkout@v5 diff --git a/.gitignore b/.gitignore index f31cee7..a423906 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ imgui.ini # Private, local-only working notes (never shared/committed). notes/ naja_sv_diagnostics.log +naja_vhdl_diagnostics.log # Viewer bundle: built by CI (-DNAJA_SCHEMATIC_WASM_MODULE=ON), shipped in the wheel. python/naja_schematic/static/naja-schematic.js diff --git a/CLAUDE.md b/CLAUDE.md index 6969db0..8b49257 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,11 +88,17 @@ backend is chosen per-platform in `CMakeLists.txt`: `NativeFileDialog.mm` Windows has no backend and isn't a supported target. Run directly, optionally with a netlist to load (dispatched by extension — -`.v` → Verilog, `.sv` → SystemVerilog via slang, otherwise treated as an SNL -directory), `--liberty ` (repeatable, once per file — defines the -primitive cell library for a `.v` design; not supported for `.sv`, since -`LocalSNLProvider::loadSystemVerilog()`/`SNLSVConstructor` has no liberty -hook, and the CLI fails loudly rather than silently ignoring it), a +`.v` → Verilog, `.sv` → SystemVerilog via slang, `.vhd`/`.vhdl` → VHDL via +naja's experimental frontend (`LocalSNLProvider::loadVHDL()`, +`VHDLConstructor::constructFiles()`; the only kind that takes several +positional files, in any order -- naja resolves the units across them), +otherwise treated as an SNL directory), `--top ` (`.sv`/VHDL only: +forces the top module/entity instead of inferring it), `--liberty ` +(repeatable, once per file — defines the +primitive cell library for a `.v` design; not supported for `.sv` or VHDL, +since `LocalSNLProvider::loadSystemVerilog()`/`SNLSVConstructor` and +`loadVHDL()` have no liberty hook, and the CLI fails loudly rather than +silently ignoring it), a diagnosis JSON to pre-load (`--diagnosis ` — see `loadDiagnosisFile()` in `AppLogic.cpp`), and/or directories to look up relative source file references in (`--source-root `, repeatable; see @@ -169,14 +175,19 @@ python3 scripts/najaeda_server.py --systemverilog a.sv b.sv --top top -D SYNTHES python3 scripts/najaeda_server.py --flist design.f --top top ``` -A design is required: either `--verilog`, or SystemVerilog via +A design is required, exactly one of: `--verilog`; SystemVerilog via `--systemverilog`/`--sv ...` and/or `--flist`/`-f ` (the two SV -inputs can be combined; `--flist` maps to `SystemVerilogConfig.flist`). -Verilog and SV are mutually exclusive. `--top`/`--define`/`-D` apply only to -SystemVerilog, and `--liberty` is rejected with SV — same restriction as the -native CLI, since the SV loader has no liberty hook. -`--allow_unknown_designs` maps to `SystemVerilogConfig.blackbox_unknown_modules` -for SV. +inputs can be combined; `--flist` is the raw loader's `flist`); or +VHDL via `--vhdl ...` (`NLDB.loadVHDL()`, files in any order). +`load_design()` uses najaeda's raw `naja` API only, like `protocol.py` +(`NLDB.loadGateLevelVerilog()`/`loadSystemVerilog()`/`loadVHDL()`/ +`loadLibertyPrimitives()`); `--top` reaches slang as +`frontend_options={"slang": ["--top", name]}` (najaeda >= 0.7.34). +`--top` applies to SystemVerilog and VHDL, `--define`/`-D` only to +SystemVerilog, and `--liberty`/`--yosys-src` are rejected with SV and VHDL — +same restriction as the native CLI, since those loaders have no liberty +hook. +`--allow_unknown_designs` maps to the loaders' `blackbox_unknown_modules`. Note the flag shapes are *not* symmetric with the native CLI above: the server takes `--verilog ` (singular) + `--liberty ...` (one flag, @@ -210,7 +221,7 @@ WASM viewer, so najaeda users get a viewer from `pip install` alone: `--open`, `--diagnosis `, `--stdio`, `--source-root ` (repeatable; `source_roots=` on `ViewerSession`/`ViewerServer`/`show()`), `--source-map FROM=TO` (repeatable; `source_maps=` there too), - `--yosys-src` (with `--verilog`: `VerilogConfig.yosys_source_attributes`)) + `--yosys-src` (with `--verilog`: `loadGateLevelVerilog(yosys_source_attributes=)`)) and its two transports: a WebSocket server that also serves the viewer page on the same port (`static/index.html` sets `Module.najaWsUrl` from `location`, query @@ -485,14 +496,24 @@ occurrence's enclosing modules. Instance entries (`instances_response`/`primitives_response` children, equipotential occurrences, `instance_resolved`'s `instance`) carry a `primitive_type` (`"and"`, `"nand"`, `"or"`, `"nor"`, `"xor"`, `"xnor"`, -`"inv"`, `"buf"`, `"dff"`, `"assign"`, `"tie0"`, `"tie1"`, `"unknown"`) that picks the gate +`"inv"`, `"buf"`, `"dff"`, `"assign"`, `"tie0"`, `"tie1"`, `"mux"`, +`"add"`, `"sub"`, `"mul"`, `"shl"`, `"shr"`, `"ashr"`, `"divmod"`, +`"unknown"`) that picks the gate symbol `SchematicView::drawInstance()` draws, and term entries carry `"clock": true` on a sequential cell's clock pin (the DFF clock notch). Both come from naja's modeling of the cell -- `SNLDesignModeling`'s truth-table checks (`isAnd`, ..., `isConst0`/`isConst1` for tie cells, whose -Liberty `function` is `"0"`/`"1"`), `isSequential` and `isClock` -- and +Liberty `function` is `"0"`/`"1"`; `isMux`, at any width, since a bus mux +is one NLDB0 mux2), `isSequential` and `isClock` -- and **never from a cell or pin name**: a cell naja has no model for (e.g. -gate-level Verilog without Liberty) is `"unknown"` and draws as a box. +gate-level Verilog without Liberty) is `"unknown"` and draws as a box. A +mux is a trapezoid (`SchematicLayout::muxSlant()`), the word-level +operators SystemVerilog lowers to (`NLDB0::isAddSub()`/`isSubtract()`, +`isMultiply()`, `isShift()` + `getShiftSignature()`, `isDivMod()`) are +boxes with a circled operator glyph (`isOperatorSymbol()`, +`kOperatorGlyphW`). `protocol.py` uses najaeda's bindings of the same +checks (`SNLDesign.isAddSub()`, `isSubtract()`, `isMultiply()`, +`isShift()`, `isShiftRight()`, `isShiftArithmetic()`, `isDivMod()`). `diagnosis_response` is different: it's a **server push**, not a reply to a request (a diagnosis run finishes on its own schedule), and it *annotates* @@ -524,15 +545,16 @@ all, so it gets diagnosis data via **File > Load Diagnosis JSON...** `DiagnosisItem` parser) instead. An item with `id_path` matches that instance only; one with only `path` matches by names, and never through an anonymous (`""`) level. In Python, an item's `path` may also be a -najaeda `netlist.Instance`, turned into both by `diagnosis_response()`. +naja `SNLPath` or a najaeda `netlist.Instance` (read through `pathIDs`, +without calling `netlist`), turned into both by `diagnosis_response()`. Getting to one hierarchical instance works in both directions, keyed by the same `id_path`/`path` pair as diagnosis items: - **Host → viewer.** `focus_instance` is a server push, `{"response":"focus_instance","path":["u1","u2"],"id_path":[3,7]}`, from - `Schematic.show_instance()` / `show(instance=...)` (which take a najaeda - `Instance`, a list of ids, or a list of names), re-pushed after each + `Schematic.show_instance()` / `show(instance=...)` (which take a naja + `SNLPath` or najaeda `Instance`, a list of ids, or a list of names), re-pushed after each `root_response` like diagnoses. The viewer answers it with a `resolve_instance` request carrying the same `path`/`id_path`, which both providers implement (`protocol.py` @@ -561,7 +583,8 @@ the same `id_path`/`path` pair as diagnosis items: `get_properties` for it. The notebook widget intercepts `instance_selected` into `Schematic.selected_id_path` (and `selected_path`, the names); `Schematic.selected` returns it as a - najaeda `netlist.Instance`, and `on_select()` gives a callback (it + naja `SNLPath` (raw API, like the whole package; empty for the top), + and `on_select()` gives a callback (it observes the ids, so moving between anonymous siblings fires it). The other hosts only log it (`protocol.py`) or ignore it (`LocalSNLProvider`). @@ -663,7 +686,7 @@ offer a schematic-side "Show Properties" entry point (only terms/instances appear as boxes/pins there). **Source references** (full design: `docs/source_information.md`; needs -najaeda >= 0.7.29). naja keeps, per object, an optional *declaration* +najaeda >= 0.7.29; VHDL fills declarations since 0.7.32). naja keeps, per object, an optional *declaration* (where it's written in the loaded input: a gate's netlist line, an instance's instantiation, a design's definition) and ordered *origins* (upstream RTL, e.g. Yosys `src`), each qualified by provider/language/ diff --git a/CMakeLists.txt b/CMakeLists.txt index a74cc9e..6d73c55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -216,6 +216,7 @@ else() naja_snl_verilog naja_snl_liberty naja_snl_systemverilog + naja_snl_vhdl naja_nl_dump ${PLATFORM_FILE_DIALOG_LIBS} ) @@ -296,6 +297,7 @@ if(NOT EMSCRIPTEN_BUILD) naja_snl_verilog naja_snl_liberty naja_snl_systemverilog + naja_snl_vhdl naja_nl_dump ) target_compile_definitions(naja-schematic-provider-tests PRIVATE diff --git a/README.md b/README.md index dc46e5d..fc08b5f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ from najaeda import netlist import naja_schematic netlist.load_liberty("cells.lib") -netlist.load_verilog("design.v") +netlist.load_gate_level_verilog("design.v") naja_schematic.show() # the viewer, in the cell output ``` @@ -71,8 +71,9 @@ your own tool). nets from a JSON report, e.g. from a formal equivalence check. - **SVG export** of the schematic, from the File menu or headless from the command line. -- Loads gate-level Verilog (with Liberty), SystemVerilog, or naja SNL - netlists. +- Loads gate-level Verilog (with Liberty), SystemVerilog, VHDL + (experimental) or naja SNL netlists. Muxes are drawn as muxes, and the + adders, multipliers and shifters SystemVerilog becomes as operator boxes. ## Command-line options @@ -82,8 +83,9 @@ plus options, in any order. Their flags differ slightly: | Desktop app (`naja-schematic-standalone`) | Browser server (`naja-schematic`) | What it does | |---|---|---| | `design.v` | `--verilog design.v` | Load a gate-level Verilog netlist | -| `design.sv` | `--sv a.sv b.sv --top top` | Load SystemVerilog (elaborated with slang) | +| `design.sv --top top` | `--sv a.sv b.sv --top top` | Load SystemVerilog (elaborated with slang) | | | `--flist design.f`, `-D NAME=VALUE` | SystemVerilog command file, defines | +| `a.vhd b.vhd --top top` | `--vhdl a.vhd b.vhd --top top` | Load VHDL, files in any order (naja's experimental frontend) | | `snl_dir/` | | Load a naja-if (SNL) netlist directory | | `--liberty a.lib --liberty b.lib` | `--liberty a.lib b.lib` | Liberty cell libraries (Verilog only) | | `--diagnosis report.json` | `--diagnosis report.json` | Show a diagnosis report on the design | @@ -93,7 +95,7 @@ plus options, in any order. Their flags differ slightly: | | `--open` | Open the viewer in a browser | | | `--port N`, `--host H` | Where to serve (default `localhost:8081`) | -Without a design, the desktop app starts empty: use the **File** menu (Open Verilog..., Open SystemVerilog..., Open naja-if...). A +Without a design, the desktop app starts empty: use the **File** menu (Open Verilog..., Open SystemVerilog..., Open VHDL..., Open naja-if...). A diagnosis report can also be loaded later with **File > Load Diagnosis JSON...**. `naja-schematic --help` lists every server option. diff --git a/REUSE.toml b/REUSE.toml index 7d08e65..b98ff6b 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -127,8 +127,12 @@ path = [ "tests/VerilogLanguageTest.cpp", "tests/data/anonymous_protocol_cases.json", "tests/data/source_protocol_cases.json", + "tests/data/sv/operators.sv", + "tests/data/sv/two_tops.sv", "tests/data/sources/gate/alu_syn.v", "tests/data/sources/rtl/alu.v", + "tests/data/vhdl/leaf.vhd", + "tests/data/vhdl/top.vhd", "tests/provider/LocalSNLProviderProtocolTest.cpp", ] SPDX-FileCopyrightText = "2026 najaeda contributors" diff --git a/docs/source_information.md b/docs/source_information.md index 966e0fe..319236c 100644 --- a/docs/source_information.md +++ b/docs/source_information.md @@ -25,6 +25,9 @@ What fills them in najaeda 0.7.29: named nets and instantiations; origins for lowered logic. Provider `slang`, language `systemverilog`, representation `rtl`. The legacy slot is still written too, and the viewer hides it when typed references exist. +- **VHDL** (since najaeda 0.7.32): declarations for entities, ports, + signals and instantiations (provider `naja-vhdl`, language `vhdl`, + representation `rtl`). - **Gate-level Verilog**: every design, instance, term and declared net gets its netlist declaration (provider `naja-verilog`, language `verilog`). The spans are partial: a module's keyword and name, an diff --git a/python/README.md b/python/README.md index 597e79a..95f88d5 100644 --- a/python/README.md +++ b/python/README.md @@ -25,7 +25,7 @@ from najaeda import netlist import naja_schematic netlist.load_primitives("xilinx") -top = netlist.load_verilog("design.v") +top = netlist.load_gate_level_verilog("design.v") naja_schematic.show() # interactive view in the cell output ``` @@ -43,23 +43,25 @@ view ``` The view and your najaeda code can point each other at instances. -`show_instance()` takes a najaeda `Instance`, a list of instance ids (as -`Instance.pathIDs`) or a list of instance names (a plain string is one +`show_instance()` takes a najaeda `Instance` or a naja `SNLPath`, a list +of instance ids (as `Instance.pathIDs`) or a list of instance names (a plain string is one name, never split on `/`, since escaped names can contain it). Anonymous instances have no name to go by: reach them with an `Instance` or ids. The viewer opens its tree down to that instance, selects it, and draws it alone in the schematic. Each pin there is open, so you can click one to add its net and grow the schematic from the instance. In the other direction, the instance selected in the viewer (click it in the tree or the -schematic) is available as a najaeda `Instance`: +schematic) is available as a naja `SNLPath` (najaeda's raw API), and its +ids, which give the najaeda `Instance`, as `selected_id_path`: ```python view.show_instance(netlist.get_instance_by_path(["u_sub", "u_and"])) view.show_instance(["u_sub", "u_and"]) # same thing -inst = view.selected # what the user clicked, or None -inst.get_model_name() +path = view.selected # what the user clicked (an SNLPath), or None +path.getModel().getName() +inst = netlist.Instance(view.selected_id_path) -view.on_select(lambda inst: print("selected", inst)) +view.on_select(lambda path: print("selected", path)) ``` `naja_schematic.show(inst)` (or `show(["u_sub", "u_and"])`) starts a new @@ -95,7 +97,7 @@ server.annotate(items) # diagnosis overlay, kept across reloads server.show_instance([3, 7]) # instance ids, names, or a najaeda Instance server.on_select(lambda id_path, path: print("selected", path)) with server.replacing_design(): # holds design_lock, then viewers reload - netlist.load_verilog("other.v") + netlist.load_gate_level_verilog("other.v") server.stop() ``` diff --git a/python/naja_schematic/protocol.py b/python/naja_schematic/protocol.py index b9fc74f..bfc937e 100644 --- a/python/naja_schematic/protocol.py +++ b/python/naja_schematic/protocol.py @@ -17,7 +17,11 @@ log = logging.getLogger("naja_schematic") # 0.7.29: typed source references (getSourceDeclaration/getSourceOrigins). -MIN_NAJAEDA_VERSION = (0, 7, 29) # keep in step with pyproject.toml +# 0.7.31: load_gate_level_verilog/GateLevelVerilogConfig (load_verilog and +# VerilogConfig now warn). +# 0.7.34: word-level operator checks (isAddSub, isMultiply, isShift, isDivMod, +# ...) and loadSystemVerilog's frontend_options. +MIN_NAJAEDA_VERSION = (0, 7, 34) # keep in step with pyproject.toml def check_najaeda_version(): @@ -64,10 +68,10 @@ def has_visible_primitive_instances(design): # sequential model) -- never from its name. A cell naja has no model for is # "unknown" and draws as the generic box. LocalSNLProvider.cpp's # getPrimitiveType() mirrors this, same as the rest of the wire protocol. -# The truth-table checks are only asked of a cell with exactly one output -# bit: on a multi-output cell (NLDB0's full adder, a multi-output Liberty -# cell) naja's getTruthTable() throws, and najaeda doesn't translate that -# exception -- it aborts the process. +# The gate checks are only asked of a cell with exactly one output bit: a +# multi-output cell (NLDB0's full adder, a multi-output Liberty cell) is +# never drawn as a gate. A mux and the NLDB0 word-level operators are +# recognized at any width (a bus mux is one NLDB0 mux2 with bus pins). _PRIMITIVE_TYPE_CHECKS = ( ("isConst0", "tie0"), ("isConst1", "tie1"), @@ -88,6 +92,18 @@ def get_primitive_type(model): return "assign" if model.isSequential(): return "dff" + if model.isAddSub(): + return "sub" if model.isSubtract() else "add" + if model.isMultiply(): + return "mul" + if model.isShift(): + if not model.isShiftRight(): + return "shl" + return "ashr" if model.isShiftArithmetic() else "shr" + if model.isDivMod(): + return "divmod" + if model.isMux(): + return "mux" if output_bit_count(model) != 1: return "unknown" for check, ptype in _PRIMITIVE_TYPE_CHECKS: @@ -1051,8 +1067,12 @@ def handle_message(message): return [json.dumps(r) for r in handle_request(json.loads(message))] -def _instance_id_path(instance): - # A najaeda netlist.Instance's id path (top excluded); None for anything else. +def instance_id_path(instance): + # The id path (top excluded) of a naja SNLPath, or of a najaeda + # netlist.Instance (read through its pathIDs, without using netlist); + # None for anything else. + if isinstance(instance, naja.SNLPath): + return list(instance.getInstanceIDs()) ids = getattr(instance, "pathIDs", None) return list(ids) if ids is not None else None @@ -1072,9 +1092,10 @@ def _instance_name_path(ids): def _diagnosis_item(item): - # An item whose "path" is a najaeda netlist.Instance gets its id_path - # (what the viewer matches first) and the matching name path. - ids = _instance_id_path(item.get("path")) + # An item whose "path" is a naja SNLPath (or a najaeda netlist.Instance) + # gets its id_path (what the viewer matches first) and the matching name + # path. + ids = instance_id_path(item.get("path")) if ids is None: return item return {**item, "path": _instance_name_path(ids), "id_path": ids} @@ -1085,8 +1106,8 @@ def diagnosis_response(items): items (dicts with kind/path/id_path/terminal/severity/message/source), or from a {"items": [...]} document as File > Load Diagnosis JSON... accepts. An item names its instance by "id_path" (instance ids, - preferred) and/or "path" (instance names); "path" may also be a najaeda - netlist.Instance, turned into both here.""" + preferred) and/or "path" (instance names); "path" may also be a naja + SNLPath or a najaeda netlist.Instance, turned into both here.""" if isinstance(items, dict): items = items.get("items", []) return {"response": "diagnosis_response", "items": [_diagnosis_item(i) for i in items]} diff --git a/python/naja_schematic/server.py b/python/naja_schematic/server.py index 7c37277..6249be5 100644 --- a/python/naja_schematic/server.py +++ b/python/naja_schematic/server.py @@ -128,7 +128,7 @@ class ViewerServer: server.annotate(items); server.show_instance(instance) server.on_select(lambda id_path, path: ...) with server.replacing_design(): # viewers reload the new design, - netlist.load_verilog(other) # stale requests are dropped + netlist.load_gate_level_verilog(other) # stale requests are dropped server.stop() It is also a context manager (start on enter, stop on exit). @@ -258,14 +258,14 @@ def open_browser(self): def annotate(self, items): """Overlay diagnosis items on every connected viewer, and on any that (re)loads later (see naja_schematic.diagnosis_response() for - the item shape; an item's path may be a najaeda Instance). Pass [] + the item shape; an item's path may be an SNLPath or a najaeda Instance). Pass [] to clear.""" self._session.annotate(items) def show_instance(self, target): """Reveal, select and draw one instance in every viewer, and in any - that (re)loads later. `target`: a najaeda netlist.Instance, a list - of instance ids or of instance names (top excluded; [] = top), or + that (re)loads later. `target`: a naja SNLPath (or a najaeda + netlist.Instance), a list of instance ids or of instance names (top excluded; [] = top), or one name for a child of the top.""" self._session.show_instance(target) @@ -298,7 +298,7 @@ def replacing_design(self, diagnosis=None, instance=None): under the lock (not if the block raises):: with server.replacing_design(diagnosis=items): - netlist.load_verilog(new_design) + netlist.load_gate_level_verilog(new_design) Only with `lock` (or the default one): with `run=`, swap on the thread `run` hops to and call design_changed() there.""" @@ -333,8 +333,8 @@ def selected_path(self): @property def selected(self): - """The last selected instance as a najaeda netlist.Instance, or - None if nothing is selected or it no longer exists.""" + """The last selected instance as a naja SNLPath (empty for the top), + or None if nothing is selected or it no longer exists.""" return self._session.selected # -- server thread ------------------------------------------------------- @@ -524,22 +524,40 @@ def build_arg_parser(): parser.add_argument("--flist", "-f", type=str, metavar="FILE", help="SystemVerilog command file (slang -f syntax: sources, " "+incdir+, +define+, ...); may be combined with --systemverilog") + parser.add_argument("--vhdl", nargs="+", metavar="FILE", + help="VHDL file(s) to load, in any order (naja's experimental " + "VHDL frontend)") parser.add_argument("--top", type=str, - help="SystemVerilog only: top module to elaborate") + help="SystemVerilog/VHDL only: top module (entity) to elaborate") parser.add_argument("--define", "-D", action="append", metavar="NAME[=VALUE]", help="SystemVerilog only: preprocessor define (repeatable)") return parser +def _top_db(): + """The universe's top NLDB, creating the universe and the DB if needed.""" + from najaeda import naja + + universe = naja.NLUniverse.get() or naja.NLUniverse.create() + db = universe.getTopDB() + if db is None: + db = naja.NLDB.create(universe) + universe.setTopDB(db) + return db + + def load_design(args): - """Load the liberty files and the Verilog/SystemVerilog design named by - the parsed CLI arguments into the NLUniverse; returns the top.""" - from najaeda import netlist + """Load the liberty files and the Verilog/SystemVerilog/VHDL design + named by the parsed CLI arguments into the NLUniverse, through najaeda's + raw naja API; returns the top SNLDesign.""" + from najaeda import naja + db = _top_db() is_sv = bool(args.systemverilog or args.flist) if args.xilinx: log.info("Loading Xilinx primitives") - netlist.load_primitives('xilinx') + from najaeda.primitives import xilinx + xilinx.load(db) if args.liberty: # Expand wildcards ourselves, for shells that pass them through. @@ -549,27 +567,30 @@ def load_design(args): expanded_liberty_files.extend(glob(lib)) else: expanded_liberty_files.append(lib) - for lib in expanded_liberty_files: - log.info("Loading liberty file: %s", lib) - netlist.load_liberty(lib) - - if is_sv: + log.info("Loading liberty files: %s", ", ".join(expanded_liberty_files)) + db.loadLibertyPrimitives(expanded_liberty_files) + + if args.vhdl: + log.info("Loading VHDL: %s", ", ".join(args.vhdl)) + db.loadVHDL(args.vhdl, top=args.top, + diagnostics_report_path="naja_vhdl_diagnostics.log") + elif is_sv: sv_files = args.systemverilog or [] sources = sv_files + ([f"-f {args.flist}"] if args.flist else []) log.info("Loading SystemVerilog: %s", ", ".join(sources)) - config = netlist.SystemVerilogConfig() - config.flist = args.flist - config.top = args.top - config.defines = args.define - config.blackbox_unknown_modules = args.allow_unknown_designs - top = netlist.load_system_verilog(sv_files, config=config) + db.loadSystemVerilog( + sv_files, flist=args.flist, defines=args.define, + diagnostics_report_path="naja_sv_diagnostics.log", + blackbox_unknown_modules=bool(args.allow_unknown_designs), + frontend_options={"slang": ["--top", args.top]} if args.top else None) else: log.info("Loading Verilog netlist: %s", args.verilog) - config = netlist.VerilogConfig() - config.allow_unknown_designs = args.allow_unknown_designs - config.yosys_source_attributes = args.yosys_src - top = netlist.load_verilog(args.verilog, config=config) - log.info("Design loaded: %s", top.get_name()) + db.loadGateLevelVerilog( + [args.verilog], + blackbox_unknown_modules=bool(args.allow_unknown_designs), + yosys_source_attributes=args.yosys_src) + top = naja.NLUniverse.get().getTopDesign() + log.info("Design loaded: %s", top.getName() if top else None) return top @@ -583,17 +604,19 @@ def main(argv=None): args = parser.parse_args(argv) is_sv = bool(args.systemverilog or args.flist) - if not args.verilog and not is_sv: - parser.error("provide a design: --verilog, or --systemverilog and/or --flist") - if args.verilog and is_sv: - parser.error("--verilog cannot be combined with --systemverilog/--flist") - if not is_sv and (args.top or args.define): - parser.error("--top/--define only apply to --systemverilog/--flist") - if is_sv and args.liberty: - # Same restriction as the native standalone: the SystemVerilog loader - # has no liberty hook, so fail loudly rather than silently ignore it. - parser.error("--liberty is not supported with --systemverilog/--flist") - if is_sv and args.yosys_src: + if sum(map(bool, (args.verilog, is_sv, args.vhdl))) != 1: + parser.error("provide one design: --verilog, --systemverilog and/or --flist, " + "or --vhdl") + if args.top and not (is_sv or args.vhdl): + parser.error("--top only applies to --systemverilog/--flist/--vhdl") + if args.define and not is_sv: + parser.error("--define only applies to --systemverilog/--flist") + if (is_sv or args.vhdl) and args.liberty: + # Same restriction as the native standalone: the SystemVerilog and + # VHDL loaders have no liberty hook, so fail loudly rather than + # silently ignore it. + parser.error("--liberty only applies to --verilog") + if (is_sv or args.vhdl) and args.yosys_src: parser.error("--yosys-src only applies to --verilog") if args.stdio and args.open: parser.error("--open does not apply to --stdio") diff --git a/python/naja_schematic/session.py b/python/naja_schematic/session.py index 6fc1d5f..b265735 100644 --- a/python/naja_schematic/session.py +++ b/python/naja_schematic/session.py @@ -72,27 +72,26 @@ def is_id(value): def instance_paths(target): """Normalize what show_instance() accepts to (names, ids): the instance's name path and id path, top excluded ([] = the top design). `target` is a - najaeda netlist.Instance, a list/tuple of instance ids (ints, as najaeda's - Instance.pathIDs), a list/tuple of names, or a single name for a child of - the top design. A string is one name, never split on "/": escaped names + naja SNLPath (or a najaeda netlist.Instance), a list/tuple of instance + ids (ints, as SNLPath.getInstanceIDs()), a list/tuple of names, or a + single name for a child of the top design. A string is one name, never split on "/": escaped names can contain it. Ids reach anonymous instances; names can't. Raises ValueError if the path doesn't resolve in the loaded design.""" - if hasattr(target, "pathIDs"): # najaeda.netlist.Instance - ids = list(target.pathIDs) + ids = protocol.instance_id_path(target) + if ids is not None: + names = None elif isinstance(target, str): - ids = None names = [target] elif isinstance(target, (list, tuple)): if target and all(is_id(seg) for seg in target): ids = list(target) elif all(isinstance(seg, str) for seg in target): - ids = None names = list(target) else: raise TypeError("expected a path of instance names (str) or of " f"instance ids (int), got {list(target)!r}") else: - raise TypeError(f"expected a najaeda Instance, a list of names or ids, " + raise TypeError(f"expected an SNLPath, a najaeda Instance, a list of names or ids, " f"or a name, got {type(target).__name__}") if ids is None: ids = names_to_ids(names) @@ -111,9 +110,9 @@ def instance_path(target): def selected_instance(id_path, path): - """A selection (id path and/or name path) as a najaeda netlist.Instance - (the top design for []), or None if there is none or it no longer - exists in the netlist.""" + """A selection (id path and/or name path) as a naja SNLPath (empty for + the top design), or None if there is none or it no longer exists in the + netlist.""" if id_path is not None: ids = id_path if ids_to_names(ids) is None: @@ -124,8 +123,7 @@ def selected_instance(id_path, path): return None else: return None - from najaeda import netlist - return netlist.Instance(ids) + return protocol.get_path(naja.NLUniverse.get().getTopDesign(), ids) def lock_runner(lock): @@ -380,8 +378,8 @@ def design_changed(self, diagnosis=None, instance=None): @property def selected(self): - """The viewer's selected instance as a najaeda netlist.Instance, or - None (see selected_instance()).""" + """The viewer's selected instance as a naja SNLPath, or None (see + selected_instance()).""" with self._state_lock: ids, path = self.selected_id_path, self.selected_path return selected_instance(ids, path) diff --git a/python/naja_schematic/widget.py b/python/naja_schematic/widget.py index 2e123ed..97740ea 100644 --- a/python/naja_schematic/widget.py +++ b/python/naja_schematic/widget.py @@ -80,22 +80,22 @@ def annotate(self, items): `items`: a list of diagnosis dicts -- kind ("instance"|"net"), id_path (instance ids, root excluded; preferred) and/or path - (instance names, root excluded; or a najaeda netlist.Instance, - turned into both), terminal (nets only), severity + (instance names, root excluded; or a naja SNLPath / najaeda + netlist.Instance, turned into both), terminal (nets only), severity ("info"|"warning"|"error"), message, source -- or a {"items": [...]} document, as File > Load Diagnosis JSON... reads. Anonymous instances can only be flagged by id_path (or an - Instance). Pass [] to clear. + SNLPath/Instance). Pass [] to clear. """ self._session.annotate(items) def show_instance(self, target): """Bring one hierarchical instance into view. - `target`: a najaeda netlist.Instance, a list of instance ids or of - instance names (top excluded; [] = the top design), or one name for - a child of the top. Anonymous instances are reached by Instance or - ids. The viewer opens the tree down to it, selects it, shows its + `target`: a naja SNLPath (or a najaeda netlist.Instance), a list of + instance ids or of instance names (top excluded; [] = the top + design), or one name for a child of the top. Anonymous instances are + reached by path or ids. The viewer opens the tree down to it, selects it, shows its properties, and draws it alone in the schematic with all its pins, ready to be extended pin by pin. Kept across view reloads. """ @@ -132,9 +132,11 @@ def design_changed(self, diagnosis=None, instance=None): @property def selected(self): - """The instance selected in the viewer, as a najaeda netlist.Instance - (the top design for a selected root), or None if nothing is selected - or it no longer exists in the netlist.""" + """The instance selected in the viewer, as a naja SNLPath (empty for + a selected root; `getTailInstance()`/`getModel()` give the instance + and its model), or None if nothing is selected or it no longer + exists in the netlist. `selected_id_path` has the same selection as + ids (najaeda's `netlist.Instance(ids)` takes them).""" return selected_instance(self.selected_id_path, self.selected_path) def on_select(self, callback): diff --git a/python/notebooks/getting_started.ipynb b/python/notebooks/getting_started.ipynb index cb712a4..65f1b13 100644 --- a/python/notebooks/getting_started.ipynb +++ b/python/notebooks/getting_started.ipynb @@ -177,7 +177,7 @@ "metadata": {}, "source": [ "## 5. Read the selection back\n", - "Click an instance in the tree or a box in the schematic. The callback prints on each click, and the next cell gives the selection as a najaeda `Instance`." + "Click an instance in the tree or a box in the schematic. The callback prints on each click, and the next cell reads the selection: its ids, `selected_id_path`, give the najaeda `Instance`." ] }, { @@ -198,7 +198,8 @@ "outputs": [], "source": [ "print(\"selected_path:\", view.selected_path)\n", - "inst = view.selected\n", + "ids = view.selected_id_path\n", + "inst = netlist.Instance(ids) if ids is not None else None\n", "print(inst, \"->\", inst.get_model_name() if inst else None)" ] }, diff --git a/python/notebooks/tinyrocket.ipynb b/python/notebooks/tinyrocket.ipynb index 2886200..63f77ff 100644 --- a/python/notebooks/tinyrocket.ipynb +++ b/python/notebooks/tinyrocket.ipynb @@ -54,9 +54,9 @@ "from najaeda import netlist\n", "netlist.reset()\n", "netlist.load_liberty([f\"{bench}/liberty/tutorial_cells.lib\", f\"{bench}/liberty/fakeram45_64x32.lib\"])\n", - "config = netlist.VerilogConfig()\n", + "config = netlist.GateLevelVerilogConfig()\n", "config.yosys_source_attributes = True # read the Yosys `src` attributes as source origins\n", - "top = netlist.load_verilog(f\"{bench}/verilog/tinyrocket/tinyrocket.v\", config=config)\n", + "top = netlist.load_gate_level_verilog(f\"{bench}/verilog/tinyrocket/tinyrocket.v\", config=config)\n", "top.get_name()" ] }, diff --git a/python/pyproject.toml b/python/pyproject.toml index facf0f9..d999efe 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -11,7 +11,7 @@ license = "Apache-2.0" authors = [{name = "Naja Authors", email = "contact@keplertech.io"}] requires-python = ">=3.10" dependencies = [ - "najaeda>=0.7.29", + "najaeda>=0.7.34", "websockets>=13", "anywidget>=0.9", ] diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 333980d..f341cb2 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -10,4 +10,4 @@ def top(): # NLUniverse is process-global: load the test design once for all tests. netlist.load_primitives("xilinx") - return netlist.load_verilog(str(DATA / "design.v")) + return netlist.load_gate_level_verilog(str(DATA / "design.v")) diff --git a/python/tests/protocol_cases.py b/python/tests/protocol_cases.py index c828d58..740868b 100644 --- a/python/tests/protocol_cases.py +++ b/python/tests/protocol_cases.py @@ -75,7 +75,7 @@ def load_verilog_design(spec, root): previous = os.getcwd() os.chdir(root) try: - db.loadVerilog([spec["verilog"]], yosys_source_attributes=True) + db.loadGateLevelVerilog([spec["verilog"]], yosys_source_attributes=True) finally: os.chdir(previous) top = next(d for lib in db.getLibraries() for d in lib.getSNLDesigns() diff --git a/python/tests/test_anonymous_instances.py b/python/tests/test_anonymous_instances.py index e8fd6ca..c380f47 100644 --- a/python/tests/test_anonymous_instances.py +++ b/python/tests/test_anonymous_instances.py @@ -108,11 +108,14 @@ def play_viewer(v, focus): return resolved +def snl_path(ids): + return protocol.get_path(naja.NLUniverse.get().getTopDesign(), ids) + + @pytest.mark.parametrize("ids", [[0], [1], [1, 0], [1, 1], [2, 1], []]) def test_focus_selection_round_trip(view, ids): - from najaeda import netlist v, sent = view - target = netlist.Instance(ids) + target = snl_path(ids) v.show_instance(target) focus = json.loads(sent[-1]["json"]) assert focus["id_path"] == ids @@ -122,8 +125,15 @@ def test_focus_selection_round_trip(view, ids): assert v.selected == target -def test_anonymous_siblings_are_distinct_selections(view): +def test_show_instance_accepts_a_najaeda_instance(view): + # Read through its pathIDs: a host's own netlist.Instance still works. from najaeda import netlist + v, sent = view + v.show_instance(netlist.Instance([1, 0])) + assert json.loads(sent[-1]["json"])["id_path"] == [1, 0] + + +def test_anonymous_siblings_are_distinct_selections(view): v, _ = view seen = [] v.on_select(seen.append) @@ -132,7 +142,7 @@ def test_anonymous_siblings_are_distinct_selections(view): {"request": "instance_selected", "path": [""], "id_path": ids})}, []) # Same name path each time, yet three selection changes. assert v.selected_path == [""] - assert seen == [netlist.Instance([0]), netlist.Instance([1]), netlist.Instance([0])] + assert seen == [snl_path([0]), snl_path([1]), snl_path([0])] def test_show_instance_accepts_an_id_path(view): @@ -159,9 +169,8 @@ def test_show_instance_rejects_what_cannot_name_one_instance(view): def test_selection_from_a_names_only_viewer_still_resolves(view): - from najaeda import netlist v, _ = view v._on_viewer_message(v, {"json": json.dumps( {"request": "instance_selected", "path": ["u_mid", "u_leaf"]})}, []) assert v.selected_id_path == [3, 2] - assert v.selected == netlist.Instance([3, 2]) + assert v.selected == snl_path([3, 2]) diff --git a/python/tests/test_notebook.py b/python/tests/test_notebook.py index b09c612..4adc32c 100644 --- a/python/tests/test_notebook.py +++ b/python/tests/test_notebook.py @@ -60,7 +60,7 @@ def _ask(request): assert _ask({"request": "instance_selected", "path": ["u_green"]}) == [] assert view.selected_path == ["u_green"] -assert view.selected.get_name() == "u_green" +assert netlist.Instance(view.selected_id_path).get_name() == "u_green" # Show Sources, in packages that have it (from 0.2.0; the colab job plays # the current PyPI release, which may predate it): u_red's instantiation diff --git a/python/tests/test_protocol.py b/python/tests/test_protocol.py index 6b0b859..fde70fe 100644 --- a/python/tests/test_protocol.py +++ b/python/tests/test_protocol.py @@ -1,6 +1,8 @@ +import ast import json import subprocess import sys +from pathlib import Path from types import SimpleNamespace from najaeda import naja @@ -164,8 +166,10 @@ def test_focus_instance_message(): def test_get_primitive_type_uses_naja_modeling_not_names(): - checks = ("isAssign", "isSequential", "isInv", "isBuf", "isAnd", "isNand", - "isOr", "isNor", "isXor", "isXnor", "isConst0", "isConst1") + checks = ("isAssign", "isSequential", "isAddSub", "isSubtract", "isMultiply", + "isShift", "isShiftRight", "isShiftArithmetic", "isDivMod", "isMux", + "isInv", "isBuf", "isAnd", "isNand", "isOr", "isNor", "isXor", "isXnor", + "isConst0", "isConst1") output = SimpleNamespace(getDirection=lambda: naja.SNLTerm.Direction.Output) @@ -185,22 +189,63 @@ def model(name, *true_checks, outputs=1): # A multi-output cell never reaches the truth-table checks (naja throws). assert protocol.get_primitive_type(model("FA", "isXor", outputs=2)) == "unknown" assert protocol.get_primitive_type(model("DFF2", "isSequential", outputs=2)) == "dff" - - -def test_get_primitive_type_of_full_adder(tmp_path): - # SystemVerilog `a + b` elaborates to NLDB0 full adders (two outputs), - # whose truth-table checks abort the process inside najaeda: run it in a - # subprocess, which also keeps this design out of the session's universe. - (tmp_path / "add.sv").write_text( - "module top(input [1:0] a, b, output [1:0] y); assign y = a + b; endmodule\n") + # A mux at any width: a bus mux is one model with an output per bit. + assert protocol.get_primitive_type(model("MX2", "isMux")) == "mux" + assert protocol.get_primitive_type(model("MX2_W4", "isMux", outputs=4)) == "mux" + # Word-level operators, at any width too. + assert protocol.get_primitive_type(model("ADD", "isAddSub", outputs=4)) == "add" + assert protocol.get_primitive_type(model("SUB", "isAddSub", "isSubtract", outputs=4)) == "sub" + assert protocol.get_primitive_type(model("MUL", "isMultiply", outputs=4)) == "mul" + assert protocol.get_primitive_type(model("SHL", "isShift", outputs=4)) == "shl" + assert protocol.get_primitive_type(model("SHR", "isShift", "isShiftRight", outputs=4)) == "shr" + assert protocol.get_primitive_type( + model("SRA", "isShift", "isShiftRight", "isShiftArithmetic", outputs=4)) == "ashr" + assert protocol.get_primitive_type(model("DIV", "isDivMod", outputs=8)) == "divmod" + + +REPO_DATA = Path(__file__).parents[2] / "tests" / "data" + + +def _primitive_types_in_subprocess(load): + # Run in a subprocess, which keeps this design out of the session's + # universe. `load` is an NLDB method call on `db`. Returns + # {model name: primitive_type} over the whole hierarchy. script = ( - "from najaeda import netlist, naja\n" + "from najaeda import naja\n" "from naja_schematic import protocol\n" - "netlist.load_system_verilog(['add.sv'])\n" - "top = naja.NLUniverse.get().getTopDesign()\n" - "print(sorted({protocol.get_primitive_type(i.getModel())\n" - " for i in top.getInstances()}))\n") - run = subprocess.run([sys.executable, "-c", script], cwd=tmp_path, + "db = naja.NLDB.create(naja.NLUniverse.create())\n" + "naja.NLUniverse.get().setTopDB(db)\n" + f"db.{load}\n" + "types = {}\n" + "def walk(d):\n" + " for i in d.getInstances():\n" + " m = i.getModel()\n" + " types[m.getName()] = protocol.get_primitive_type(m)\n" + " if not m.isPrimitive(): walk(m)\n" + "walk(naja.NLUniverse.get().getTopDesign())\n" + "print(sorted(types.items()))\n") + run = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True) assert run.returncode == 0, run.stderr - assert run.stdout.strip().splitlines()[-1] == "['unknown']" + return dict(ast.literal_eval(run.stdout.strip().splitlines()[-1])) + + +def test_get_primitive_type_of_word_level_operators(): + # The same SystemVerilog, and answers, as LocalSNLProvider's + # WordLevelOperatorsGetTheirPrimitiveType test. + types = _primitive_types_in_subprocess( + f"loadSystemVerilog([{str(REPO_DATA / 'sv' / 'operators.sv')!r}])") + assert types == { + "naja_add__w4": "add", "naja_sub__w4": "sub", "naja_mul__w4": "mul", + "naja_shift__w4_c2_r0_a0": "shl", "naja_shift__w4_c2_r1_a0": "shr", + "naja_shift__w4_c2_r1_a1": "ashr", "naja_divmod__u_w4": "divmod", + "naja_mux2__w4": "mux", "naja_mux2__w1": "mux", + } + + +def test_get_primitive_type_of_vhdl(): + vhdl = [str(REPO_DATA / "vhdl" / f) for f in ("top.vhd", "leaf.vhd")] + types = _primitive_types_in_subprocess( + f"loadVHDL({vhdl!r}, diagnostics_report_path=None)") + assert types["naja_mux2__w1"] == "mux" + assert types["leaf"] == "unknown" # hierarchical diff --git a/python/tests/test_transports.py b/python/tests/test_transports.py index 66e09fc..aba1c16 100644 --- a/python/tests/test_transports.py +++ b/python/tests/test_transports.py @@ -2,7 +2,10 @@ import io import json import socket +import subprocess +import sys import urllib.request +from pathlib import Path import pytest @@ -76,3 +79,38 @@ def get(path): def test_cli_rejects_liberty_with_systemverilog(): with pytest.raises(SystemExit): server.main(["--sv", "a.sv", "--liberty", "x.lib"]) + + +@pytest.mark.parametrize("argv", [ + ["--vhdl", "a.vhd", "--verilog", "b.v"], + ["--vhdl", "a.vhd", "--sv", "b.sv"], + ["--vhdl", "a.vhd", "--liberty", "cells.lib"], + ["--vhdl", "a.vhd", "--yosys-src"], + ["--vhdl", "a.vhd", "-D", "X"], + ["--verilog", "b.v", "--top", "t"], +]) +def test_cli_rejects_flags_that_do_not_apply(argv, capsys): + with pytest.raises(SystemExit) as e: + server.main(argv) + assert e.value.code == 2 + + +@pytest.mark.parametrize("flag, files, top", [ + ("--vhdl", ["vhdl/leaf.vhd", "vhdl/top.vhd"], "top"), + ("--sv", ["sv/operators.sv"], "ops"), + ("--sv", ["sv/two_tops.sv"], "second"), +]) +def test_cli_loads_design(flag, files, top, tmp_path): + # In a subprocess: loading replaces the session's design. Run from + # tmp_path, where naja writes its diagnostics report. + data = Path(__file__).parents[2] / "tests" / "data" + argv = [flag, *(str(data / f) for f in files), "--top", top] + script = ( + "import sys\n" + "from naja_schematic import server\n" + "args = server.build_arg_parser().parse_args(sys.argv[1:])\n" + "print(server.load_design(args).getName())\n") + run = subprocess.run([sys.executable, "-c", script, *argv], cwd=tmp_path, + capture_output=True, text=True) + assert run.returncode == 0, run.stderr + assert run.stdout.strip().splitlines()[-1] == top diff --git a/python/tests/test_viewer_server.py b/python/tests/test_viewer_server.py index 21a9588..54bc2fd 100644 --- a/python/tests/test_viewer_server.py +++ b/python/tests/test_viewer_server.py @@ -153,7 +153,7 @@ def on_select(id_path, path): assert seen.wait(5) assert selections == [([0, 0], ["u_sub", "u_and"])] assert server.selected_id_path == [0, 0] - assert server.selected.get_model_name() == "LUT2" + assert server.selected.getModel().getName() == "LUT2" def test_stop_closes_viewers_and_frees_the_port(top, bundle): diff --git a/python/tests/test_widget.py b/python/tests/test_widget.py index 24b8b2e..8196aa8 100644 --- a/python/tests/test_widget.py +++ b/python/tests/test_widget.py @@ -93,8 +93,7 @@ def test_focus_is_reapplied_after_each_root(top, view): assert sent_kinds(sent) == ["focus_instance", "root_response", "focus_instance"] -def test_viewer_selection_is_a_najaeda_instance(top, view): - from najaeda import netlist +def test_viewer_selection_is_an_snl_path(top, view): v, sent = view assert v.selected is None seen = [] @@ -104,12 +103,13 @@ def test_viewer_selection_is_a_najaeda_instance(top, view): v._on_viewer_message(v, {"json": json.dumps(select)}, []) assert sent == [] # a notification: nothing is sent back assert v.selected_path == ["u_sub", "u_and"] - assert v.selected == netlist.get_instance_by_path(["u_sub", "u_and"]) - assert v.selected.get_model_name() == "LUT2" + assert v.selected.getInstanceIDs() == v.selected_id_path == [0, 0] + assert v.selected.getTailInstance().getName() == "u_and" + assert v.selected.getModel().getName() == "LUT2" assert seen == [v.selected] v._on_viewer_message(v, {"json": json.dumps({"request": "instance_selected", "path": []})}, []) - assert v.selected.is_top() + assert v.selected.empty() # the top design assert len(seen) == 2 diff --git a/src/AppLogic.cpp b/src/AppLogic.cpp index 35d52c3..d429b97 100644 --- a/src/AppLogic.cpp +++ b/src/AppLogic.cpp @@ -616,6 +616,9 @@ bool appFrame(AppState& state) { static bool vrlYosysSrc = false; static char svFilesBuf[8192] = {}; static char svTopBuf[256] = {}; + static bool vhdlDialogOpen = false; + static char vhdlFilesBuf[8192] = {}; + static char vhdlTopBuf[256] = {}; #endif handleShortcuts(state); @@ -626,6 +629,7 @@ bool appFrame(AppState& state) { if (ImGui::MenuItem("Open naja-if...", "")) { snlDialogOpen = true; snlPathBuf[0] = '\0'; } if (ImGui::MenuItem("Open Verilog...", "")) { vrlDialogOpen = true; vrlFilesBuf[0] = '\0'; vrlLibertyBuf[0] = '\0'; } if (ImGui::MenuItem("Open SystemVerilog...", "")) { svDialogOpen = true; svFilesBuf[0] = '\0'; svTopBuf[0] = '\0'; } + if (ImGui::MenuItem("Open VHDL...", "")) { vhdlDialogOpen = true; vhdlFilesBuf[0] = '\0'; vhdlTopBuf[0] = '\0'; } ImGui::Separator(); // Diagnosis UI temporarily hidden — see DiagnosisStore.cpp kDiagnosisUIHidden. // if (ImGui::MenuItem("Load Diagnosis JSON...", "")) { @@ -786,6 +790,35 @@ bool appFrame(AppState& state) { if (ImGui::Button("Cancel", ImVec2(120,0))) ImGui::CloseCurrentPopup(); ImGui::EndPopup(); } + + // ==== Open VHDL dialog ==== + if (vhdlDialogOpen) { ImGui::OpenPopup("Open VHDL"); vhdlDialogOpen = false; } + ImGui::SetNextWindowSize(ImVec2(560, 0), ImGuiCond_Always); + if (ImGui::BeginPopupModal("Open VHDL", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("VHDL files, in any order (one per line):"); + ImGui::SameLine(); + if (ImGui::SmallButton("Browse...")) { + auto files = NativeFileDialog::pickFiles("Select VHDL Files", {"vhd", "vhdl"}); + appendPaths(vhdlFilesBuf, sizeof(vhdlFilesBuf), files); + } + ImGui::InputTextMultiline("##vhdlfiles", vhdlFilesBuf, sizeof(vhdlFilesBuf), ImVec2(-1, 140)); + ImGui::Spacing(); + ImGui::Text("Top entity (optional -- else the unique uninstantiated one):"); + ImGui::SetNextItemWidth(-1); + ImGui::InputText("##vhdltop", vhdlTopBuf, sizeof(vhdlTopBuf)); + ImGui::TextDisabled("naja's VHDL frontend is experimental (bit/bit_vector subset)."); + ImGui::Spacing(); + if (ImGui::Button("Open", ImVec2(120,0)) && vhdlFilesBuf[0]) { + if (localProvider) { + localProvider->loadVHDL(splitLines(vhdlFilesBuf), vhdlTopBuf); + reloadNetlist(state); + } + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120,0))) ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } #endif // ==== Main layout ==== diff --git a/src/LocalSNLProvider.cpp b/src/LocalSNLProvider.cpp index 4f77993..924b335 100644 --- a/src/LocalSNLProvider.cpp +++ b/src/LocalSNLProvider.cpp @@ -41,6 +41,7 @@ // naja formats #include "SNLVRLConstructor.h" #include "SNLSVConstructor.h" +#include "VHDLConstructor.h" #include "SNLLibertyConstructor.h" // naja serialization #include "SNLCapnP.h" @@ -136,9 +137,10 @@ static std::string displayChain(const std::vector& chain) { // (e.g. gate-level Verilog loaded without Liberty) is Unknown and draws as // the generic box. protocol.py's get_primitive_type() mirrors this, same as // the rest of the wire protocol (see CLAUDE.md). -// The truth-table checks are only asked of a cell with exactly one output -// bit: on a multi-output cell (NLDB0's full adder, a multi-output Liberty -// cell) naja's getTruthTable() throws from isConst0()/isInv()/... +// The gate checks are only asked of a cell with exactly one output bit: a +// multi-output cell (NLDB0's full adder, a multi-output Liberty cell) is +// never drawn as a gate. A mux and the NLDB0 word-level operators are +// recognized at any width (a bus mux is one NLDB0 mux2 with bus pins). static size_t outputBitCount(const SNLDesign* model) { size_t count = 0; for (auto* term : model->getBitTerms()) { @@ -151,6 +153,16 @@ static PrimitiveType getPrimitiveType(const SNLDesign* model) { if (!model) return PrimitiveType::Unknown; if (NLDB0::isAssign(model)) return PrimitiveType::Assign; if (SNLDesignModeling::isSequential(model)) return PrimitiveType::Dff; + if (NLDB0::isAddSub(model)) + return NLDB0::isSubtract(model) ? PrimitiveType::Sub : PrimitiveType::Add; + if (NLDB0::isMultiply(model)) return PrimitiveType::Mul; + if (NLDB0::isShift(model)) { + const auto shift = NLDB0::getShiftSignature(model); + if (!shift.right) return PrimitiveType::Shl; + return shift.arithmetic ? PrimitiveType::Ashr : PrimitiveType::Shr; + } + if (NLDB0::isDivMod(model)) return PrimitiveType::DivMod; + if (SNLDesignModeling::isMux(model)) return PrimitiveType::Mux; if (outputBitCount(model) != 1) return PrimitiveType::Unknown; if (SNLDesignModeling::isConst0(model)) return PrimitiveType::Tie0; if (SNLDesignModeling::isConst1(model)) return PrimitiveType::Tie1; @@ -566,6 +578,26 @@ void LocalSNLProvider::loadSystemVerilog(const std::vector& sources } } +void LocalSNLProvider::loadVHDL(const std::vector& files, + const std::string& topEntity) { + Console::Log("LocalSNLProvider: loading VHDL (" + std::to_string(files.size()) + " files)"); + try { + db_ = freshDB(); + std::vector paths(files.begin(), files.end()); + auto* userLib = NLLibrary::create(db_, NLName("work")); + VHDLConstructor vhdlCtor(userLib); + if (auto* top = vhdlCtor.constructFiles(paths, topEntity)) { + db_->setTopDesign(top); + Console::Log("Top design: " + designName(top)); + } else { + findAndSetTop(); + } + } catch (const std::exception& e) { + Console::Error("loadVHDL failed: " + std::string(e.what())); + db_ = nullptr; + } +} + // --------------------------------------------------------------------------- // Request dispatch // --------------------------------------------------------------------------- diff --git a/src/LocalSNLProvider.h b/src/LocalSNLProvider.h index 1bb4094..ca66f06 100644 --- a/src/LocalSNLProvider.h +++ b/src/LocalSNLProvider.h @@ -45,6 +45,11 @@ class LocalSNLProvider : public INetlistProvider { void loadSystemVerilog(const std::vector& sources, const std::string& topModule = ""); + // Load VHDL (naja's experimental frontend; files in any order). If + // topEntity is empty, naja infers the unique uninstantiated entity. + void loadVHDL(const std::vector& files, + const std::string& topEntity = ""); + // A terminal named for a human or a script -- {"id_path"|"path": the // containing instance (root excluded, [] = a top-level port), // "terminal": its base name, "bit": optional bus bit} -- turned into the diff --git a/src/SchematicLayout.cpp b/src/SchematicLayout.cpp index 7cbd1af..fee779c 100644 --- a/src/SchematicLayout.cpp +++ b/src/SchematicLayout.cpp @@ -132,11 +132,36 @@ SymbolGeometry symbolGeometry(PrimitiveType type, const size_t rows = std::max(nL, nR); g.h = std::max(2.f * kPinPitch, kPinPitch * float(rows + 1)); for (size_t i = 0; i < nL; ++i) g.leftY.push_back(kPinPitch * float(i + 1)); - for (size_t i = 0; i < nR; ++i) g.rightY.push_back(kPinPitch * float(i + 1)); - g.w = snapUp(std::max(6.f * kGrid, labelWidth(leftNames) + labelWidth(rightNames) + 3.f * kGrid), kGrid); + if (type == PrimitiveType::Mux) { + // Outputs centered: h is a kPinPitch multiple, so h/2 (and every + // pin a pitch from it) is on the grid. + for (size_t i = 0; i < nR; ++i) + g.rightY.push_back(0.5f * g.h + kPinPitch * (float(i) - 0.5f * float(nR - 1))); + } else { + for (size_t i = 0; i < nR; ++i) g.rightY.push_back(kPinPitch * float(i + 1)); + } + const float glyph = isOperatorSymbol(type) ? kOperatorGlyphW : 0.f; + g.w = snapUp(std::max(6.f * kGrid, labelWidth(leftNames) + labelWidth(rightNames) + glyph + 3.f * kGrid), kGrid); return g; } +float muxSlant(float h, size_t rightPins) { + const float outerPin = 0.5f * h - 0.5f * kPinPitch * float(rightPins ? rightPins - 1 : 0); + return std::max(0.f, std::min(0.25f * h, outerPin - 0.5f * kPinPitch)); +} + +bool isOperatorSymbol(PrimitiveType type) { + switch (type) { + case PrimitiveType::Add: case PrimitiveType::Sub: + case PrimitiveType::Mul: case PrimitiveType::DivMod: + case PrimitiveType::Shl: case PrimitiveType::Shr: + case PrimitiveType::Ashr: + return true; + default: + return false; + } +} + // --------------------------------------------------------------------------- // Layered placement // --------------------------------------------------------------------------- diff --git a/src/SchematicLayout.h b/src/SchematicLayout.h index a204073..8e4a438 100644 --- a/src/SchematicLayout.h +++ b/src/SchematicLayout.h @@ -69,7 +69,8 @@ float footprintWidth(const InstanceShape& s, const NameWidthFn& width); // --------------------------------------------------------------------------- // Symbols. A gate (AND/OR/XOR families, INV/BUF, assign, tie cells) is drawn as its -// standard symbol, sized by its input count; anything else is a box sized to +// standard symbol, sized by its input count; anything else (including a mux +// and a word-level operator, see below) is a box sized to // fit its pin names, which are drawn inside it. Pins sit on the kPinPitch // grid: a gate's inputs are centered on its single output, a box's pins run // down from the top on each side. Pin offsets are from the symbol's top edge. @@ -89,6 +90,18 @@ SymbolGeometry symbolGeometry(PrimitiveType type, const std::vector& leftNames, const std::vector& rightNames); +// A mux is a box whose right side is narrowed to a trapezoid: its inputs run +// down from the top (the select pin, last, at the bottom), its outputs are +// centered on the right. muxSlant() is how far the right edge's ends are +// pulled in, top and bottom, for a mux `h` high with `rightPins` outputs -- +// always short of the outermost output, so every pin touches the body. +float muxSlant(float h, size_t rightPins); + +// Word-level operators (add, sub, mul, shifts, divmod) are boxes with room +// for their operator glyph between the two columns of pin names. +bool isOperatorSymbol(PrimitiveType type); +inline constexpr float kOperatorGlyphW = 2.f * kPinPitch; + // --------------------------------------------------------------------------- // Layered placement (Sugiyama-style): // 1. cycles are broken (DFS back edges are ignored for layering); diff --git a/src/SchematicView.cpp b/src/SchematicView.cpp index 7f93c75..77dbbe6 100644 --- a/src/SchematicView.cpp +++ b/src/SchematicView.cpp @@ -189,11 +189,11 @@ void drawNegationBubble(SchematicPainter* dl, ImVec2 tip, float radius, ImU32 fi dl->AddCircle(center, radius, kInstanceLineColor, 16, 1.25f); } -// Draws a dashed rectangle in screen space (no corner rounding). +// Draws a dashed closed polygon in screen space. // dashPx / gapPx are in screen pixels so they stay legible at any zoom. -void addDashedRect(SchematicPainter* dl, ImVec2 rmin, ImVec2 rmax, - ImU32 col, float thickness, - float dashPx = 6.0f, float gapPx = 4.0f) { +void addDashedPolygon(SchematicPainter* dl, const std::vector& pts, + ImU32 col, float thickness, + float dashPx = 6.0f, float gapPx = 4.0f) { auto seg = [&](float ax, float ay, float bx, float by) { float dx = bx - ax, dy = by - ay; float len = sqrtf(dx * dx + dy * dy); @@ -210,10 +210,17 @@ void addDashedRect(SchematicPainter* dl, ImVec2 rmin, ImVec2 rmax, on = !on; } }; - seg(rmin.x, rmin.y, rmax.x, rmin.y); // top - seg(rmax.x, rmin.y, rmax.x, rmax.y); // right - seg(rmax.x, rmax.y, rmin.x, rmax.y); // bottom - seg(rmin.x, rmax.y, rmin.x, rmin.y); // left + for (size_t i = 0; i < pts.size(); ++i) { + const ImVec2 a = pts[i], b = pts[(i + 1) % pts.size()]; + seg(a.x, a.y, b.x, b.y); + } +} + +// Draws a dashed rectangle in screen space (no corner rounding). +void addDashedRect(SchematicPainter* dl, ImVec2 rmin, ImVec2 rmax, + ImU32 col, float thickness) { + addDashedPolygon(dl, { rmin, ImVec2(rmax.x, rmin.y), rmax, ImVec2(rmin.x, rmax.y) }, + col, thickness); } } // namespace @@ -594,6 +601,69 @@ static void drawDffInstance(SchematicPainter* dl, const InstanceShape& inst, } } +// --------------------------------------------------------------------------- +// Mux — a trapezoid: full height on the input side, its right edge pulled +// in by SchematicLayout::muxSlant() top and bottom, short of the centered +// outputs (see symbolGeometry()). Pin names inside, as in a box. +// --------------------------------------------------------------------------- +static void drawMuxInstance(SchematicPainter* dl, const InstanceShape& inst, + const SchematicView& sv, + const ImVec2& canvasPos, const ImVec2& canvasSize) { + ImVec2 rmin, rmax; + sv.worldRectToScreen(inst.x, inst.y, inst.w, inst.h, canvasPos, canvasSize, rmin, rmax); + size_t rightPins = 0; + for (const auto& p : inst.ports) if (p.lx >= 0.0f) ++rightPins; + const float slant = SchematicLayout::muxSlant(inst.h, rightPins) * sv.transform.scale; + std::vector pts = { + rmin, ImVec2(rmax.x, rmin.y + slant), ImVec2(rmax.x, rmax.y - slant), ImVec2(rmin.x, rmax.y), + }; + dl->AddConvexPolyFilled(pts.data(), int(pts.size()), kInstanceFillColor); + if (inst.partialInterface) + addDashedPolygon(dl, pts, kInstanceLineColor, 1.25f); + else + dl->AddPolyline(pts.data(), int(pts.size()), kInstanceLineColor, 1.25f, ImDrawFlags_Closed); + if (inst.diagOutline != 0) + dl->AddPolyline(pts.data(), int(pts.size()), inst.diagOutline, 3.5f, ImDrawFlags_Closed); + if (inst.selected) addSelectionOutline(dl, rmin, rmax); + drawInstanceName(dl, inst, sv, rmin); + drawPorts(dl, inst, sv, canvasPos, canvasSize, /*namesInside=*/true); +} + +// --------------------------------------------------------------------------- +// Word-level operator (add, sub, mul, shifts, divmod) — a box, with its +// operator in a circle between the two columns of pin names, in the room +// symbolGeometry() left for it (kOperatorGlyphW). +// --------------------------------------------------------------------------- +static const char* operatorGlyph(PrimitiveType type) { + switch (type) { + case PrimitiveType::Add: return "+"; + case PrimitiveType::Sub: return "-"; + case PrimitiveType::Mul: return "*"; + case PrimitiveType::Shl: return "<<"; + case PrimitiveType::Shr: return ">>"; + case PrimitiveType::Ashr: return ">>>"; + case PrimitiveType::DivMod: return "/%"; + default: return ""; + } +} + +static void drawOperatorInstance(SchematicPainter* dl, const InstanceShape& inst, + const SchematicView& sv, + const ImVec2& canvasPos, const ImVec2& canvasSize) { + drawGenericInstance(dl, inst, sv, canvasPos, canvasSize); + float fontSize = labelFontSize(kInstanceLabelBaseSize, sv.transform.scale); + if (fontSize <= 0.0f) return; + ImVec2 rmin, rmax; + sv.worldRectToScreen(inst.x, inst.y, inst.w, inst.h, canvasPos, canvasSize, rmin, rmax); + const ImVec2 c((rmin.x + rmax.x) * 0.5f, (rmin.y + rmax.y) * 0.5f); + const float r = 0.5f * SchematicLayout::kOperatorGlyphW * 0.8f * sv.transform.scale; + dl->AddCircle(c, r, kInstanceLineColor, 24, 1.25f); + const char* glyph = operatorGlyph(inst.primitiveType); + ImFont* font = ImGui::GetFont(); + ImVec2 ts = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, glyph); + dl->AddText(font, fontSize, ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f), kPinLabelColor, glyph); +} + // --------------------------------------------------------------------------- // Top-level design port — a "flag": a small pentagon sized to its // name, pointing the way the signal flows. The wire attaches at the @@ -751,6 +821,15 @@ void SchematicView::drawInstance(SchematicPainter* dl, const InstanceShape& inst case PrimitiveType::Dff: drawDffInstance(dl, inst, *this, canvasPos, canvasSize); break; + case PrimitiveType::Mux: + drawMuxInstance(dl, inst, *this, canvasPos, canvasSize); + break; + case PrimitiveType::Add: case PrimitiveType::Sub: + case PrimitiveType::Mul: case PrimitiveType::DivMod: + case PrimitiveType::Shl: case PrimitiveType::Shr: + case PrimitiveType::Ashr: + drawOperatorInstance(dl, inst, *this, canvasPos, canvasSize); + break; default: drawGenericInstance(dl, inst, *this, canvasPos, canvasSize); break; diff --git a/src/Types.h b/src/Types.h index deb4e7f..2b97eb3 100644 --- a/src/Types.h +++ b/src/Types.h @@ -72,6 +72,15 @@ enum class PrimitiveType { Assign, Tie0, // constant-0 cell (Liberty function "0", e.g. TIEL) Tie1, // constant-1 cell (Liberty function "1", e.g. TIEH) + Mux, // 2:1 multiplexer (NLDB0 mux2, or a cell whose truth table is a mux) + // Word-level NLDB0 operators (SystemVerilog lowering), one box per word. + Add, + Sub, + Mul, + Shl, // shift left + Shr, // logical shift right + Ashr, // arithmetic shift right + DivMod, // quotient and remainder }; inline const char* toString(PrimitiveType t) { @@ -88,6 +97,14 @@ inline const char* toString(PrimitiveType t) { case PrimitiveType::Assign: return "assign"; case PrimitiveType::Tie0: return "tie0"; case PrimitiveType::Tie1: return "tie1"; + case PrimitiveType::Mux: return "mux"; + case PrimitiveType::Add: return "add"; + case PrimitiveType::Sub: return "sub"; + case PrimitiveType::Mul: return "mul"; + case PrimitiveType::Shl: return "shl"; + case PrimitiveType::Shr: return "shr"; + case PrimitiveType::Ashr: return "ashr"; + case PrimitiveType::DivMod: return "divmod"; default: return "unknown"; } } @@ -105,6 +122,14 @@ inline PrimitiveType primitiveTypeFromString(const std::string& s) { if (s == "assign") return PrimitiveType::Assign; if (s == "tie0") return PrimitiveType::Tie0; if (s == "tie1") return PrimitiveType::Tie1; + if (s == "mux") return PrimitiveType::Mux; + if (s == "add") return PrimitiveType::Add; + if (s == "sub") return PrimitiveType::Sub; + if (s == "mul") return PrimitiveType::Mul; + if (s == "shl") return PrimitiveType::Shl; + if (s == "shr") return PrimitiveType::Shr; + if (s == "ashr") return PrimitiveType::Ashr; + if (s == "divmod") return PrimitiveType::DivMod; return PrimitiveType::Unknown; } diff --git a/src/main_native.cpp b/src/main_native.cpp index ab44de2..5cc5420 100644 --- a/src/main_native.cpp +++ b/src/main_native.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -17,15 +18,17 @@ #include "Console.h" int main(int argc, char* argv[]) { - // CLI usage: naja-schematic-standalone [] [--liberty ]... [--diagnosis ] + // CLI usage: naja-schematic-standalone [...] [--liberty ]... [--diagnosis ] // [--source-root ]... [--source-map FROM=TO]... - // [--yosys-src] + // [--yosys-src] [--top ] // [--export [--focus ] // [--trace ]... [--equipotential ]...] - // is loaded by extension (.sv/.v/otherwise-assumed-SNL-directory); + // is loaded by extension (.sv/.v/.vhd/.vhdl/otherwise-assumed-SNL-directory); + // only VHDL takes several, all .vhd/.vhdl, in any order; + // --top (.sv and VHDL) forces the top module/entity instead of inferring it; // --liberty defines the primitive cell library for a .v design (repeatable, - // once per file) -- it is not supported for .sv, since loadSystemVerilog()/ - // SNLSVConstructor has no liberty hook; + // once per file) -- it is not supported for .sv or VHDL, since loadSystemVerilog()/ + // SNLSVConstructor and loadVHDL()/VHDLConstructor have no liberty hook; // --diagnosis pre-loads a diagnosis_response-shaped JSON so an external // caller (a script, or naja-agent's skill, after an edit-check cycle) can // open a fully annotated view in one command instead of requiring a human @@ -41,7 +44,8 @@ int main(int argc, char* argv[]) { // (see HeadlessExport.h for what --focus/--trace/--equipotential take). auto* provider = new LocalSNLProvider(); - std::string designPath; + std::vector designPaths; + std::string top; std::string diagnosisPath; std::vector libertyPaths; bool yosysSrc = false; @@ -64,6 +68,8 @@ int main(int argc, char* argv[]) { diagnosisPath = argv[++i]; } else if (arg == "--liberty" && i + 1 < argc) { libertyPaths.push_back(argv[++i]); + } else if (arg == "--top" && i + 1 < argc) { + top = argv[++i]; } else if (arg == "--yosys-src") { yosysSrc = true; } else if (arg == "--source-map" && i + 1 < argc) { @@ -76,13 +82,31 @@ int main(int argc, char* argv[]) { provider->addSourceMap(map.substr(0, eq), map.substr(eq + 1)); } else if (arg == "--source-root" && i + 1 < argc) { provider->addSourceRoot(argv[++i]); - } else if (designPath.empty()) { - designPath = arg; + } else { + designPaths.push_back(arg); } } + auto extension = [](const std::string& path) { + const auto dot = path.rfind('.'); + return dot == std::string::npos ? std::string() : path.substr(dot + 1); + }; + auto isVHDL = [&](const std::string& path) { + const std::string ext = extension(path); + return ext == "vhd" || ext == "vhdl"; + }; + const std::string designPath = designPaths.empty() ? "" : designPaths.front(); + if (designPaths.size() > 1 && !std::all_of(designPaths.begin(), designPaths.end(), isVHDL)) { + std::cerr << "only VHDL designs take several files (all .vhd/.vhdl)\n"; + return 1; + } + if (!top.empty() && (designPath.empty() || (extension(designPath) != "sv" && !isVHDL(designPath)))) { + std::cerr << "--top only applies to .sv and VHDL designs\n"; + return 1; + } + if (!designPath.empty()) { - std::string ext = designPath.size() >= 3 ? designPath.substr(designPath.rfind('.') + 1) : ""; + std::string ext = extension(designPath); if (ext == "sv") { if (!libertyPaths.empty()) { std::cerr << "--liberty is only supported for .v designs, not .sv\n"; @@ -92,7 +116,13 @@ int main(int argc, char* argv[]) { std::cerr << "--yosys-src is only supported for .v designs\n"; return 1; } - provider->loadSystemVerilog({designPath}); + provider->loadSystemVerilog({designPath}, top); + } else if (isVHDL(designPath)) { + if (!libertyPaths.empty() || yosysSrc) { + std::cerr << "--liberty and --yosys-src are only supported for .v designs, not VHDL\n"; + return 1; + } + provider->loadVHDL(designPaths, top); } else if (ext == "v") { provider->loadVerilog({designPath}, libertyPaths, yosysSrc); } else { diff --git a/tests/SchematicLayoutTest.cpp b/tests/SchematicLayoutTest.cpp index 856f691..eb07737 100644 --- a/tests/SchematicLayoutTest.cpp +++ b/tests/SchematicLayoutTest.cpp @@ -217,6 +217,42 @@ TEST(Symbols, GateWithSeveralOutputsIsDrawnAsABox) { EXPECT_EQ(g.rightY, (std::vector{20.f, 40.f})); } +TEST(Symbols, MuxInputsRunDownFromTheTopAndItsOutputIsCentered) { + auto g = symbolGeometry(PrimitiveType::Mux, {"A", "B", "S"}, {"Y"}); + EXPECT_FALSE(isGateSymbol(PrimitiveType::Mux)); + EXPECT_EQ(g.leftY, (std::vector{20.f, 40.f, 60.f})); + EXPECT_EQ(g.rightY, (std::vector{0.5f * g.h})); +} + +TEST(Symbols, MuxSlantStopsShortOfItsOutermostOutput) { + for (size_t nL = 1; nL <= 6; ++nL) + for (size_t nR = 1; nR <= nL; ++nR) { + auto g = symbolGeometry(PrimitiveType::Mux, std::vector(nL, "A"), + std::vector(nR, "Y")); + EXPECT_TRUE(onGrid(g.w) && onGrid(g.h)) << nL << "x" << nR; + for (float y : g.rightY) EXPECT_TRUE(onGrid(y)) << nL << "x" << nR; + const float slant = muxSlant(g.h, nR); + EXPECT_GT(slant, 0.f) << nL << "x" << nR; + EXPECT_LT(slant, g.rightY.front()) << nL << "x" << nR; + EXPECT_GT(g.h - slant, g.rightY.back()) << nL << "x" << nR; + } +} + +TEST(Symbols, OperatorBoxHasRoomForItsGlyph) { + for (auto type : {PrimitiveType::Add, PrimitiveType::Sub, PrimitiveType::Mul, + PrimitiveType::Shl, PrimitiveType::Shr, PrimitiveType::Ashr, + PrimitiveType::DivMod}) { + EXPECT_TRUE(isOperatorSymbol(type)); + EXPECT_FALSE(isGateSymbol(type)); + auto op = symbolGeometry(type, {"A", "B"}, {"Y"}); + auto box = symbolGeometry(PrimitiveType::Unknown, {"A", "B"}, {"Y"}); + EXPECT_GE(op.w, box.w + kOperatorGlyphW - kGrid) << int(type); + EXPECT_TRUE(onGrid(op.w)) << int(type); + EXPECT_EQ(op.leftY, box.leftY); + } + EXPECT_FALSE(isOperatorSymbol(PrimitiveType::Mux)); +} + // --------------------------------------------------------------------------- // layeredPlacement // --------------------------------------------------------------------------- diff --git a/tests/data/sv/operators.sv b/tests/data/sv/operators.sv new file mode 100644 index 0000000..d239aa9 --- /dev/null +++ b/tests/data/sv/operators.sv @@ -0,0 +1,14 @@ +module ops(input logic [3:0] a, b, input logic [1:0] n, input logic s, input logic x, y, z, + output logic [3:0] sum, diff, prod, shl, shr, sra, m, q, r, + output logic m1); + assign sum = a + b; + assign diff = a - b; + assign prod = a * b; + assign shl = a << n; + assign shr = a >> n; + assign sra = $signed(a) >>> n; + assign m = s ? a : b; + assign m1 = x ? y : z; + assign q = a / b; + assign r = a % b; +endmodule diff --git a/tests/data/sv/two_tops.sv b/tests/data/sv/two_tops.sv new file mode 100644 index 0000000..58dc2aa --- /dev/null +++ b/tests/data/sv/two_tops.sv @@ -0,0 +1,8 @@ +// Two uninstantiated modules: --top picks which one is the top. +module first(input a, output y); + assign y = a; +endmodule + +module second(input a, output y); + assign y = ~a; +endmodule diff --git a/tests/data/vhdl/leaf.vhd b/tests/data/vhdl/leaf.vhd new file mode 100644 index 0000000..c4961a1 --- /dev/null +++ b/tests/data/vhdl/leaf.vhd @@ -0,0 +1,9 @@ +-- One level of the VHDL test design: a 2:1 mux on bits. +entity leaf is + port (a, b, s : in bit; y : out bit); +end entity; + +architecture rtl of leaf is +begin + y <= a when s = '1' else b; +end architecture; diff --git a/tests/data/vhdl/top.vhd b/tests/data/vhdl/top.vhd new file mode 100644 index 0000000..4573c9d --- /dev/null +++ b/tests/data/vhdl/top.vhd @@ -0,0 +1,12 @@ +-- Top of the VHDL test design (tests/data/vhdl): instantiates leaf.vhd. +entity top is + port (a, b, s : in bit; y, n : out bit); +end entity; + +architecture rtl of top is + signal m : bit; +begin + u_leaf : entity work.leaf port map (a => a, b => b, s => s, y => m); + y <= m; + n <= not (a and b); +end architecture; diff --git a/tests/provider/LocalSNLProviderProtocolTest.cpp b/tests/provider/LocalSNLProviderProtocolTest.cpp index 1f13718..a47249b 100644 --- a/tests/provider/LocalSNLProviderProtocolTest.cpp +++ b/tests/provider/LocalSNLProviderProtocolTest.cpp @@ -366,3 +366,86 @@ TEST_F(SourceProtocol, SourceMapsMatchAtPathBoundaries) { EXPECT_TRUE(ask({{"/synth", "/nowhere"}, {"/synth/machine", sources}})); // falls through EXPECT_FALSE(ask({})); } + +// --------------------------------------------------------------------------- +// Loading from files: the provider's own loaders, then what the viewer is +// told about the result. +// --------------------------------------------------------------------------- +namespace { + +struct LoadedProvider { + LocalSNLProvider provider; + json replies = json::array(); + + LoadedProvider() { + if (!NLUniverse::get()) NLUniverse::create(); + provider.on_message([this](const std::string& msg) { replies.push_back(json::parse(msg)); }); + } + + json ask(const json& request) { + replies = json::array(); + provider.send(request.dump()); + return replies.empty() ? json() : replies[0]; + } + + // name -> primitive_type of the primitives directly in `designRef`. + std::map primitiveTypes(const json& designRef) { + std::map types; + const json reply = ask({{"request", "load_primitives"}, {"design_ref", designRef}}); + for (const auto& c : reply["children"]) + types[c["model_name"].get()] = c["primitive_type"].get(); + return types; + } +}; + +std::string testData(const std::string& rel) { + return std::string(NAJA_SCHEMATIC_TEST_DATA) + "/" + rel; +} + +} // namespace + +TEST(ProviderLoading, LoadsVHDLInAnyFileOrder) { + LoadedProvider p; + p.provider.loadVHDL({testData("vhdl/top.vhd"), testData("vhdl/leaf.vhd")}); + ASSERT_TRUE(p.provider.hasDesign()); + json root = p.ask({{"request", "load_root"}})["root"]; + EXPECT_EQ(root["name"], "top"); + EXPECT_TRUE(root.value("has_sources", false)); // naja's VHDL frontend records declarations + + json instances = p.ask({{"request", "load_instances"}, {"design_ref", root["design_ref"]}})["children"]; + ASSERT_EQ(instances.size(), 1u); + EXPECT_EQ(instances[0]["name"], "u_leaf"); + EXPECT_EQ(p.primitiveTypes(instances[0]["design_ref"])["naja_mux2__w1"], "mux"); +} + +TEST(ProviderLoading, VHDLTopEntityCanBeForced) { + LoadedProvider p; + p.provider.loadVHDL({testData("vhdl/top.vhd"), testData("vhdl/leaf.vhd")}, "leaf"); + ASSERT_TRUE(p.provider.hasDesign()); + EXPECT_EQ(p.ask({{"request", "load_root"}})["root"]["name"], "leaf"); +} + +TEST(ProviderLoading, BadVHDLLeavesNoDesign) { + LoadedProvider p; + p.provider.loadVHDL({testData("vhdl/missing.vhd")}); + EXPECT_FALSE(p.provider.hasDesign()); +} + +// The word-level operators SystemVerilog lowers to are drawn as such, from +// NLDB0's own identification of the model (never its name). +TEST(ProviderLoading, WordLevelOperatorsGetTheirPrimitiveType) { + LoadedProvider p; + p.provider.loadSystemVerilog({testData("sv/operators.sv")}); + ASSERT_TRUE(p.provider.hasDesign()); + json root = p.ask({{"request", "load_root"}})["root"]; + auto types = p.primitiveTypes(root["design_ref"]); + EXPECT_EQ(types["naja_add__w4"], "add"); + EXPECT_EQ(types["naja_sub__w4"], "sub"); + EXPECT_EQ(types["naja_mul__w4"], "mul"); + EXPECT_EQ(types["naja_shift__w4_c2_r0_a0"], "shl"); + EXPECT_EQ(types["naja_shift__w4_c2_r1_a0"], "shr"); + EXPECT_EQ(types["naja_shift__w4_c2_r1_a1"], "ashr"); + EXPECT_EQ(types["naja_divmod__u_w4"], "divmod"); + EXPECT_EQ(types["naja_mux2__w4"], "mux"); // a bus mux: one model, 4 outputs + EXPECT_EQ(types["naja_mux2__w1"], "mux"); +} diff --git a/thirdparty/naja b/thirdparty/naja index 5835954..52620d2 160000 --- a/thirdparty/naja +++ b/thirdparty/naja @@ -1 +1 @@ -Subproject commit 583595484305a596ae2c555f0908826467d69377 +Subproject commit 52620d2012be118c28cafd6089400984b5c3e674