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().