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 exported constants
Section titled “The exported constants”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):
| constant | value | meaning |
|---|---|---|
DEFAULT_UE_PRI_MIN | 0 | floor of the scale |
DEFAULT_UE_PRI_USER | 10 | default for user assignments in auto mode |
DEFAULT_UE_PRI_INTERNAL_MIN | 50 | low end of the band reserved for internal events |
DEFAULT_UE_PRI_INTERNAL_MAX | 100 | high end of the internal band |
DEFAULT_UE_PRI_RST | 2147483647 | reset writes — maximum, dominates everything |
from kathryn import DEFAULT_UE_PRI_USER, DEFAULT_UE_PRI_RSTWrite your own priorities relative to DEFAULT_UE_PRI_USER
(e.g. DEFAULT_UE_PRI_USER + 1) so the relationship to ordinary writes stays
explicit.
Setting the priority
Section titled “Setting the priority”from kathryn import (priority, set_priority, set_priority_auto, get_priority, get_priority_mode)| call | effect |
|---|---|
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 onwith priority(123): r |= a # this write is built at priority 123assert get_priority() == 77 # restored to the manual 77, not to autoSame-register conflicts, resolved
Section titled “Same-register conflicts, resolved”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 tieAll 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 15Different 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 losesNow 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]; // +2REG_x[7:0] <= VAL_val_5[7:0]; // +3 last ⇒ x settles to 5x 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.
Reset dominance
Section titled “Reset dominance”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)endThis 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.