change_tick.bend

bendvy-ecs-core@0.1.0.3 · 90 lines · 17 definitions

1import Base
2
3# Logical change timestamp. low wraps modulo 2^32; epoch preserves the history
4# needed to age dormant components/readers across low wraps within runtime limits.
5# Raw constructors are trusted: execution must not supply future stamps/cursors.
6type Tick is Data:
7  Tick{epoch:Nat,low:U32}
8
9# Pinned Bevy ad678262 change_detection/mod.rs MAX_CHANGE_AGE.
10# Exact epochs replace periodic storage sweeps, not this observable age bound.
11def max_change_age() -> U32:3258167296
12
13# Initial administrative timestamp. Zero is valid, not component absence or a
14# fabricated first-run cursor; first-run reader policy is supplied separately.
15def zero() -> Tick:Tick{0n,0}
16
17def incremented(wrapped:Bool,epoch:Nat,low:U32) -> Tick:
18  match wrapped:
19    case False{}:Tick{epoch,U32.add(low,1)}
20    case True{}:Tick{1n+epoch,0}
21
22# Advance one logical execution boundary, carrying the epoch on low-word wrap.
23def next(tick:Tick) -> Tick:
24  match tick:
25    case Tick{epoch,+low}:incremented(U32.is_eq(low,4294967295),epoch,low)
26
27# Diagnostic low word only; never use this projection as an independent cursor.
28def low(tick:Tick) -> U32:
29  match tick:
30    case Tick{_,value}:value
31
32# Monotone logical ordinal for existing Nat-based observation/runtime domains.
33# Backend Nat representation limits still apply to consumers of this projection.
34def total(tick:Tick) -> Nat:
35  match tick:
36    case Tick{epoch,value}:Nat.add(Nat.mul(epoch,1n+4294967295n),U32.to_nat(value))
37
38def ordered_epochs(equal:Bool,earlier:Bool,stamp:U32,this:U32) -> Bool:
39  match equal:
40    case True{}:U32.is_le(stamp,this)
41    case False{}:earlier
42
43# Explicit validity of a timestamp relative to this execution. Future values
44# are not made recent by saturating Nat subtraction.
45def not_future(stamp:Tick,this:Tick) -> Bool:
46  match stamp this:
47    case Tick{+se,sl} Tick{+te,tl}:ordered_epochs(Nat.is_eq(se,te),Nat.is_lt(se,te),sl,tl)
48
49def crossing_age(lowWrapped:Bool,stamp:U32,this:U32) -> U32:
50  match lowWrapped:
51    case False{}:max_change_age()
52    case True{}:U32.min(U32.sub(this,stamp),max_change_age())
53
54def distance(same:Bool,adjacent:Bool,+stamp:U32,+this:U32) -> U32:
55  match same adjacent:
56    case True{} _:U32.min(U32.sub(this,stamp),max_change_age())
57    case False{} True{}:crossing_age(U32.is_lt(this,stamp),stamp,this)
58    case False{} False{}:max_change_age()
59
60def valid_age(valid:Bool,stamp:Tick,this:Tick) -> U32:
61  match valid stamp this:
62    case False{} _ _:max_change_age()
63    case True{} Tick{+se,sl} Tick{+te,tl}:distance(Nat.is_eq(se,te),Nat.is_eq(1n+se,te),sl,tl)
64
65# True elapsed age capped at Bevy's MAX_CHANGE_AGE, including multiple wraps.
66# Invalid future stamps return MAX; use not_future when distinguishing invalid
67# inputs from legitimately old data. No global storage maintenance is required.
68def age(+stamp:Tick,+this:Tick) -> U32:
69  valid_age(not_future(stamp,this),stamp,this)
70
71def valid_newer(valid:Bool,stamp:Tick,last:Tick,+this:Tick) -> Bool:
72  match valid:
73    case False{}:False{}
74    case True{}:U32.is_gt(age(last,this),age(stamp,this))
75
76# Compare component age with independent reader age. Equality and two maximally
77# old timestamps are not new. A future stamp OR reader cursor rejects as False;
78# callers separately establish component presence and first-run reader policy.
79def newer(+stamp:Tick,+last:Tick,+this:Tick) -> Bool:
80  valid_newer(Bool.and(not_future(stamp,this),not_future(last,this)),stamp,last,this)
81
82# Detached full-timestamp diagnostic; the epoch is never discarded.
83def show(tick:Tick) -> String:
84  match tick:
85    case Tick{epoch,low}:Nat.show(epoch) ++ ":" ++ U32.show(low)
86# Distinguish an uninitialized lifecycle cursor from the legitimate zero tick.
87def cursor_show(cursor:Maybe<&2,Tick>) -> String:
88  match cursor:
89    case None{}:"never"
90    case Some{tick}:show(tick)