Skip to content

Write Priority

Every assignment in Kathryn — every update event — carries an integer priority. When several writes target the same register, the emitter sorts them by priority inside the register’s always block, placing the highest-priority write last so that under non-blocking (<=) semantics it dominates. Larger value wins; ties keep program order (the update pool sorts stably), so the last-declared write wins a tie.

The priority applied to an assignment is read at the moment the assignment is built — set the priority before the assignment it should govern.

This is the Decentralized Update abstraction: many independent writers, one register, resolved by declared priority (highest sorted last, wins):

flowchart TB
    W1["writer A (pri=10)"] --> S["sort by priority<br/>(highest last)"]
    W2["writer B (pri=11)"] --> S
    W3["writer C (pri=12)"] --> S
    S --> Q["always block for REG_x<br/>last write dominates"]
    Q --> R["x settles to the<br/>highest-priority value"]

The DEFAULT_UE_PRI_* constants are sourced directly from the Rust host (the authoritative name list is published as kathryn.priority.PRIORITY_CONST_NAMES, so new host constants appear automatically):

constantvaluemeaning
DEFAULT_UE_PRI_MIN0floor of the scale
DEFAULT_UE_PRI_USER10default for user assignments in auto mode
DEFAULT_UE_PRI_INTERNAL_MIN50low end of the band reserved for internal events
DEFAULT_UE_PRI_INTERNAL_MAX100high end of the internal band
DEFAULT_UE_PRI_RST2147483647reset writes — maximum, dominates everything
from kathryn import DEFAULT_UE_PRI_USER, DEFAULT_UE_PRI_RST

Write your own priorities relative to DEFAULT_UE_PRI_USER (e.g. DEFAULT_UE_PRI_USER + 1) so the relationship to ordinary writes stays explicit.

from kathryn import (priority, set_priority, set_priority_auto,
get_priority, get_priority_mode)
calleffect
set_priority(p)pin every subsequent assignment to manual priority p until changed
set_priority_auto()return to auto mode — priority resets to DEFAULT_UE_PRI_USER
get_priority()the value that will be applied to subsequently-built assignments
get_priority_mode()"Auto" or "Manual"
priority(p)context manager: manual p on enter, previous mode/value restored on exit

The context manager is the idiomatic form — it governs exactly the assignments in its body and restores whatever was active before, whether that was auto mode or another manual value:

set_priority_auto()
assert get_priority_mode() == "Auto"
assert get_priority() == DEFAULT_UE_PRI_USER # 10
set_priority(77) # manual from here on
with priority(123):
r |= a # this write is built at priority 123
assert get_priority() == 77 # restored to the manual 77, not to auto

The two par test models tc12/tc13 isolate the rule: three parallel branches assign the same register on the same cycle.

Same priority — program order breaks the tie (tc12)

Section titled “Same priority — program order breaks the tie (tc12)”
SAME_PRI = DEFAULT_UE_PRI_USER + 1
with seq():
with par_auto():
with priority(SAME_PRI):
self.x |= self.val_5 # earliest
with priority(SAME_PRI):
self.x |= self.val_10
with priority(SAME_PRI):
self.x |= self.val_15 # latest → wins on tie

All three fire on the same edge; the stable sort keeps declaration order, so x <= 15 is emitted last and wins. The emitted always block:

REG_x[7:0] <= VAL_val_5[7:0];
REG_x[7:0] <= VAL_val_10[7:0];
REG_x[7:0] <= VAL_val_15[7:0]; // last ⇒ x settles to 15

Different priorities — priority overrides order (tc13)

Section titled “Different priorities — priority overrides order (tc13)”
PRI_LOW, PRI_MID, PRI_HIGH = (DEFAULT_UE_PRI_USER + n for n in (1, 2, 3))
with seq():
with par_auto():
with priority(PRI_HIGH):
self.x |= self.val_5 # declared FIRST, highest → wins
with priority(PRI_LOW):
self.x |= self.val_10 # lowest
with priority(PRI_MID):
self.x |= self.val_15 # declared LAST, but loses

Now the writes are emitted in ascending priority order regardless of where they appear in the source:

REG_x[7:0] <= VAL_val_10[7:0]; // +1 (lowest first)
REG_x[7:0] <= VAL_val_15[7:0]; // +2
REG_x[7:0] <= VAL_val_5[7:0]; // +3 last ⇒ x settles to 5

x settles to 5 even though x <= 15 was declared last — priority, not declaration order, decides.

flowchart LR
    A["val_10 (+1 lowest)"] --> M["priority resolution<br/>(ascending, highest last)"]
    B["val_15 (+2 mid)"] --> M
    C["val_5 (+3 highest)"] --> M
    M --> O["x = val_5"]

The same experiment inside a pipeline stage (tc20/tc21) behaves identically; see Assignment Ordering.

reg.reset(v) binds its write at DEFAULT_UE_PRI_RST — the maximum — so it sorts last in the register’s always block and dominates every user assignment on any cycle where the reset condition holds:

always @(posedge WIRE_clk) begin
if (...) REG_r[7:0] <= WIRE_d[7:0]; // user write (priority 10)
if (WIRE_mrst) REG_r[7:0] <= VAL_0; // reset write (max priority, wins)
end

This is why a held master reset pins an entire pipeline at its reset values no matter what the stages are doing. At the other end of the scale, a wire’s default(v) fallback is bound at a low internal priority so that any real drive overrides it. See Reset & Defaults.