Skip to content

ICL

ICL text emission and import; see ICL / PDL import for the setup import_icl needs.

warptap.icl_emit

ICL (Instrument Connectivity Language) text emitter (implementation_plan.md §7 Stage 10). Extends the "stateless pretty-printer over one model" family SVF/STAPL already established (:mod:warptap.tap_ir_svf, :mod:warptap.tap_ir_stapl) up one abstraction level: those two render a scan-pattern (:mod:warptap.tap_ir ops); this one renders network topology (:class:~warptap.icl_model.PhysicalGraph/:class:~warptap.icl_model.ModuleInstance) -- a genuinely different input shape, hence its own error-class family (IclEmitError, not TapIr*Error).

Grammar, confirmed this session against three independently-authored real implementations (an ANTLR4 grammar, a Ruby/Treetop grammar, and a C++ tool's real fixture files, all agreeing): Module <name> { ... } is brace-delimited with no endmodule keyword -- unlike every Verilog-family text this project has emitted so far, an easy wrong-instinct default to avoid. Port declarations (ScanInPort/ScanOutPort/SelectPort/CaptureEnPort/etc.) are grouped into named ScanInterface { Port ...; } blocks. ScanMux <name> SelectedBy <expr> { <value> : <signal>; ... } selects among named scan sources.

A SIB has no dedicated ICL keyword at all -- it's a design pattern, confirmed real (IEEE 1687-2014 Appendix E example code, found reproduced near-verbatim in four independent open-source repos this session): a ScanRegister whose ScanInSource is a ScanMux gated by the register's own committed value:

ScanRegister SR { ScanInSource SIBmux; CaptureSource SR; ResetValue 1'b0; }
ScanMux SIBmux SelectedBy SR { 1'b0 : SI; 1'b1 : fromSO; }

This maps directly onto rtl/sib_cell.v's own shift_ff/po/select shape: SR is po, the ScanMux is exactly shift_ff <= po ? nested_so : si. One real, deliberate divergence from the textbook pattern: the real SIB_mux_pre example also relays CaptureEn/ShiftEn/UpdateEn/Reset/TCK through per-instance To<X>Port ports, supporting arbitrary nesting depth. warptap's actual RTL doesn't do this at all -- sib_cell.v has no such relay ports, and sib_insert.py wires capture_dr/shift_dr/update_dr/tck/trst_n identically to every cell in the network directly from tap_core, with no per-SIB relay. This emitter renders that honestly: one shared top-level ScanInterface binds those five control signals to every SIB and instrument module instance directly, rather than fabricating relay ports v1's flat network (implementation_plan.md §7 Stage 4's own scoping decision) has no use for and warptap's own RTL doesn't build.

Another real, deliberate scope boundary, stated loudly rather than papered over: ICL describes scan-chain topology -- which module's SI binds to which module's SO, and which value of a ScanMux's select source chooses which arm -- not RTL-level clock-edge timing hazards. rtl/sib_cell.v's real nested_select output is po & shift_ff & select (implementation_plan.md §7 Stage 9's own hard-won fix: gates a WRITE instrument's update-latch commit so it only ever fires on an edge that leaves its SIB open both before and after, never one that opens or closes it -- see :mod:warptap.sib_insert's module docstring for the full story). ICL has no vocabulary for "before and after this specific clock edge" -- only for a register's settled value. This emitter therefore renders the textbook, ICL-idiomatic SR & SEL form (matching the real SIB_mux_pre example's own LogicSignal toSel_SR_SEL { SR[0] & SEL; } exactly) for describing which segment a SIB's host-side select signal reaches -- correct at the topology level ICL operates on -- and does NOT attempt to re-encode the RTL-level timing refinement in ICL text. That refinement is proven correct by this project's own RTL cross-simulation (:mod:warptap.sib_model), a separate, lower-level concern than what a network-topology description language is for.

Real Instrument module shape, confirmed real (same Appendix E source): DataInPort/ DataOutPort { Source ...; }, and a ScanRegister with WriteDataSource/ WriteEnSource for a real write-target register. Maps directly onto rtl/instrument_write.v's real (non-stub) update latch.

Chain wiring has no dedicated topology syntax -- confirmed real: plain module Instance <name> Of <ModuleType> { InputPort SI = <prevInstance>.SO; ... } statements, each SIB's SI explicitly bound to the previous slot's SO (or the top module's own TDI for the first slot) -- mirrors :mod:warptap.sib_insert's own prev_so threading exactly.

AccessLink ... Of STD_1149_1_2001 binds the SIB chain to the real TAP. Its shape is AccessLink <name> Of STD_1149_1_2001 { BSDLEntity <entity>; <INSTRUCTION> { ScanInterface { <instance>[.<scanIf>]; } } }, where <INSTRUCTION> is a bare identifier naming an instruction in the BSDL file for <entity>. Sources that confirmed it (read directly, not recalled): the vendored grammar (third_party/icl_parser/src/icl_parser/icl.g4, rules accessLink1149_def/bsdl_instr_ref/bsdl_instr_name); the IJTAG benchmark set's ICL/Standard/E30.icl (whose header says it holds snippets from IEEE Std 1687 Annex E example E.30) and MultiCoreAccessLink.icl; the MAST project's ICL grammar (its rules are labelled 1687 section 6.4.16) and test files; and ASSET InterTech's IJTAG article, whose USER1 block names the FPGA BSDL's own USER1 instruction. Not confirmed: the normative IEEE 1687-2014 clause text, which is paywalled and was not read.

This emitter names EXTEST -- :data:warptap.bsdl_emit.NETWORK_ACCESS_BSDL_INSTRUCTION, the instruction :mod:warptap.bsdl_emit declares in the BSDL it writes. rtl/tap_core.v presents external_dr_tdo (where the SIB chain plugs in) at tdo for any instruction that decodes to neither IDCODE nor BYPASS, but :mod:warptap.sib_insert decodes EXTEST from the current instruction into every top-level SIB's select, so the chain captures, shifts and updates only under EXTEST; under SAMPLE/PRELOAD it reaches tdo but holds still. (An earlier version emitted the instruction name wdr_select and its docstrings claimed it named EXTEST: wdr_select is the instruction name in the benchmark file whose shape this emitter copied, not part of ICL's syntax, and no TAP warptap inserts has an instruction of that name.) For a TAP inserted with ijtag_access_opcode, to_icl(..., ijtag_access=True) names IJTAG_ACCESS instead (:data:warptap.bsdl_emit.IJTAG_ACCESS_BSDL_INSTRUCTION), the instruction the BSDL written with the same opcode declares; there, EXTEST and SAMPLE/PRELOAD select BYPASS.

Open question, deliberately not guessed at: the ScanInterface { ... } list names only the chain's first slot, while the top-level slots' SEL ports are never bound in the Instance statements. Every readable published example wires its listed instances implicitly, and none has an explicitly SI/SO-chained sequence listed by its head only, so the sources do not say whether the list should name every top-level slot. That shape is unchanged from before.

The vendored raw ICL parser cannot check any ordinarily formatted AccessLink: its AccessLinkGeneric_def is a lexer rule (icl.g4:395-396), so AccessLink X Of STD_1149_1_2001 lexes as one token and every such block -- including the published ones above -- fails to parse. The claim in earlier versions of this docstring that the emitted block was "grammatically valid" was therefore never actually checked by that tool; the validation paragraph below covers include_access_link=False output only.

No confirmed real ICL mechanism exists for a READ instrument's fixed capture_value stub (a warptap-internal testing artifact with no real hardware counterpart) -- this emitter does not fabricate a plausible-looking CaptureSource for one; it emits an explanatory comment instead, matching this project's "document limitations loudly rather than invent something that looks plausible" discipline (e.g. Stage 4's closed-instrument free-running limitation, Stage 7's STAPL COMPARE scope note before it was resolved).

Live validation status against the vendored Honza255/icl_parser (this session's findings, not assumed): this emitter's output was iteratively corrected against several real, previously-unknown requirements of that tool's real grammar/checker -- every ScanInterface must contain a classifiable port (a TMSPort/ToTMSPort, ShiftEnPort/SelectPort, or ToShiftEnPort/ToSelectPort, confirmed by a real, named error the tool raises otherwise); a bare TAP-level reset must be a TRSTPort, not a generic ResetPort; WriteDataSource/WriteEnSource belong to DataRegister, never ScanRegister (this project's own first attempt got this wrong, corrected against the tool's real grammar file, not by re-reading research notes); and a leaf instrument module's scan ports need no ScanInterface wrapper at all. With those fixed, every module this emitter produces parses and passes the tool's own structural/semantic checks cleanly -- proving grammar and structural correctness independently of this project's own code, the same role OpenOCD/the Jam STAPL Player already play for SVF/STAPL (implementation_plan.md §7 Stage 7). The tool's own deeper retargeting-vector computation (IclRegisterModel, used for iWrite/iRead/iApply-vector generation) used to hit a real internal AssertionError against any width>1 instrument's DataRegister+ScanRegister pairing -- three separate bit-serial-scan-port assumptions in the vendored tool itself, root-caused and fixed directly in third_party/icl_parser (see :mod:warptap.icl_import's module docstring for the full diagnosis). Stage 10 ships with both grammar/structural AND retargeting-graph live validation now passing for every network shape this emitter produces, width>1 included.

IclEmitError

Bases: WarptapError

Raised when a :class:~warptap.icl_model.PhysicalGraph asks for something this ICL emitter can't represent -- a slot with neither an instrument nor a nested network (or both at once; this re-validates the same invariant :class:~warptap.icl_model.validate_physical_graph/ :class:~warptap.sib_insert.SibInsertError enforce elsewhere, since a PhysicalGraph can be built directly, e.g. in tests, without ever calling insert_sib_network), a zero-width instrument, or a WRITE instrument with no signal_bits -- rather than silently emitting something that looks plausible but describes nothing real.

to_icl

to_icl(graph: PhysicalGraph, root: ModuleInstance, *, include_access_link: bool = True, bsdl_entity_name: str | None = None, ijtag_access: bool = False) -> str

Render the complete ICL description of graph (from sib_plan.build_sib_plan, ideally called with top_name set to the real target module's own name -- root.name is used directly as this file's top Module name, no separate module_name parameter, since accepting the real name twice would only reintroduce a keep-two-values-in-sync risk sib_plan.build_sib_plan's own top_name fix was meant to remove).

Emits: render_sib_module_type() once, one render_scan_mux_module() per distinct :class:~warptap.icl_model.ScanMuxNode (any depth/branch), one render_instrument_ module() per distinct instrument, then the top module -- a ScanInterface naming the five real JTAG pins (:mod:warptap.tap_ports), the SIB/mux/instrument Instance statements (render_sib_instances), and (when include_access_link) a trailing AccessLink warptap_tap Of STD_1149_1_2001 block: BSDLEntity <entity>; then EXTEST { ScanInterface { <first slot>; } } -- the instruction that selects the network (:data:warptap.bsdl_emit.NETWORK_ACCESS_BSDL_INSTRUCTION), named exactly as :func:warptap.bsdl_emit.to_bsdl declares it, so the ICL and the BSDL agree. The module docstring lists the sources that confirmed this syntax, what is still unconfirmed, and the open question about which slots the ScanInterface list should name. No ActiveSignals clause: it names signals the AccessLink itself provides, and warptap's network needs none. The chain's first slot works here whether it's a SIB or a mux -- both module kinds declare the identically-shaped ScanInterface client { Port SI; Port SEL; Port SO; }.

bsdl_entity_name sets the BSDLEntity name (default: root.name, the top module's name); pass the same string given to :func:warptap.bsdl_emit.to_bsdl when the BSDL file's entity isn't named after the top module. It must be an ICL identifier. An AccessLink needs a scan interface to name, so include_access_link=True with an empty chain raises :class:IclEmitError rather than emitting a block ICL's grammar rejects.

ijtag_access=True names IJTAG_ACCESS in the AccessLink instead of EXTEST: pass it for a TAP inserted with an ijtag_access_opcode, whose BSDL (to_bsdl given the same opcode) declares that instruction.

include_access_link defaults to True. include_access_link=False output is unchanged by everything above (pinned byte-for-byte by tests/test_icl_emit_golden.py). The vendored Honza255/icl_parser -- Stage 10's own live-validation oracle -- raises "Not supported" for AccessLink regardless of what's inside it (verified directly against the real library, not assumed), and additionally mis-lexes an ordinarily formatted AccessLink header (see the module docstring), so callers validating against that specific tool should pass include_access_link=False; that is a real limitation of that one external tool, not of the ICL this function emits, and is documented here rather than silently worked around. Raises :class:IclEmitError for anything v1 can't represent.

warptap.icl_import

ICL import (implementation_plan.md §7 Stage 12): parses real .icl files entirely via the vendored Honza255/icl_parser (the same submodule Stage 10's icl_emit.py validates against), never touching raw ICL text directly -- extending this project's established "shell out to a real independent tool, never round-trip through a self-written parser" discipline to parsing, not just validation.

Scope, narrowed deliberately, matching the approved plan's own language: this recognizes only warptap's own canonical SIB-network shape -- a top module whose direct children are :data:~warptap.icl_emit.SIB_MODULE_TYPE-typed instances chained InputPort SI = <prev>.SO; from the top module's own tdi through to its tdo, each gating exactly one instrument instance (found via that SIB's own fromSO binding) named with :data:~warptap.icl_emit.INSTRUMENT_MODULE_PREFIX, OR (recursively) another :data:~warptap.icl_emit.SIB_MODULE_TYPE instance -- a hierarchy SIB gating a nested sub-network, detected structurally via that same fromSO binding, exactly mirroring :mod:warptap.icl_emit's own emission the other direction. This is not a general IEEE 1687 SIB-network importer -- arbitrary real-world ICL (parametrized generic instruments, bare ScanMux edge cases, an IR-decoded DR-mux TAP with no SIB pattern at all, all genuinely different shapes found in the vendored tool's own tests/test_icls corpus) is correctly rejected here, not silently misinterpreted -- see tests/test_icl_import_external_fixtures.py for the specific, pinned rejection reason each one hits.

Direction is detected structurally, not by name: an instrument instance carrying an IclDataRegister (DataRegister in ICL text) is WRITE; one with only an IclScanRegister is READ -- matches :mod:warptap.icl_emit's own real DataRegister-vs- ScanRegister grammar distinction (Stage 10's own hard-won finding) exactly.

A real, permanent round-trip limitation, not a bug: :mod:warptap.icl_emit records which real host net/bit an instrument's CaptureSource/WriteDataSource represents only in a // comment (e.g. // CaptureSource fans out from real host port bit(s): bist_start[0]) -- the CaptureSource/WriteDataSource clause ICL text itself actually carries is always a self-reference (CaptureSource DR[...]/WriteDataSource SR[...]), because ICL has no confirmed mechanism for naming an external signal there that this project's own research found (see icl_emit.py's own module docstring). ANTLR discards comments during parsing, so :func:import_icl has no way to recover the original :class:~warptap.icl_model.SignalBinding values, nor a READ instrument's original fixed capture_value stub (also comment-only). Every imported :class:~warptap.icl_model.InstrumentNode therefore always has signal_bits=() and capture_value=0, regardless of what the original network actually had -- stated here permanently rather than silently returning plausible-looking but fabricated values. Chain order, instrument names, widths, and READ/WRITE direction all round-trip exactly.

A real bug in the vendored tool's own retargeting-graph construction, found, precisely diagnosed, and fixed directly in the vendored submodule (third_party/icl_parser, not a warptap-side workaround): Ijtag(...) used to raise a bare AssertionError from IclRegisterModel for any network containing a width>1 instrument, regardless of instrument count or READ/WRITE direction. Root cause was three separate assumptions in icl_register_model.py/icl_items.py that a bit-serial scan port (a SIB's 1-bit SI/SO/fromSO/toSI) connects only to a same-width signal, when real SIB connectivity requires it to bind to a register of any other width in both directions (narrower driving wider: replicate; wider driving narrower: pick one representative bit) -- including a whole-port ScanInSource binding (e.g. ScanInSource SI[4:0]; on a 5-bit register), which the tool's own hand-written test fixtures never exercise (they only ever bind a single explicit bit, e.g. ScanInSource TDI[0];). Fixed in get_element_driver/get_scanin_named_index to resolve a scan port's driver at its own natural width and treat a whole-port ScanInSource reference's bit 0 as the register's MSB entry point.

A real, important correction to the paragraph above, found via a real upstream maintainer's own review (Honza255/icl_parser PR #1): the crash's actual trigger was narrower than first diagnosed. Confirmed directly (checked out the vendored tool's own pre-fix commit and tested both shapes against it): a width>1 register alone never crashes the retargeting-graph build, as long as the ScanOutPort/ScanInPort touching it stays scalar (1 bit), which is what every real ICL fixture in the tool's own corpus already does -- only warptap's own emitter, at the time, additionally widened those scan ports to match the register's own width (icl_emit.py's own module docstring covers the fix), which is what actually triggered the crash. So this fix's own value is now genuinely in question: it may never be needed for any ICL this project emits going forward, now that :func:~warptap.icl_emit. render_instrument_module no longer produces the non-standard shape that required it. Left here, not rewritten, as an honest record of the original (over-broad) diagnosis -- see the vendored fork's own PR #1 thread for how this played out with its maintainer. A separate, independent performance bug in the same construction path -- icl_process.py computing inspect.stack()[0][3] (a full stack capture with per-frame source-file resolution) purely to log each parse handler's own already-known name -- made any network large enough to be interesting impractically slow; replaced with the equivalent but allocation-free sys._getframe().f_code.co_name (roughly 15x faster on the affected paths, confirmed by profiling).

A much larger, separate performance finding: Ijtag(...)'s constructor always built a full Z3 SMT-solver-based retargeting-vector model (IclRegisterModel/create_retageter, used for that tool's own iWrite/iRead/iApply vector generation) as an inseparable part of construction, with no way to opt out through the tool's own original public API. Profiling found this -- not the graph traversal above -- is the real scaling bottleneck: roughly linear in instrument count, but a network of just 5 instruments at width 4 exceeded five minutes, while the exact same shape completes in well under a second once this step is skipped. :func:import_icl never uses ijtag.ijtag_reg_model/ijtag.icl_retargeter at all -- only ijtag.icl_instance (the structural parse tree, fully built before this step even starts) -- so this cost was always being paid for nothing. Fixed by adding an additive build_register_model: bool = True parameter to Ijtag.__init__ (default preserves every existing caller's behavior exactly, including icl_emit.py's own validation tests, which deliberately want the full retargeting-graph build); :func:import_icl passes False. Confirmed empirically: a 100-instrument, width-16 network (1600 total bits, comfortably industrial-scale) imports in ~7 seconds with this fix, versus not completing at all within five minutes at a small fraction of that size beforehand.

Ijtag(...) also raises ValueError unconditionally for any AccessLink block, regardless of content (a real, still-open gap in the vendored tool, not warptap's). All three are caught here and re-raised as :class:IclImportError naming the real cause, rather than letting a bare third-party traceback surface as this project's own failure; the AssertionError branch remains as a safety net for any other, not-yet-diagnosed internal assertion the vendored tool might still raise for a shape this project hasn't hit.

IclImportError

Bases: WarptapError

Raised when the given .icl file(s) don't describe a network :func:import_icl can recognize -- not a warptap-shaped SIB network at all (flat or nested), or a real gap in the vendored icl_parser itself (its "Not supported" AccessLink gap, or some other not-yet-diagnosed internal AssertionError in its retargeting-graph construction -- see module docstring for the width>1 instrument case that used to live here and is now fixed) -- rather than a bare, undiagnosed exception from third-party code.

import_icl

import_icl(icl_paths: List[Path], top_module: str, *, icl_parser_module) -> Tuple[PhysicalGraph, ModuleInstance]

Parse icl_paths entirely via icl_parser_module (the vendored Ijtag class, injected the same way tests/conftest.py's icl_parser_module fixture already provides it to Stage 10's validation tests) and convert the result into warptap's own :class:~warptap.icl_model.PhysicalGraph/:class:~warptap.icl_model.ModuleInstance.

Raises :class:IclImportError for: a real icl_parser gap (its own retargeting-graph AssertionError, or its "Not supported" AccessLink rejection -- both already documented above); a top module with no :data:~warptap.icl_emit.SIB_MODULE_TYPE/ :data:~warptap.icl_emit.SCAN_MUX_MODULE_PREFIX instances at all; a SIB/ScanMux chain (flat or nested) that doesn't run cleanly from tdi to tdo; an instrument instance not named per :data:~warptap.icl_emit.INSTRUMENT_MODULE_PREFIX convention; or a READ instrument with other than exactly one ScanRegister. A hierarchy SIB or ScanMuxNode (one whose fromSO/fromArmK resolves to another SIB/ScanMux instance rather than an instrument) is detected structurally and recursed into -- matching :mod:warptap.icl_emit's own emission the other direction.