resolver module#

The resolver#

Batching, lockfile replay, and the run’s parts report. Mostly internal machinery; what a caller uses is the driver surface at the end of the file – parts_verdict(), flush(), commit_lock() and uncommitted().

Construction enqueues, a flush resolves the batch, and writes happen once at the end of the run.

A flush is triggered by the first read of deferred state, by introspection, or by Design.Initialized at the end of elaboration, whichever comes first. The last is the common case, which is what makes batching worth having. Resolving a batch means replaying whatever the lock can answer, sweeping orphaned entries for adoption, and asking the chain for the rest; each component then binds its part in the frame it was constructed in, through the PartsResolved event.

Pins are committed once the end-of-elaboration flush has resolved everything, so an ordinary run pins by itself and a driver need do nothing. parts_verdict() is still the driver’s to ask for, on every exit path including a failed one, since only the driver knows how a run ended and what to do about it. Verdict carries the counts, the failures and the exit status.

class PartsResolved[source]#

Bases: Event

Fired after a batch resolves, so each component binds in its own frame.

A provider’s materialize therefore runs as though it were where the component was constructed, and any ambient context it reads while building geometry – a symbol style set in an enclosing class body, say – is the one that was active there. A handler on this event must not construct a part that itself resolves through a chain; see NestedDeferral.

class Resolution(part, member)[source]#

Bases: object

A part, and the chain member that answered.

The two travel together because the member cannot be recovered from the part: identity.provider is free text the provider chose, two members may answer to one name, and a delegating provider names its inner source. Only the chain knows, and it only says so at the moment it answers – so the answer carries it from there to the lockfile.

Parameters:
part: ResolvedPart#
member: str#
class Severity(*values)[source]#

Bases: Enum

Whether an event fails the run. A property of the mode, not of the event.

INFO = 0#
FAILURE = 1#
class EventKind(*values)[source]#

Bases: Enum

What the resolver records. Severity is decided per mode; see severity_of.

PINNED = 'pinned'#
ADOPTED = 'adopted'#
FORFEITED = 'forfeited'#
RESOLVED = 'resolved'#

The lock could not answer, so the query resolved fresh. Informational under read-write, where it is the ordinary way a design grows; unlocked and fatal under a pinned mode.

REKEYED = 'rekeyed'#

A chain group the lock does not recognize. stale-chain under a pinned mode.

REPLACED = 'replaced'#

A provider disowned its payload. replay-failed under a pinned mode.

UNRESOLVABLE = 'unresolvable'#

the chain is unreachable. A failure in every mode.

Type:

Could not resolve at all

DIFFERS = 'differs'#

Fresh resolution differs from the lock. check only, and its namesake.

severity_of(kind, mode)[source]#

Whether kind fails the run under mode.

The same event is informational in one mode and a failure in another, which is what makes the report categories differ by mode without the mechanism differing.

Return type:

Severity

Parameters:
name_of(kind, mode)[source]#

The category name a mode reports this event under.

Return type:

str

Parameters:
class VerdictEvent(kind, description, detail='', origin='')[source]#

Bases: object

One recorded event, with what it was about.

Parameters:
kind: EventKind#
description: str#
detail: str = ''#

What happened, beyond the category. May span several lines.

origin: str = ''#

Where the component was constructed, when the event names it.

class Verdict(mode=LockMode.OFF, events=list())[source]#

Bases: object

What the run has to say about parts.

Non-empty on essentially every successful build, since hits and adoptions are recorded too. The exit status keys to the failure categories alone, never to emptiness: “has something to say” is not “has something to complain about”.

Parameters:
mode: LockMode = 'off'#
events: list[VerdictEvent]#
record(kind, description, detail='', origin='')[source]#
Return type:

None

Parameters:
counts()[source]#
Return type:

dict[str, int]

property failures: list[VerdictEvent]#
property exit_status: int#

Nonzero if and only if some failure category is non-empty.

summary()[source]#

The one-line count summary a build prints.

“292 pinned, 7 adopted, 1 resolved” tells the reader their edit was absorbed, which a silent success would hide.

Return type:

str

report()[source]#

The full report: the summary, then every failure.

Each failure is a line naming its category and query, followed by indented lines for where the component was constructed, when known, and for each line of its detail.

Return type:

str

class DeferredState(query, source, lock, lock_scope, origin, batch, part=None, resolved=False, binding=False, failure=None)[source]#

Bases: object

Everything one deferred component carries.

Lives on the component rather than in a side table, so nothing has to reach around the proxy to find it.

Parameters:
query: CanonicalQuery#
source: PartSource#
lock: Lock#
lock_scope: str#
origin: str#
batch: Batch#

The batch this query is pending in.

A direct reference rather than an id into a registry. The registry existed only because this was an id, and nothing ever removed from it, so it grew by one entry per design for the life of the process. A reference keeps the batch alive for exactly as long as a state needs it, which is what the registry was emulating.

part: ResolvedPart | None = None#
resolved: bool = False#
binding: bool = False#

_bind sets fields and provider code reads them back while the part is not yet marked resolved.

Type:

Reentrancy guard, and not optional

failure: str | None = None#

Why this query will never resolve, once that is settled.

A flush drains pending, so a state that failed is terminal but otherwise indistinguishable from one that has not been asked yet: part is None and resolved is False in both cases. Recording the reason here is what lets a later attribute read say which it is, and say why, rather than raising the name it was asked for. Set wherever an unresolvable event is recorded, and carrying the same text, so the exception and the report agree.

caller_site(component)[source]#

Where a part was constructed, for diagnostics.

Reads jitx’s own FileInfo, which Sourced.__new__ attaches to every structural object at construction. Walking the stack from here instead found the instantiation trampoline in jitx._structural, because by the time a query is enqueued the design frame is no longer the caller – the same reason jitx captures this at __new__ rather than on demand.

Return type:

str

Parameters:

component (object)

class Run[source]#

Bases: object

State that outlives any one design: the report, and the lockfiles.

A process may elaborate several designs – jitx build A B, or a test suite – and what they share is exactly what is keyed by file rather than by design. Two designs writing one lockfile must hold the same parsed object or their staged writes diverge, and parts_verdict() is asked once for the run however many designs it covered.

What they do not share is on Batch; see Batch.resolution_cache.

verdict: Verdict#
pending_writes: dict[Any, Lockfile]#
lockfiles: dict[str | None, Lockfile]#
class Batch(run)[source]#

Bases: object

One pending set of queries, resolved together. One per design.

Parameters:

run (Run)

pending: list[DeferredState]#
resolution_cache: dict[tuple[str, str], Resolution]#

Answers already had from the chain, keyed (query digest, chain).

Holds Resolution, not a bare part: the answering member has to survive the cache, or a part served from it loses the provenance the lockfile needs.

Per design, and spanning that design’s flushes, which is the lifetime it needs: nothing stages a resolved miss into the lockfile under off, read or check, so without it a second flush could re-ask a question the first already answered and bound, and one run would give two answers to one question. It does nothing within a flush – the key carries the chain digest and _chain_answers is called once per chain group, so a cross-group hit is impossible and the per-call dedup covers the rest.

Not shared between designs. Two boards carry no reproducibility promise to each other beyond the lockfile, and under read-write they agree anyway, because the first design’s answer is staged into the shared in-memory lockfile and the second finds it at the lock lookup.

add(state)[source]#
Return type:

None

Parameters:

state (DeferredState)

on_design_initialized(_event=None)[source]#

Flush this design’s remaining queries, commit the lockfile, and retire.

Runs at Design.Initialized, where the pending set is complete and a recovered pin still reaches the board. Every driver gets the same behaviour, including a bare python design.py.

Return type:

None

Parameters:

_event (Any)

flush(final=False)[source]#

Resolve everything pending, then fire PartsResolved.

Return type:

None

Parameters:

final (bool)

current_batch()[source]#

This thread’s pending batch, creating and arming one if needed.

The end-of-elaboration flush is registered here rather than at module import. Event.on outside an active instantiation returns a PrePostInit wrapper for use in a class body, so a module-level decorator registers nothing; called from _defer, this runs inside instantiation and registers for real, with the frame.

Return type:

Batch

reset()[source]#

Drop all resolver state. For tests and for a fresh run in one process.

Return type:

None

enqueue(component, query, *, source, lock, origin)[source]#

Register a component’s query with the current batch.

Everything the resolution depends on is captured here; nothing is read again at flush time.

Return type:

DeferredState

Parameters:
flush(final=False)[source]#

Resolve everything pending in this thread’s current batch.

Return type:

None

Parameters:

final (bool)

parts_verdict()[source]#

The run’s parts report and exit status.

Called on every exit path, a failed translation included: an unresolvable miss leaves a component unbound, and an unbound component raises in translation, so a driver that asked only on success would report nothing in exactly the cases that most need explaining.

When the verdict contains failures, that report is what the user should see, with the translation exception named as its consequence. When it contains only informational events, the exception stands on its own.

Return type:

Verdict

commit_lock()[source]#

Write pending pins, and return the paths written.

Called automatically once the end-of-elaboration flush has resolved everything, so an ordinary run pins without a driver doing anything. Public and idempotent so a harness that flushed by hand can still commit; it writes nothing when nothing is staged.

Return type:

list[str]

uncommitted()[source]#

How many lockfiles are holding pins that were never written.

Zero after any run that reached the end of elaboration. Non-zero means pins were staged by an early flush and the run stopped before finishing, which is the case where writing would produce a partial lock.

Return type:

int