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.