Skip to content

PDL

Driving an inserted network, and PDL text emission/import.

warptap.pdl_interpreter

PDL (Procedural Description Language) interpreter and static SIB-network retargeting (implementation_plan.md §7 Stage 5, §3.3). iWrite/iRead/iRunLoop are non-executing setup commands mutating pending state; iApply is the sole action command, resolving the SIB path to the current iTarget scope and emitting a two-phase Capture-Shift-Update sequence as :mod:warptap.tap_ir ops -- pure data, no simulation side effect. A downstream backend (tap_ir_play.play() for cross-sim testing; a future Stage 7 SVF/STAPL emitter) walks the same ops list, demonstrating the "stateless pretty-printers over one IR" property §3.4 promises.

Never touches the netlist/insertion layer -- constructed directly from sib_plan.build_sib_plan's two return values, mirroring SibNetworkRegister(graph)'s own dependency shape, matching Stage 4's "keep it fully independent" precedent.

Stage 11 adds self.history (:mod:warptap.pdl_history) alongside self.program: a parallel, additive-only record of each successful command call in call order, needed because which instrument/value a given iApply actually targeted is provably unrecoverable from the low-level ShiftDR ops alone (see pdl_history's own module docstring). Nothing about self.program's own deferred/batched lowering changes.

PDLError

Bases: WarptapError

PDL-command-layer misuse not already covered by a lower-layer exception (ICLAddressError for a bad iTarget path, SibRetargetError for an unreachable instrument) -- e.g. iWrite/iRead/iApply called before any iTarget.

PDLInterpreter

Drives iTarget/iWrite/iRead/iRunLoop/iApply against a single SIB network (implementation_plan.md §7 Stage 4), flat or nested.

program holds DR scans only; it never loads an instruction. Before playing it, the TAP must hold EXTEST, the only instruction that selects the network (:data:warptap.tap_model.NETWORK_ACCESS_INSTRUCTION): prepend select_instruction(OPCODE_EXTEST) (:func:warptap.tap_integrity.select_instruction), or select_instruction(<its ijtag_access_opcode>) for a TAP inserted with one, where IJTAG_ACCESS selects the network instead. Every reset loads IDCODE, under which the network holds still, so a program played without that step reads nothing back and commits nothing.

iApply

iApply() -> list[Union[ShiftDR, GotoState]]

The sole action command. Resolves the current scope's gating SIB, emits phase 1 (one or more rounds closing whatever's open and opening the new target -- sized against the network's actual prior state, implementation_plan.md §3.3's network_configuration seam) then phase 2 (shift the real payload/expected-read value with the target's segment now spliced in), clears the pending write/read for this target, and returns the ops (also appended to self.program).

Phase 1 is one round per :func:~warptap.sib_retarget.stage_open_sequence entry, not always exactly one: a closed SIB's nested content isn't physically part of the live scan chain yet (confirmed against real RTL, tests/test_sib_cell_nested_cross_sim. py), so opening a target nested d levels deep genuinely needs d separate Capture-Shift-Update rounds, each one committing one more ancestor open before the next round can address anything inside it. For today's -- and every pre-nesting -- case (a directly-gated top-level instrument), this loop runs exactly once, with the same open_now/open_after values phase 1 always used, so behavior is byte-identical to before this loop existed.

Each round's length is a function of whatever is open right now (self. _currently_open for the first round; the previous round's own result after that), not a static len(graph.chain) baseline -- if a previous iApply targeting a different instrument left some other SIB open, the first round must shift through that instrument's still-physically-present content (as don't-care garbage) to close it. This is load-bearing for correctly sequencing back-to-back iApply calls, not merely future-SAT-extension bookkeeping.

For a READ instrument this garbage shift has zero lasting effect (bc1_shift_only.v has no update latch). For a WRITE instrument it does (implementation_plan.md §7 Stage 9): every phase-1 round always feeds 0 for a non-target slot's content (sib_layout.compose_bits's own documented behavior), and instrument_write.v's update-latch commits on any edge that leaves its gating SIB open both before and after -- which includes a phase-1 round's own Update-DR whenever a WRITE instrument stays open across it. Retargeting away from an open WRITE instrument (closing it) is safe -- the closing edge itself is gated off (open-before but not-open-after) -- and a value survives being closed and later reopened via a different intermediate target, since it's never touched while closed.

The one case this used to NOT protect -- fixed by :func:~warptap.sib_retarget. stage_open_sequence's own currently_open parameter (retargeting shift-length optimization plan): calling iApply twice in a row for the same still-open WRITE instrument with no intervening different target. Before the fix, phase 1's round saw that instrument open both before and after (nothing changed) and still ran a real Update-DR, so its 0 don't-care fill committed, clobbering the value before phase 2 ever ran -- confirmed as a real bug on real RTL (not just the Python model), for both a plain SIB and a :class:~warptap.icl_model.ScanMuxNode arm, before this fix landed. Since target_open in that exact scenario is byte-identical to self._currently_open, :func:~warptap.sib_retarget.stage_open_sequence now returns zero rounds for phase 1 -- no redundant Update-DR occurs at all, so there is nothing left to clobber the value. See tests/test_sib_insert_write_instrument_ cross_sim.py/tests/test_sib_insert_scan_mux_cross_sim.py for the permanent real- RTL regression tests proving this.

Targeting an instrument gated by a :class:~warptap.icl_model.ScanMuxNode arm -- including switching directly from one already-open arm to a different one -- needed no changes here at all (multi-arm ScanMux plan Phase 4): open_path_to's own PathStep.value already carries which arm, and phase 1's existing round-per- stage_open_sequence-entry loop already isolates "commit the new arm" (phase 1, target_sib=None) from "deliver its payload" (phase 2) into separate rounds -- exactly the same separation a hierarchy SIB's own open-then-descend already needed, not a new mechanism. Confirmed by :mod:tests.test_pdl_interpreter_scan_mux, which drives this exact method (not a hand-built compose_bits sequence) through an arm switch.

A real, confirmed bug, fixed by self._committed_writes: phase 2's own compose_bits call always re-asserts the identical opened/opened state before and after, so stays_matched/stays_open always holds for the target across that edge -- phase 2 always commits its own payload, regardless of whether a fresh iWrite was queued this apply. Before this fix, no fresh iWrite meant payload silently defaulted to 0, which still committed -- a read-only iApply of an already-open WRITE instrument (no fresh write, iRead only) correctly observed its own real value (self-captured at Capture-DR, before that same round's own zero-fill Update-DR), but silently zeroed it out for any later touch. Confirmed as a real bug on real RTL (not just the Python model), for both a plain SIB and a :class:~warptap.icl_model.ScanMuxNode arm, before this fix landed -- and unrelated to the phase-1 fix two paragraphs up (that one only ever protected the first reapply; this covers every later one too, and every plain read-only touch in between). Now, absent a fresh iWrite, phase 2's own payload falls back to self._committed_writes, the target's own last-known committed value, instead of a bare 0 -- re-asserting what's already there is a real, physically correct no-op commit, not a fresh clobber. See tests/test_sib_insert_write_instrument_cross_ sim.py/tests/test_sib_insert_scan_mux_cross_sim.py for the permanent real-RTL regression tests proving this.

iRead

iRead(expected: int, field: Optional[str] = None) -> None

Queue an expected value for the current scope's next iApply -- populates the emitted ShiftDR's tdo/mask fields (§ note in iApply), not evaluated here. With no field (the default), the whole instrument's content is compared, exactly as before -- resolution of which bits that means is deferred to iApply (which already looks up the target's width via _target_layout anyway), not resolved eagerly here, so this path adds no new lookup/failure mode for existing callers. With field naming a declared :class:~warptap.icl_model.Alias, only that sub-range is masked in; the rest of the instrument's content is don't-care for this read -- real PDL's named sub-field addressing, resolved eagerly here since it needs the instrument's declared aliases, unlike the whole-instrument case.

iRunLoop

iRunLoop(count: int, *, sck_port: Optional[str] = None) -> None

Not deferred like iWrite/iRead: a "wait N cycles" has no pending value to overwrite before some later flush, so this appends directly to self.program at the position encountered. With no sck_port (the default -- byte-identical to pre-Stage-15 behavior), cycles TCK in Run-Test-Idle exactly as before -- real PDL's -tck selector. With sck_port naming a real functional-clock port, appends a :class:~warptap.tap_ir.PulsePin instead of a :class:~warptap.tap_ir.Runtest -- real PDL's -sck (system clock) selector, reusing the exact primitive Stage 13/14 already built and real-cross-sim-validated for "pulse a named signal independent of TCK's own timing domain," rather than inventing a second, parallel concept. ICL has no confirmed mechanism for declaring a port as "the system clock" (unlike TCK, which is a real, named port kind), so sck_port is a Python-side-only parameter, never bound to an ICL declaration -- the same "caller must separately supply the real clock's name" pattern faultflow_retarget.retarget_faultflow_patterns's own clock_port parameter already established. PDL text itself, unlike ICL, does have a real place for this name -- pdl_emit.to_pdl renders it as -sck's own operand (confirmed against a real, independently-authored PDL grammar; see that module's own docstring).

iTarget

iTarget(dotted: str) -> None

Resolve dotted against the module-instantiation tree, becoming the scope iWrite/iRead address. Lets ICLAddressError propagate unwrapped for a bad path -- it already names the problem precisely. Recorded to self.history only once resolution succeeds, matching every other command's own error-tolerant semantics (nothing else here mutates state on a failed call either).

iWrite

iWrite(value: int, field: Optional[str] = None) -> None

Queue value to be written to the current scope on the next iApply. With no field (the default -- byte-identical to pre-Stage-15 behavior), addresses the whole instrument, as before. With field naming a declared :class:~warptap.icl_model.Alias on the current scope's instrument, merges value into just that sub-range of the pending value via :func:_set_bit_range, preserving whatever else is already queued for this instrument -- real PDL's named sub-field addressing (TDR_bit/UCreg/ICL Alias, confirmed real by independent research sources), closing what was previously a documented, stated gap rather than a silent one.

current (the sub-field merge's own starting point) prefers whatever's already pending for this instrument in the current, not-yet-applied batch (an earlier iWrite call since the last iApply, matching the field-merge precedent this docstring already describes) -- falling back to self._committed_writes, the instrument's own last-known-committed value from a previous iApply, only when nothing is pending yet this batch. A real, confirmed bug fix: without this fallback, a sub-field write in a fresh apply cycle (no other field of the same instrument also queued this batch) silently clobbered every other field to 0 instead of preserving its own real last-committed value -- see :func:PDLInterpreter.iApply's own docstring for the full mechanism, shared with a closely related phase-2 payload bug this same self._committed_writes fixes. Raises :class:PDLError for an unknown field.

warptap.pdl_emit

PDL (Procedural Description Language) text emitter (implementation_plan.md §7 Stage 11). Walks a PDLInterpreter's own self.history (:mod:warptap.pdl_history) -- not self.program, which is already-lowered low-level tap_ir ops with no retained per-instrument identity, see pdl_history's own module docstring -- producing real PDL statement text, the third of §3.4's "stateless pretty-printers over one IR/model" family (after tap_ir_svf.py/tap_ir_stapl.py's tap_ir emitters and icl_emit.py's icl_model emitter).

No independent PDL semantic validation tool exists anywhere -- confirmed by an exhaustive search of the real PDL ecosystem, and reconfirmed directly against the vendored Honza255/icl_parser submodule's own retargeting engine (IclRegisterModel/Ijtag. iWrite/iApply/getiApplyVectors): tried directly against a warptap-canonical SIB network and confirmed it doesn't compute a correct result for one -- that engine's own README states plainly that DataRegister retargeting ("Data registers"/"Retargeting for data registers") is explicitly not supported, exactly the register kind a WRITE instrument's own committed value lives in.

A real, independent GRAMMAR does now exist, though: that same submodule's src/pdl_parser/pdl.g4 was a bare, never-compiled ANTLR grammar (a real "PDL0 grammar v20130806", not a stub) with zero generated lexer/parser code and zero Python wiring anywhere in that repository. Compiled here via ANTLR 4.7.2 (matching the exact version already pinned for the ICL grammar) after fixing several real bugs in the grammar file itself (an unterminated rule, an undefined rule reference, two rule names colliding with Python builtins in the Python3 target, a missing WS token) and adding one new entry rule (see :func:tests.conftest.pdl_parser_module's own docstring for why: invoking the grammar's bare commands rule directly does not require full input consumption, which silently hid real failures) -- see the vendored fork's own commit history for the full list. This closes the gap at the same tier Stage 7's SVF/STAPL validation already sits at: independent grammar correctness (this emitter's output parses as real PDL syntax), not independent retargeting- semantics correctness (which nothing here checks, for any format, including the ones with real external validators) -- see tests/test_pdl_emit_grammar_validation.py.

A real, confirmed, permanent gap in this specific bundled grammar: it has no iTarget/scoping construct at all (absent from its own keyword list, from command's own alternatives, and from the file entirely) -- an older PDL dialect than the one this project's own earlier research found iTarget in (implementation_plan.md's own §3.2/§9 research, predating this grammar ever being compiled). Every real to_pdl() output opens with iTarget, so this grammar can validate each statement's own inner syntax but never a full emitted sequence as one unit -- test_pdl_emit_grammar_validation.py strips iTarget lines before validating everything else, and separately pins the gap itself with a real, reported parse error rather than leaving it silently unchecked.

Live-validating against this newly-compiled grammar caught one real, genuine bug this way: this emitter used to render -sck as a bare flag with no port name, reasoning (at the time) that neither PDL text nor ICL gave the port a confirmed place to live. The compiled grammar's own irunloop_def rule proves that reasoning incomplete for the PDL-text half of that claim -- -sck takes a mandatory port-name operand (WS '-sck' WS port, the last WS itself a real grammar bug fixed above) -- and pdl_emit.py already has that name in hand (PdlRunLoopStmt.sck_port), so omitting it produced text that fails to parse as real PDL. Fixed by rendering it: iRunLoop <count> -sck <port>;.

Field addressing (Stage 15): real PDL addresses a named sub-field (TDR_bit/UCreg/ ICL Alias) inside iWrite/iRead. pdl_history.PdlWriteStmt/PdlReadStmt.field is either a declared :class:~warptap.icl_model.Alias name (when PDLInterpreter.iWrite/ iRead was called with one) or the whole target instrument's own name (Stage 5's original, still-supported case) -- this emitter renders exactly that string either way, not a fabricated sub-field; _instrument_width resolves the correct hex-padding width by searching the target instrument's own aliases first, falling back to its whole width.

Value formatting: iWrite/iRead values render as a zero-padded 0x-hex literal sized to the addressed field's own width (ceil(width / 4) hex digits -- the alias's width when one was addressed, else the whole instrument's) -- matching real PDL usage this session's research found, and this project's own "always emit every field explicitly" house style (tap_ir_svf.py, tap_ir_stapl.py).

iRunLoop renders -sck <port> or bare -tck (Stage 15) depending on whether PdlRunLoopStmt.sck_port is set -- matching whichever selector PDLInterpreter.iRunLoop(..., sck_port=...) was actually called with (Stage 5's original -tck-only rendering was the sck_port is None case all along). -tck stays a bare flag (TCK is always the one, implicit TAP clock, needing no name); -sck renders its own port name, per the newly-compiled grammar's own requirement (see module docstring above).

PdlEmitError

Bases: WarptapError

Raised when to_pdl is given a statement it can't render -- currently only an iWrite/iRead field naming an instrument absent from graph (a caller passing a history/graph pair that didn't come from the same PDLInterpreter).

to_pdl

to_pdl(history: List[PdlStatement], graph: PhysicalGraph) -> str

Render history as PDL text, one statement per line, in the exact call order they were recorded. Raises :class:PdlEmitError if an iWrite/iRead statement names an instrument not present in graph -- never happens for a history/graph pair that came from the same :class:~warptap.pdl_interpreter.PDLInterpreter, but guards against a caller passing mismatched arguments rather than silently emitting an unsized literal.

warptap.pdl_import

PDL import: replays real PDL text against a live :class:~warptap.pdl_interpreter. PDLInterpreter (:mod:warptap.pdl_emit's own natural companion -- the "other direction" :mod:warptap.icl_import already plays for ICL, extended to PDL now that a real, compiled PDL grammar exists -- see :mod:warptap.pdl_emit's own module docstring for how that grammar went from an unwired, never-compiled artifact to a real parser).

Structurally different from :mod:warptap.icl_import's own shape, and deliberately so: ICL is declarative (a topology to reconstruct into a fresh :class:~warptap.icl_model. PhysicalGraph/:class:~warptap.icl_model.ModuleInstance); PDL is procedural (a program to execute). So this doesn't build new objects the way import_icl does -- it drives an already-constructed :class:~warptap.pdl_interpreter.PDLInterpreter's own public iTarget/ iWrite/iRead/iApply/iRunLoop methods, in call order, exactly as if the caller had typed those calls directly. That naturally populates the interpreter's own history (comparable against the original for a round trip) and program (real, usable tap_ir ops) as a side effect, with no separate reconstruction step needed for either.

iTarget is hand-recognized, not grammar-parsed -- confirmed empirically (not assumed): the vendored, bundled PDL grammar (third_party/icl_parser/src/pdl_parser/pdl.g4, compiled this session) is a real "PDL0" dialect with no iTarget/scoping construct at all (absent from its own keyword list, absent from every command alternative, absent anywhere in the file) -- an older dialect than the one this project's own earlier research found iTarget in (implementation_plan.md's own §3.2/§9). Every other statement kind (iWrite/iRead/iApply/iRunLoop) is genuinely grammar-parsed here, walking the real compiled ANTLR parse tree via its own generated context accessors, never regexed -- iTarget is the one necessary exception, recognized by a small, tightly-scoped pattern matching exactly what :func:~warptap.pdl_emit.to_pdl itself ever renders for it (iTarget <dotted>;, one bare name, no real grammar construct to defer to).

v1 scope: the 5 statement kinds to_pdl() actually emits -- iTarget, iWrite, iRead, iApply, iRunLoop. Everything else the real grammar recognizes (iProc/ iCall, iOverrideScanInterface, iTake/iRelease/iMerge, iClock/ iClockOverride, iState, iGetReadData/iGetMiscompares/iGetStatus/ iSetFail, iScan, iNote) is rejected with a specific, named :class:PdlImportError -- matching :mod:warptap.icl_import's own "reject what's outside scope, don't misparse it" discipline, never silently ignored. Within the five supported kinds, a sub-form :class:~warptap.pdl_interpreter.PDLInterpreter itself has no equivalent for is rejected the same way: iApply -together (no batched-apply concept here), iRunLoop -time (cycle counts only, no wall-clock timing), and an enum-named or omitted iWrite/iRead value (to_pdl() only ever emits a numeric hex literal).

A real, stated scope limitation: this parses one statement per line, matching to_pdl()'s own always-one-statement-per-line rendering convention exactly -- not the full pdl_source grammar (multiple statements per line, a statement spanning multiple lines, free-standing comments). This imports what to_pdl() emits; it is not a general-purpose parser for arbitrary, hand-authored .pdl files.

PdlImportError

Bases: WarptapError

Raised for a PDL statement outside this importer's v1 scope (see module docstring for the exact list), or when the compiled PDL grammar itself rejects a line as invalid syntax -- rather than silently misinterpreting or dropping it.

import_pdl

import_pdl(pdl_text: str, interp: PDLInterpreter, *, pdl_lexer_class, pdl_parser_class) -> None

Replay pdl_text against interp (an already-constructed :class:~warptap.pdl_interpreter.PDLInterpreter over the caller's own graph/root), one line at a time, exactly as if each statement had been typed as a direct method call. pdl_lexer_class/pdl_parser_class are the compiled grammar's own pdlLexer/ pdlParser classes, injected the same way :func:~warptap.icl_import.import_icl accepts its own icl_parser_module -- see tests/conftest.py for how a caller/test imports them from the vendored submodule.

Raises :class:PdlImportError for anything outside v1 scope (see module docstring) or a line the compiled grammar itself rejects as invalid PDL syntax; lets any error from interp's own methods (e.g. :class:~warptap.icl_model.ICLAddressError for an unknown iTarget) propagate unwrapped, matching this project's own "let a lower layer's own precisely-named exception through rather than re-wrapping it into something vaguer" convention (e.g. pdl_interpreter.iTarget itself already does this).

warptap.pdl_verify

PDL functional verification (implementation_plan.md §7 Stage 9, §3.3's own promised "replay a retargeted sequence, compare against the PDL-declared expected value").

Core insight needing no new "expected value" concept: PDLInterpreter.iApply() already populates a ShiftDR's tdo/mask fields whenever a prior iRead() was queued (pdl_interpreter.py's own docstring: "not evaluated here") -- this module is that consumer. Pure comparison, no simulation involved; :func:correlate_observed is the only piece that touches a raw trace, and even that's just index bookkeeping via tap_ir_play.shift_op_ranges().

Deliberately does not predict what a real external signal's value should be -- that's the whole point of Stage 9's two-oracle split (see sib_insert.py/ sib_model.py's Stage 9 docs): scan-mechanics correctness is SibNetworkRegister/tap_ir_play.play()'s job; functional content correctness is checked here, against real RTL, with "expected" coming only from what the PDL author declared via iRead.

PDLVerifyError

Bases: WarptapError

Raised when observed_by_shift_op doesn't have enough entries to cover every shift op ir_ops actually contains -- a mismatched trace/ops pair, not a failed comparison (a failed comparison is a normal, non-exceptional ReadCheckResult).

ReadCheckResult

Bases: NamedTuple

check_reads

check_reads(ir_ops: List[_IrOp], observed_by_shift_op: List[int]) -> List[ReadCheckResult]

Compare each shift op's PDL-declared expectation (op.tdo/op.mask, populated only when an iRead was queued for that iApply) against the corresponding entry of observed_by_shift_op (one int per ShiftIR/ShiftDR op, e.g. from :func:correlate_observed or tap_ir_play.play()'s own return value).

A shift op with tdo is None (a bare write, no iRead queued) produces no result -- masked equality, matching PDLInterpreter's own _read_mask_bits convention: an iRead only ever asserts about its own instrument's content bits within a possibly- longer phase-2 shift, never the whole chain.

correlate_observed

correlate_observed(ir_ops: List[_IrOp], tdo_by_cycle: List[int]) -> List[int]

Turn a raw per-cycle TDO trace (reset lead-in already stripped by the caller, matching tap_ir_play.to_cycles()'s own precondition) into one int per ShiftIR/ShiftDR op, via tap_ir_play.shift_op_ranges().