Inspection¶
Scope¶
Developer-facing inspection facilities for TileFoundry IR: DOT graph, Python DSL printer (round-trippable), pattern declaration reports, interactive HTML viewer, and dump integration.
1. HIR DOT¶
def hir_function_to_dot(fn: hir.Function) -> str:
"""Render an SSA HIR function as Graphviz DOT.
Args:
fn: Function to render.
Returns:
Graphviz DOT text.
"""
...
def module_entry_to_dot(module: Module) -> str:
"""Render a Module's entry function as Graphviz DOT.
Args:
module: Module whose entry is rendered.
Returns:
Graphviz DOT text.
"""
...
- constraints:
- Each
Var,Call, andConstantnode MUST show its type, shape, dtype, and sharding annotations. Shared expressions MUST be deduplicated by object identity.
2. Python DSL Printer¶
Canonical Python DSL printer that outputs executable @func /
@module source. The output is round-trippable: write as_script(fn) to a
Python file and import it, and the real authoring decorators MUST produce a
structurally equivalent IR. Printing that rebuilt value MUST reproduce the
same canonical text.
PythonPrinter.print(value, ctx=None, indent="") is the printer's one public
entry (core-ir §2.4); callers do not
call visit, which is the printers' own dispatch. It writes a DType as its
name, a tensor, pointer, mesh or layout type at indent, a Printable by
handing it the printer, an Enum, a Target through its to_python(), a tuple
item by item, a Python scalar as itself, and any other IR node through the
visitor, joining statement lines into one text. A value with no Python form
raises NotImplementedError. HirPrinter.print(fn, options=...) writes a whole
HIR function and prints any other value the same way.
2.1 Function printer¶
class PythonPrintOptions:
"""Control optional non-canonical printer annotations.
Attributes:
show_types: attribute; Whether to append inferred types.
comment_metadata_types: attribute; Metadata classes rendered as comments.
comment_opt_in: attribute; Declared keys a reader asked a comment for.
"""
show_types: bool = False
comment_metadata_types: tuple[type[IRMetadata], ...] = ()
comment_opt_in: frozenset[str] = frozenset()
def hir_function_to_python(
fn: hir.Function, *, options: PythonPrintOptions | None = None
) -> str:
"""Render a HIR function as Python DSL source.
Args:
fn: Function to render.
options: Optional inspection annotations.
Returns:
Python DSL source.
"""
...
- constraints:
- The output MUST include imports, a bare
@funcsignature with completeTensor[...]annotations, and the SSA body.
2.2 Module printer¶
def as_script(
fn: hir.Function | tir.PrimFunction | Module,
*,
module: str | None = None,
options: PythonPrintOptions | None = None,
) -> str:
"""Render a HIR/TIR function or Module as Python DSL source.
Args:
fn: Function or Module to render.
module: Optional wrapper class name for a Function.
options: Optional inspection annotations.
Returns:
Python DSL source.
"""
...
def module_to_python(fn: hir.Function, module_name: str = "M") -> str:
"""Render a function through the module-wrapper compatibility alias.
Args:
fn: Function to render.
module_name: Wrapper class name.
Returns:
Python DSL source.
"""
...
For TIR, as_script emits @prim_func and statement-form DSL. A
PrimFunction passed with module=None is rendered standalone; passing
module="Name" wraps it in an @module class, preserving the caller's
explicit wrapper request. Module output computes imports from names actually
used by the rendered program and MUST remain lint-clean.
- constraints:
- Module input MUST emit every HIR Function. Meshes used by a function are written at their scope boundary, and mixed HIR/TIR Modules MUST be rejected.
The printer MUST emit each Module definition once before the slots that use it,
with class-body bindings preserving shared definitions and instance keys.
It MUST emit only what a Module declares, never what the Module
resolves to — an inherited Target or topology hierarchy prints nothing, so
the declaration-versus-inheritance split survives the round trip. A declared
Target prints as the @module(target=...) argument and a declared hierarchy
as the @module(topologies=...) argument
(parser §3).
Every dimension referenced only by a declared topology expression MUST still
be emitted in the file header. A topology ShapeDim MUST use the same DSL
expression text as tensor and Mesh geometry, including public constructors such
as ceildiv, so importing restores the same expression tree.
- constraints:
- The decorator MUST print in its called form,
@module()included. A bare decorator has not run while the class body is evaluated, so a body naming a child call could not resolve it (parser §2.1). - Child definitions MUST precede their slot bindings. Instance calls print
the slot key and optional index, with exactly
Call.args, following core-ir §2.1.
The printer MUST import the concrete Target class from its provider module and
embed repr(target) as the constructor expression. That representation MUST
rebuild an equal value of the same concrete class when the emitted source is
executed. A Target subclass with a different constructor customizes ordinary
__repr__; there is no printer-specific Target hook or construction registry.
2.3 DSL text forms¶
DSL text forms for tensor / layout / shard annotations are owned by
parser. The printer reuses those forms only when they
round-trip without losing mesh / layout / storage information;
otherwise it falls back to the verbose ShardLayout(...).
Layouts and placements use the forms described in
shard §6, with acceptance rules in
parser §2. The printer uses sugar when it
preserves the value and the needed lexical bindings; otherwise it emits a
complete constructor. Python-evaluated constructor arguments retain
Layout(...) and ComposedLayout(...).
HIR operations and tile-window loops MUST use tf.<name>(...) and
tf.tile(...). Op values MUST use their schema name and recursively rendered
attributes, including layout sugar. A single Tuple[...] input MUST be emitted
as an explicit tuple of operands, retaining a trailing comma for one operand.
Storage attributes MUST be emitted as strings.
The printer has one type-text surface: the canonical Tensor[...] form of
parser §2.1, used by as_script(), comments, DOT,
the viewer graph, and viewer detail panels. Comments may flatten this text onto
one physical line, but MUST NOT change its syntax or semantics. The same
printer visitor supplies type, dimension, layout, and shard-attribute text;
there is no separate compact, display-only type language.
The meaning of Split / Partial / Broadcast is owned by
shard; these forms define only render syntax. Canonical DType text
is the descriptor's name. Tensor annotations and DType op attributes MUST
emit that name as a quoted DSL string.
2.4 Pretty-print / debug display contract¶
Pretty print is the core presentation layer. Sugar, debug dumps, DOT, and viewer type/value text reuse the same canonical DSL text forms in §2.3. That keeps round-trippable source, labels, and detail panes semantically aligned.
- op attributes that are
DType,TensorType,Layout, orShardLayoutare rendered through the §2.3 printer;DTypeuses its canonical name and these values do not use rawrepr()output
repr() is a debug surface, not the source of truth. It may delegate to
the §2.3 implementation for context-free values, but context-dependent
printing (for example, choosing stable mesh names across a whole function)
must use an explicit pretty-printer API rather than relying on no-argument
repr().
2.5 Mesh bindings and file header¶
The print context records imports and declarations while the printer visits the
program. After the body has been visited, the context emits the file header;
it MUST contain only imports and DimVar declarations reached by the output.
There is no module-level mesh hoist or global mesh name map.
TIR binding names MUST be allocated independently for each def, including
every specialization and each parent or child function. Collect all bindings
before allocating names: preserve valid identifiers, replace other characters
with underscores, and resolve collisions with _2, _3, and later suffixes
without consuming a later authored identifier. Parameters, LetStmt bindings,
mesh bindings, loop variables, and their references MUST use the same map.
Parent and child modules share the file's import collection, not binding names.
HIR printing MUST preserve existing binding names after identifier normalization
and collision resolution. Only unnamed values receive generated names. Their
numeric positions follow definition order independently of which values already
have names. Name allocation MUST reserve later authored identifiers before
assigning generated names or collision suffixes.
Constant tensor parameters MUST retain ConstTensor annotations.
A TIR SymbolRef MUST print its nested child-slot path followed by its function
name, with each component rendered as a Python identifier. The printer reads
that path from the IR and MUST NOT infer it by searching the module tree.
On import, a local helper(...) remains a retained PrimFunction call;
child.run(...) and launch targets retain their slot paths in SymbolRef.
The DSL surface MUST be imported with from tilefoundry.dsl import *, which
re-exports ir.types and the func, module, and prim_func decorators.
The file header starts with from __future__ import annotations, a blank line,
and from tilefoundry.dsl import *, without a noqa comment. Imports supplied
by a Target's to_python() provider follow unchanged.
Mesh headers omit row-major
strides and use the layout keyword. A topology is written by name only when the
function inherits it from the enclosing module; standalone prim_func and
explicit @prim_func(target=...) headers retain Topology(...). String
attributes use double quotes, and TIR tuple elements occupy separate lines.
Recoverable mesh selections MUST print relative to an active lexical parent;
a matching slice uses the slice form. Otherwise the printer emits a complete
mesh constructor. The function's execution mesh is available while printing
its signature and body; a named constant outside those bindings does not
create a printable lexical name.
A sharded type uses placement sugar only when each referenced mesh level has
an active binding with the same geometry. Equal axis names and topology sizes
alone do not establish that binding. Otherwise it MUST print the complete
ShardLayout(..., mesh=Mesh(...)) value, including in a register allocation
outside the scope that uses it.
Analysis annotations use the same type text for value-producing statements, but
a structural LoopRegion or MeshRegion line MUST NOT restate the aggregate
type of its body. The region may still carry selected analysis metadata.
2.6 Specialization printing¶
A dispatch prototype (hir.md §1.1)
prints as its base @func with a pass body, followed by each
variant as an @<name>.specialize(pattern) block in declared order:
# example
@func
def f(x: Tensor[(S,), "f32"]) -> Tensor[(S,), "f32"]:
pass
@f.specialize(RangePattern("S", 1, 3))
def small_sequence(x: Tensor[(S,), "f32"]) -> Tensor[(S,), "f32"]:
...
The pattern prints in its constructor form (RangePattern("S", 1, 3);
other Pattern subclasses fall back to repr(pattern)). The emitted binding
mirrors the authoring surface (parser.md §2.1);
when an IR variant has no display label, the printer synthesizes a valid binding
from its canonical specialization signature. Because a
dispatch prototype has a DimVar parameter, its rendering is a
display-only surface (§2.7): human-readable, not a round-trip
validation artifact.
2.7 Round-trip contract¶
For authored TIR, canonical fixture source is the contract:
as_script(module) == fixture_source. This pins the spelling authors consume,
not merely a printer/parser fixed point.
ShapeOf has the authored DSL spelling shape_of(x, axis=0). The printer MUST
use that spelling, and the expression MUST survive printing and re-import.
This TIR runtime extent query is distinct from HIR tf.shape_of.
A rendering is one of two surfaces:
Canonical — the rendering of a function with no DimVar parameter, or a
Module whose Functions have no DimVar parameter and whose symbolic dimensions
occur in round-trippable tensor, Mesh, or topology geometry. It
MUST round-trip: printing, importing, and the test-side structural comparison
MUST agree over
- Params: shape, dtype, storage, layout.attrs, layout.shape, layout.strides, mesh identity (topology name/size, layout shape, names)
- Body: op class, args, keyword attrs, types
- DType annotations and op attributes preserve the selected descriptor singleton through their canonical names
- Partial layouts preserve mesh names through the canonical
parser §2.1 value-state form, and preserve
Partial.reductionplus the attrs-position mesh axis in the underlying IR
Display-only — the rendering of a function with a DimVar parameter, a
dispatch prototype and its .specialize variants (§2.6).
A display-only rendering is
human-readable and MUST NOT be used as a structural round-trip validation
artifact.
The printer MUST emit all LoopRegion args before its for statement.
Invariant parameters print using their external argument names through the
capture alias chain, including captured tile induction variables. Reparsing
MUST preserve the parameters and arguments across loop and mesh boundaries.
A canonical tile window with static integer scaling and translation MUST retain
its index form (t * c or t * c ± k): start c * iv ± k, element count equal
to the tile step, and stride c. This keeps index dimension arithmetic inside
the subscript when reparsed.
A canonical grid loop MUST render each yielded expression under its own unique
binding. After the loop body has emitted every yielded expression, the printer
MUST emit one carry = yield assignment per carried value, in
LoopRegion.params[:len(yield_values)] order. Those assignments are the final statements
in the loop body. This preserves references to the old carry until the update
point and lets the parser's final-RHS carry rule rebuild the same loop.
A DimVar shape entry prints as its bare name, and a shape-valued op attribute
holding one MUST print it the same way rather than as its repr. The rendering
MUST therefore also emit, once, a declaration binding each such name to the
DimVar it stands for, with the envelope bounds it was declared with. Without
those declarations the printed source names something nothing defines and cannot
be imported at all, which is a weaker artifact than display-only is meant to be
-- the whole point of printing a program is that somebody can be handed the file.
The bounds are not recoverable from the name, so they are restated rather than
inferred.
2.8 Record comment forms¶
A record attached to an expression is typed fields and nothing else (core-ir §2). What one looks like on a printed line is decided here, by walking its fields: a family that wrote its own form string would be deciding presentation, and every family would then spell one value its own way.
A record MUST declare that it renders as a comment, and MAY declare which keys it emits: a field by its own name, anything else as a projection carrying its key, its type, and where its value comes from. Metadata with no declaration renders as nothing, which is how an annotation that is not a report -- a binding name, a constraint -- stays off the line.
A record an analysis report projects (analysis §1)
MUST NOT emit a key that projection cannot state; it MAY emit fewer, because a
comment is read on a line and JSON is read by a program. A record no report
projects is comment-only -- SourceSpanMetadata is where an expression was
authored, which no family measures -- and there is no projection for it to be a
subset of.
One separator per layer, and a separator MUST NOT appear inside a value it separates unless that value brackets itself:
| Layer | Separates | With |
|---|---|---|
| value | bytes read from bytes written | / |
| value | a whole quantity from one unit's share | @ |
| value | a mapping's key from its value | : |
| value | a mapping's entries | , |
| field | a key from its value | = |
| record | one field from the next | space |
| line | one record from the next | ; |
- constraints:
- A key MUST be its declared name with
_written as-. It MUST NOT be derived from the value, and a unit MUST be part of the name --ideal-ns=13, neverideal=13ns-- so the unit is stated once, where the field states it. - A value MUST be an
int, astr, a mapping of them, or a type with a declared rendering.intandstrMUST NOT be wrapped in a type that renders identically. - A
strthat is a token renders bare. Text that is a sentence MUST render as a quoted, escaped DSL string literal (§2.3), which is what lets it hold a separator the ladder uses; a reader splits a layer outside the quotes. - A key whose value equals what it declared it says nothing by MUST be left out, so a record that measured nothing collapses to its family name.
- A record declaring exactly one key MUST use the family name as that key: for a record of one thing the family and the key state the same thing.
- The family name MUST be the class name without a trailing
Metadata, in kebab case. A record whose reported name differs MUST state that name where its keys are declared. - Part zero of a line is the value's own type, which carries no key: it is not a measurement of the value, it is the value, and it is DSL text (§2.3) that pastes back. Every later part is a record.
Analysis rendering prints each reached definition once, with its Function summary and node records (analysis §2).
2.9 Pattern declaration reports¶
PatternPrinter renders operation-declaration patterns as reports rather than
as executable Python source. It is a pure MRO-dispatched visitor and does not
take a print context:
printer = PatternPrinter()
printer.written(pattern) # value shape, with named holes
printer.rules(pattern) # ordered, de-duplicated condition lines
printer.described(pattern) # value shape plus an indented predicates line
printer.alternatives(pattern) # flattened structural Or/Switch branches
printer.declaration(op_type) # rendered declaration sections, still structured
printer.refusal(refusal) # consumer-facing match failure
Layout values use single-line positional forms such as Layout((r, W), (l,
1)), ComposedLayout(Swizzle(b, 4, 3), p, Layout(...)), and
ShardLayout(Layout(...), (B(), B(), B()), Mesh(...)). Named wildcards print
their names, unnamed wildcards print _, and star captures print *name.
Predicate conditions retain structural traversal and authored declaration
order; de-duplication keeps the first occurrence. Alternative bindings retain
declaration order. An OrPattern containing only ordinary
values is one set-valued alternative rather than several structural branches.
For tensor arrangements, rules shared by every alternative appear in the
common predicates block; rules unique to one alternative remain indented under
that arrangement. Parameter defaults are stated on their parameter line.
Every Pattern carries its own predicates slot. The structural line comes
only from that pattern's structural fields, while its predicate block comes
only from pattern.predicates; the printer MUST NOT infer one role from the
text produced for the other. ShardLayoutPattern arrangements render all
three declared fields through the same visitor/rules dispatch: local layout,
shard attrs, and holder mesh. Instruction declarations call the reserved field
execution mesh everywhere, including the standalone facts field and the
declaration attributes section.
A refusal at a SwitchPattern reports its parameter name, actual subject,
and declared branch values: param has no matching branch for subject;
branches: value, .... It does not report a predicate from a rejected branch.
Instruction reports use match_data(paths, reason="") and
match_text(identifier, data). JSON provides path, an ordered list of
{"name": ..., "value": ...} entries, and reason. Values are printable strings.
All accepted paths merge by the first occurrence of each name; distinct values
join with |. Parameters and internal pattern captures receive the same
format. The text starts with the instruction identifier, then two spaces and
name=value entries separated by ,. A refusal places its
<parameter>: <failed-node text> reason on the next indented line. A successful
candidate has an empty reason. Type inference uses the same rendering.
Refusal subjects use the same DSL notation as the report operands; dtype and
storage values use their surface names. Structural patterns use that notation
as well, rather than mixing Python repr with DSL text.
Standalone refusal renders the path and reason without an instruction id;
the former reason (bindings) spelling is removed.
This report printer is separate from PythonPrinter: it does not inherit the
round-trip source printer, request import or mesh-name registration, or accept
a PrintContext. Pattern IR produces no text; verification may import this
printer lazily to render structural Refusal evidence without creating an
ir to inspection dependency.
3. Viewer¶
The viewer is the interactive HIR inspector. Viewer(root).serve(port,
open_browser) (root = hir.Function or Module) starts a local HTTP
server and opens a browser page that lays the graph out client-side
via the vendored WebAssembly Graphviz build (@hpcc-js/wasm) — there is
no server-side dot process. The page offers pan / zoom, a detail panel,
collapsible function regions, node search, and upstream/downstream
highlight on top of the rendered SVG.
3.1 Architecture¶
HIR Function / Module
│ ViewerBuilder (visitor)
▼
graphviz.Digraph ──► /api/dot?collapsed=<csv> (DOT text)
+ DetailIndex ──► /api/expr/<visual_id> (detail JSON, on demand)
──► /api/palette (colour palette)
│
▼ (browser)
@hpcc-js/wasm layout(dot, "svg", "dot") → innerHTML → SVG in #graph
│
▼
d3-zoom pan/zoom · click → detail + highlight · search · collapse toggle
- Backend is the
tilefoundry.inspection.viewerpackage.Viewer(root) .serve(...)ensures the vendored JS is cached, then serves five GET routes and returns the bound port:/(page),/static/<name>(first-party files, then cached vendor JS),/api/dot?collapsed=<csv>(a freshViewerBuilder(root, collapsed).build().source),/api/expr/<visual_id>(detail JSON formatted on demand from theDetailIndex), and/api/palette(the colour palette). Nodotsubprocess exists anywhere in the package. The server runs on a background daemon thread;servereturns the bound port immediately unlessblockholds the process open. Whenblockis unset it followsopen_browser— an interactive call (open_browser=True) blocks until interrupted so the page stays reachable; a programmatic call returns. - Builder.
ViewerBuilderwalks the HIRFunction/Moduledirectly into agraphviz.Digraph; there is no intermediate model. ACall(target=hir.Function)and a top-levelFunctionshare one unified emitter — a collapsed region renders as a stand-in node, an expanded one as asubgraph cluster_<region_visual_id>. - Frontend is
static/index.html+static/viewer.js(first-party, committed). On load it fetches/api/palette, then renders/api/dot; each collapse toggle re-fetches/api/dot?collapsed=...and re-renders. Rendering calls@hpcc-js/wasmlayout()to produce an SVG string that is injected viainnerHTML(no d3-graphviz data-join). - Vendored assets. The browser JS is NOT committed.
ensure_assets()downloads each exact-pinned URL once to a user cache ($TILEFOUNDRY_VIEWER_ASSET_DIR→$XDG_CACHE_HOME→~/.cache/tilefoundry/viewer-assets/<manifest-version>), verified against a baked-in SHA256 manifest; any mismatch raises. The repo'sstatic/holds only the first-party page assets (an allowlisting.gitignorekeeps vendor JS out).
3.2 Visual identity + detail index¶
visual_id is the stable id of every emitted artifact:
"__".join(call_path) + "__" + local. The call path namespaces inline
expansions, so two calls to the same callee produce disjoint node ids and
disjoint detail entries; a dispatch prototype's variants, which share the
base name, are likewise disambiguated by their canonical specialization
signature. region_visual_id follows the same scheme, and collapse state
is a set of region ids.
DetailIndex maps visual_id → DetailRef(hir_expr, kind, call_path,
region_visual_id?, param_index?). It is a click-lookup index, NOT a graph
model: it holds live HIR references, never pre-formatted panel JSON. Each
/api/dot build rebuilds and atomically replaces the index, so
/api/expr/<visual_id> always resolves against the currently displayed
graph; an id that was collapsed away returns 404.
3.3 Node rendering¶
- Function node. Title row: a
▼/▶toggle port, the clickablefn <name>title, then one cell per parameter showing the param name. When expanded the params span two rows —:pin<i>(top, where the caller connects) and:pout<i>(bottom, where the body reads) — so a single port is never both an external sink and an internal source. A collapsed stand-in instead carries:out<i>result ports. The cluster is tinted by nesting depth (an independent low-saturation channel). - Op call node. Title row (op name + per-operand input ports labelled
with the Op's declared field names), then one field row per non-input
attribute (
axis: 2,new_shape: …), then the result-type row. - Return producer. In an expanded function the real body producer of
each return slot carries a bottom
▼ out<i>marker (no separate anchor node), and a region's direct return producers share one rank. Output ports live on the collapsed stand-in only — an expanded header has no output port (that would read as the function depending on itself). - Var / Constant / Tuple.
Varshows its name;Constantuses the compact pretty value (const(0)/const([1.0f, …]), truncated past 8 elements);Tuplebundles its elements. Op attributes that are constants / types render through the §2.4 pretty-print, never rawrepr. - Type text. Graph labels and detail panels use the canonical type text from
§2.3, including
Tensor[...]and placement sugar when a scope binding is available.Reshard/ layout attrs render through the same visitor (never rawrepr).
3.4 Interaction contract¶
- Detail panel. Clicking a node title fetches
/api/expr/<visual_id>and rendersparams(name | type),returns(idx | type) andattrs(key | value), formatted on demand from the live HIR expr. Type text is canonical (§3.3); DimVar /@storagetokens are re-coloured client-side from/api/paletteusing the same rule as the graph. A stale id (after a collapse changed the index) yields 404 and the panel clears. - Upstream / downstream highlight. Clicking a node also highlights its
connectivity cone: unrelated nodes/edges dim, upstream edges take one
colour and downstream edges another. A header selector chooses the
direction — bidirectional / single / upstream / downstream — and
re-applies to the current selection on change. Clicking empty canvas
clears. Adjacency is derived from the rendered SVG (
g.edgetitles, ports stripped to bare node ids), transitively. - Search. A header search box highlights nodes whose id or visible label contains the query and dims the rest; it re-applies after a re-render and is independent of the highlight-direction mode.
- Collapse / expand. The
▼/▶toggle cell on a function node flips that region's collapse state, which re-fetches/api/dot?collapsed=<csv>and re-renders. Collapse state is mirrored in the URL hash so a refresh restores it. Highlight/search are pure client-side SVG class switching and never re-render the DOT. - Pan / zoom. Mouse wheel zooms and dragging pans (d3-zoom), composed on top of Graphviz's own layout transform.
3.5 File layout¶
Vendored browser JS lives only in the user cache, never in the repo.
4. Dump Integration¶
tilefoundry.dump.DumpScope + FileDumper / MemoryDumper / NullDumper
provide per-test, per-pass IR dumping (see passes §6).
# example
from tilefoundry.dump import DumpFlags, dump, current_scope
dump("ir.py", src, DumpFlags.PASS_IR)
class DumpFlags(IntFlag):
"""Enumerate dump categories."""
NONE = 0
PASS_IR = 1
CODEGEN_SOURCE = 2
BUILD_LOG = 4
ANALYSIS = 8
ALL = PASS_IR | CODEGEN_SOURCE | BUILD_LOG | ANALYSIS
class DumpScope:
"""Install or narrow a dynamically scoped dump destination."""
def __init__(
self,
subdir: str | None = None,
flags: DumpFlags | None = None,
*,
dumper: IDumpper | None = None,
) -> None: ...
def dump(
self,
name: str,
content: str | bytes | Callable[[], str | bytes],
flag: DumpFlags,
) -> None: ...
def dump(
name: str,
content: str | bytes | Callable[[], str | bytes],
flag: DumpFlags,
) -> None: ...
- constraints:
DumpScope(subdir, flags)MUST nest beneath the active scope and intersect its flags with the parent's. Without a parent it MUST remain a no-op.DumpScope(dumper=..., flags=...)MUST replace the active scope.- Both
dump(...)andDumpScope.dump(...)accept strings, bytes, or a no-argument callable returning either. Module-leveldump(...)MUST NOT call the producer without an active scope. Both interfaces MUST NOT call the producer when the requested flag is disabled. When enabled, the producer MUST be called once immediately before writing; its exceptions propagate to the caller. - Plain child threads MUST start without the parent's scope; asyncio tasks MUST inherit a copy of the creating context.
- Test output MUST be rooted at
test_results/<file-stem>/<test-name>/. A non-master worker appends__<worker-id>to the test-name leaf rather than adding a worker directory.pytest.mark.no_dumpdisables it. - A completed
analyze(...)call MUST dump its complete report data asanalysis.jsonwhenDumpFlags.ANALYSISis enabled. Every conclusion in that dump MUST come from the selected typed records; the report layer MUST NOT reconstruct target-aware findings that no record carries.