code wiki / (root) / nx_editstack_lib.nx

nx_editstack_lib.nx source

↩ module page · 316 lines · 15701 B

1// nx_editstack_lib.nx -- THE NON-DESTRUCTIVE OPERATION STACK: record, undo, redo, replay, re-evaluate. 2// This is the spine every content-creation tool is built on (Blender's modifier stack, ZBrush's layers, 3// Houdini's graph) and the one thing this estate has never had. /compare/dcc DC1 and DC2. 4// 5// CHECK-BEFORE-BUILD, and the incumbent was READ, not guessed. nx_spendgate named nx_editor_canvas.nx as 6// the nearest candidate and it is NOT this: it is a 2D page-layout canvas ([kind,x,y,w,h] elements -- 7// heading, text, button, image) whose undo is ec_save/ec_undo, a ONE-DEEP FULL-ARRAY SNAPSHOT copied in 8// and out. That cannot redo, cannot replay, cannot re-evaluate a parameter at depth, and costs O(state) 9// per step. nx_siteedit_history.nx is file REVISION history, a different layer again. So the estate's only 10// existing undo is one-deep and snapshot-shaped; this is the log-shaped primitive underneath, and 11// nx_editor_canvas is a candidate consumer of it rather than a rival to it. 12// 13// WHY UNDO IS REPLAY-FROM-BASE AND NOT AN INVERSE OP. Applying an inverse operation is how a float editor 14// loses precision: scale by 3/7 then by 7/3 does not return you to where you started, so undo DRIFTS and 15// the drift is invisible because nobody digests the state. Replaying the log from the base cannot drift, 16// by construction, and it is what makes the bit-exact claim on the /compare/dcc board checkable rather 17// than asserted. The cost is O(cells x ops) per undo, paid deliberately and named here so the next reader 18// does not mistake it for an oversight. 19// 20// THE STATE IS AN i64 CELL VECTOR ON PURPOSE. A consumer maps its own data onto cells (a mesh maps vertex 21// components, the canvas above would map [kind,x,y,w,h]), so there is ONE stack in the estate rather than 22// one per subject. All integer: no float enters, which is why replay is bit-exact. 23// 100% sovereign. No hardware writes (Rule 26). license_tier: ORIGINAL 24import "nx_syscalls.nx" 25 26// ---- arena layout, all i64 words ---- 27// [0..ES_HDR) header 28// [ES_HDR .. +ncells) BASE cells -- the state the log replays from, never mutated by an op 29// [ES_HDR+ncells .. +ncells) LIVE cells -- the current state 30// [ES_HDR+2*ncells ..) op records, ES_REC words each 31const ES_H_CAP: i64 = 0 32const ES_H_NCELL: i64 = 1 33const ES_H_COUNT: i64 = 2 34const ES_H_HEAD: i64 = 3 35const ES_H_ERR: i64 = 4 36const ES_HDR: i64 = 5 37const ES_REC: i64 = 5 // [kind, a0, a1, a2, a3] -- one word per field, each named below 38const ES_I64_BYTES: i64 = 8 39 40// OP RECORD FIELD OFFSETS, NAMED ONCE AND USED EVERYWHERE. The layout above was declared in a comment and 41// then hand-counted at every read and write of it -- in this file, and in consumers that call es_op_field 42// with a bare offset. A hand-written offset beside a record layout is a second copy of that layout's shape, 43// and the two drift silently: widen or reorder the record and every bare offset still compiles and still 44// reads a word, just the wrong one, with no diagnostic anywhere. These names are the single source of truth 45// for the layout inside this organ and outside it, and ES_REC above is exactly ES_F_A3 + 1. 46const ES_F_KIND: i64 = 0 47const ES_F_A0: i64 = 1 48const ES_F_A1: i64 = 2 49const ES_F_A2: i64 = 3 50const ES_F_A3: i64 = 4 51 52// EVERY REFUSAL IS NAMED. A guard that returns a bare -1 for four different causes sends every reader at 53// the wrong one; es_err_name below turns each into a token a gate can assert on. 54const ES_OK: i64 = 0 55const ES_E_CELL: i64 = 0 - 1 56const ES_E_FULL: i64 = 0 - 2 57const ES_E_ATBASE: i64 = 0 - 3 58const ES_E_ATHEAD: i64 = 0 - 4 59const ES_E_DIV: i64 = 0 - 5 60const ES_E_KIND: i64 = 0 - 6 61const ES_E_IDX: i64 = 0 - 7 62const ES_E_RANGE: i64 = 0 - 8 63const ES_E_ARGS: i64 = 0 - 9 64const ES_E_RESERVED: i64 = 0 - 10 65 66const ES_OP_SET: i64 = 1 67const ES_OP_ADD: i64 = 2 68const ES_OP_SCALE: i64 = 3 // rational scale: cell = cell * a1 / a2, integer, a2 == 0 REFUSES 69const ES_OP_RADD: i64 = 4 // range add over [a0, a1) 70 71// THE CONSUMER OP RANGE. A consumer whose gestures are not expressible as SET/ADD/SCALE/RADD (a canvas 72// align, a boolean, a modifier) records its OWN op kind here and drives its own replay -- see es_push_raw, 73// es_restore_base and es_head_set below. The range starts high and es_push_raw REFUSES anything below it, 74// so a consumer kind can never collide with a generic one and es_apply can never silently interpret a 75// record it does not understand. Reserving the range is what makes the escape hatch safe rather than a 76// second vocabulary nobody can audit. 77const ES_OP_CONSUMER_BASE: i64 = 1024 78 79// THE DIGEST IS A CHANGE DETECTOR, NOT A CRYPTOGRAPHIC HASH, and it is labelled so no caller mistakes it 80// for one. Multiply-accumulate fold over the live cells using the PUBLISHED FNV-1a 64-bit prime as the 81// multiplier; the cell index is mixed in so a permutation of the same values does not collide. What a gate 82// needs of it is that different states differ -- which is itself asserted as a tooth rather than assumed. 83const ES_HASH_MUL: i64 = 1099511628211 84 85func es_live_off(st: *i64) -> i64 { return ES_HDR + st[ES_H_NCELL] } 86func es_ops_off(st: *i64) -> i64 { return ES_HDR + st[ES_H_NCELL] + st[ES_H_NCELL] } 87 88func es_ncells(st: *i64) -> i64 { return st[ES_H_NCELL] } 89func es_cap(st: *i64) -> i64 { return st[ES_H_CAP] } 90func es_count(st: *i64) -> i64 { return st[ES_H_COUNT] } 91func es_head(st: *i64) -> i64 { return st[ES_H_HEAD] } 92func es_err(st: *i64) -> i64 { return st[ES_H_ERR] } 93 94func es_err_name(e: i64) -> *u8 { 95 if e == ES_OK { return "OK" as *u8 } 96 if e == ES_E_CELL { return "REFUSED-CELL-OUT-OF-RANGE" as *u8 } 97 if e == ES_E_FULL { return "REFUSED-OP-CAP-REACHED" as *u8 } 98 if e == ES_E_ATBASE { return "REFUSED-ALREADY-AT-BASE" as *u8 } 99 if e == ES_E_ATHEAD { return "REFUSED-ALREADY-AT-HEAD" as *u8 } 100 if e == ES_E_DIV { return "REFUSED-SCALE-DENOMINATOR-ZERO" as *u8 } 101 if e == ES_E_KIND { return "REFUSED-UNKNOWN-OP-KIND" as *u8 } 102 if e == ES_E_IDX { return "REFUSED-OP-INDEX-OUT-OF-RANGE" as *u8 } 103 if e == ES_E_RANGE { return "REFUSED-CELL-RANGE-INVALID" as *u8 } 104 if e == ES_E_ARGS { return "REFUSED-BAD-ARGUMENTS" as *u8 } 105 if e == ES_E_RESERVED { return "REFUSED-KIND-BELOW-CONSUMER-RANGE" as *u8 } 106 return "REFUSED-UNCLASSIFIED" as *u8 107} 108 109func es_words_for(cap_ops: i64, ncells: i64) -> i64 { 110 return ES_HDR + ncells + ncells + cap_ops * ES_REC 111} 112 113// Returns 0 (a null pointer) on bad arguments rather than allocating something unusable. 114func es_new(cap_ops: i64, ncells: i64) -> *i64 { 115 if cap_ops <= 0 { return 0 as *i64 } 116 if ncells <= 0 { return 0 as *i64 } 117 let st: *i64 = sys_mmap(es_words_for(cap_ops, ncells) * ES_I64_BYTES) as *i64 118 st[ES_H_CAP] = cap_ops 119 st[ES_H_NCELL] = ncells 120 st[ES_H_COUNT] = 0 121 st[ES_H_HEAD] = 0 122 st[ES_H_ERR] = ES_OK 123 return st 124} 125 126// Seeds BOTH the base and the live cell, because the base is what replay restores to. 127func es_base_set(st: *i64, i: i64, v: i64) -> i64 { 128 if i < 0 { st[ES_H_ERR] = ES_E_CELL; return ES_E_CELL } 129 if i >= st[ES_H_NCELL] { st[ES_H_ERR] = ES_E_CELL; return ES_E_CELL } 130 st[ES_HDR + i] = v 131 st[es_live_off(st) + i] = v 132 return ES_OK 133} 134 135// Out of range sets the error and returns 0; a caller that cares reads es_err. Documented rather than 136// silently clamped, because a clamp is how an out-of-range read becomes a plausible wrong answer. 137func es_cell(st: *i64, i: i64) -> i64 { 138 if i < 0 { st[ES_H_ERR] = ES_E_CELL; return 0 } 139 if i >= st[ES_H_NCELL] { st[ES_H_ERR] = ES_E_CELL; return 0 } 140 return st[es_live_off(st) + i] 141} 142 143func es_op_field(st: *i64, idx: i64, f: i64) -> i64 { 144 if idx < 0 { st[ES_H_ERR] = ES_E_IDX; return 0 } 145 if idx >= st[ES_H_COUNT] { st[ES_H_ERR] = ES_E_IDX; return 0 } 146 if f < 0 { st[ES_H_ERR] = ES_E_ARGS; return 0 } 147 if f >= ES_REC { st[ES_H_ERR] = ES_E_ARGS; return 0 } 148 return st[es_ops_off(st) + idx * ES_REC + f] 149} 150 151// THE INTERPRETER. Every bound is checked BEFORE any cell is written, so a refused op leaves the state 152// exactly as it found it -- which is what lets the gate assert "refused AND digest unchanged" instead of 153// merely "refused". 154func es_apply(st: *i64, kind: i64, a0: i64, a1: i64, a2: i64, a3: i64) -> i64 { 155 let n: i64 = st[ES_H_NCELL] 156 let lv: i64 = es_live_off(st) 157 if kind == ES_OP_SET { 158 if a0 < 0 { return ES_E_CELL } 159 if a0 >= n { return ES_E_CELL } 160 st[lv + a0] = a1 161 return ES_OK 162 } 163 if kind == ES_OP_ADD { 164 if a0 < 0 { return ES_E_CELL } 165 if a0 >= n { return ES_E_CELL } 166 st[lv + a0] = st[lv + a0] + a1 167 return ES_OK 168 } 169 if kind == ES_OP_SCALE { 170 if a0 < 0 { return ES_E_CELL } 171 if a0 >= n { return ES_E_CELL } 172 if a2 == 0 { return ES_E_DIV } 173 st[lv + a0] = st[lv + a0] * a1 / a2 174 return ES_OK 175 } 176 if kind == ES_OP_RADD { 177 if a0 < 0 { return ES_E_CELL } 178 if a1 < a0 { return ES_E_RANGE } 179 if a1 > n { return ES_E_RANGE } 180 var i: i64 = a0 181 while i < a1 { st[lv + i] = st[lv + i] + a2; i = i + 1 } 182 return ES_OK 183 } 184 return ES_E_KIND 185} 186 187// THE BASE->LIVE COPIER, EXTRACTED SO THERE IS EXACTLY ONE OF IT. es_replay used to open with this loop 188// inline. A consumer driving its own replay (see es_push_raw) needs the same first step, and re-typing it 189// there would be a second copier free to drift from this one -- a bug you fix by rewriting the line rather 190// than by extracting the fix is a bug you write again. 191func es_restore_base(st: *i64) -> i64 { 192 let n: i64 = st[ES_H_NCELL] 193 let lv: i64 = es_live_off(st) 194 var i: i64 = 0 195 while i < n { st[lv + i] = st[ES_HDR + i]; i = i + 1 } 196 return ES_OK 197} 198 199// A DIRECT POINTER AT THE LIVE CELL RUN. A consumer whose own state layout IS the cell vector can then hand 200// its existing functions the stack's live cells and operate on them in place -- no shadow copy to keep in 201// sync, and es_digest therefore always describes exactly the state the consumer is showing. Byte arithmetic 202// because the arena is one allocation and the live run starts es_live_off WORDS into it. 203func es_live_ptr(st: *i64) -> *i64 { 204 return ((st as i64) + es_live_off(st) * ES_I64_BYTES) as *i64 205} 206 207// Restore the live cells from the base, then re-apply ops [0, head). 208func es_replay(st: *i64) -> i64 { 209 es_restore_base(st) 210 let h: i64 = st[ES_H_HEAD] 211 let ob: i64 = es_ops_off(st) 212 var k: i64 = 0 213 while k < h { 214 let o: i64 = ob + k * ES_REC 215 let rc: i64 = es_apply(st, st[o + ES_F_KIND], st[o + ES_F_A0], st[o + ES_F_A1], st[o + ES_F_A2], st[o + ES_F_A3]) 216 if rc != ES_OK { st[ES_H_ERR] = rc; return rc } 217 k = k + 1 218 } 219 return ES_OK 220} 221 222// THE CAP IS CHECKED BEFORE THE OP IS APPLIED. The first draft of this function applied first and checked 223// after, which would have mutated the state on a refused push -- the exact shape of defect the refusal is 224// there to prevent. A full stack REFUSES by name and never silently drops the op. 225// A push after an undo truncates the redo tail (standard editor semantics); count follows head so the 226// truncation is visible in the counters rather than implied. 227// THE RECORDER, EXTRACTED. The redo-tail truncation (count follows head) is the one rule that BOTH push 228// paths have to obey, and two organs that must agree should be made unable to disagree rather than asked 229// to remember: there is exactly one copy of it, here, and every push goes through it. 230func es_record(st: *i64, kind: i64, a0: i64, a1: i64, a2: i64, a3: i64) -> i64 { 231 let h: i64 = st[ES_H_HEAD] 232 if h >= st[ES_H_CAP] { st[ES_H_ERR] = ES_E_FULL; return ES_E_FULL } 233 let o: i64 = es_ops_off(st) + h * ES_REC 234 st[o + ES_F_KIND] = kind 235 st[o + ES_F_A0] = a0 236 st[o + ES_F_A1] = a1 237 st[o + ES_F_A2] = a2 238 st[o + ES_F_A3] = a3 239 st[ES_H_HEAD] = h + 1 240 st[ES_H_COUNT] = h + 1 241 return ES_OK 242} 243 244func es_push(st: *i64, kind: i64, a0: i64, a1: i64, a2: i64, a3: i64) -> i64 { 245 // The cap is re-checked here BEFORE es_apply even though es_record checks it too. That is deliberate 246 // and it is the whole point of the original comment above: without this line a full stack would have 247 // MUTATED the live cells and only then refused. es_record's check remains the authority on recording. 248 if st[ES_H_HEAD] >= st[ES_H_CAP] { st[ES_H_ERR] = ES_E_FULL; return ES_E_FULL } 249 let rc: i64 = es_apply(st, kind, a0, a1, a2, a3) 250 if rc != ES_OK { st[ES_H_ERR] = rc; return rc } 251 return es_record(st, kind, a0, a1, a2, a3) 252} 253 254// RECORD WITHOUT INTERPRETING -- the consumer-driven half of the stack. The caller has already applied the 255// gesture to the live cells with its OWN interpreter (es_live_ptr hands it those cells) and is asking only 256// that the gesture be logged, so that undo, redo, replay and re-evaluation still come from one place. 257// A kind below ES_OP_CONSUMER_BASE is REFUSED BY NAME: those belong to es_apply, and a log that mixed the 258// two vocabularies could be replayed by neither interpreter with any honesty. 259func es_push_raw(st: *i64, kind: i64, a0: i64, a1: i64, a2: i64, a3: i64) -> i64 { 260 if kind < ES_OP_CONSUMER_BASE { st[ES_H_ERR] = ES_E_RESERVED; return ES_E_RESERVED } 261 return es_record(st, kind, a0, a1, a2, a3) 262} 263 264// MOVE THE CURSOR, BOUNDED. A consumer implementing undo/redo over its own vocabulary needs to step head 265// and then run its own replay; it must not reach into ES_H_HEAD itself, or the bound lives in as many 266// places as there are consumers. Out of range REFUSES and leaves the cursor exactly where it was. 267func es_head_set(st: *i64, h: i64) -> i64 { 268 if h < 0 { st[ES_H_ERR] = ES_E_IDX; return ES_E_IDX } 269 if h > st[ES_H_COUNT] { st[ES_H_ERR] = ES_E_IDX; return ES_E_IDX } 270 st[ES_H_HEAD] = h 271 return ES_OK 272} 273 274func es_undo(st: *i64) -> i64 { 275 if st[ES_H_HEAD] <= 0 { st[ES_H_ERR] = ES_E_ATBASE; return ES_E_ATBASE } 276 st[ES_H_HEAD] = st[ES_H_HEAD] - 1 277 return es_replay(st) 278} 279 280func es_redo(st: *i64) -> i64 { 281 if st[ES_H_HEAD] >= st[ES_H_COUNT] { st[ES_H_ERR] = ES_E_ATHEAD; return ES_E_ATHEAD } 282 st[ES_H_HEAD] = st[ES_H_HEAD] + 1 283 return es_replay(st) 284} 285 286// DC2, THE NON-DESTRUCTIVE HALF: change the parameters of operation idx and re-evaluate. Operations after 287// idx are PRESERVED and re-applied on top of the new value -- that is the whole property, and it is why an 288// editor built on this does not lose work when a parameter is corrected. 289// THE ARGUMENT WRITER, EXTRACTED. es_reeval used to write these four words inline; a consumer re-evaluating 290// an op in its OWN vocabulary needs the identical write followed by its own replay, and es_op_field is a 291// reader only. One writer, so the index bound cannot be right here and missing there. 292func es_op_args_set(st: *i64, idx: i64, a0: i64, a1: i64, a2: i64, a3: i64) -> i64 { 293 if idx < 0 { st[ES_H_ERR] = ES_E_IDX; return ES_E_IDX } 294 if idx >= st[ES_H_COUNT] { st[ES_H_ERR] = ES_E_IDX; return ES_E_IDX } 295 let o: i64 = es_ops_off(st) + idx * ES_REC 296 st[o + ES_F_A0] = a0 297 st[o + ES_F_A1] = a1 298 st[o + ES_F_A2] = a2 299 st[o + ES_F_A3] = a3 300 return ES_OK 301} 302 303func es_reeval(st: *i64, idx: i64, a0: i64, a1: i64, a2: i64, a3: i64) -> i64 { 304 let rc: i64 = es_op_args_set(st, idx, a0, a1, a2, a3) 305 if rc != ES_OK { return rc } 306 return es_replay(st) 307} 308 309func es_digest(st: *i64) -> i64 { 310 let n: i64 = st[ES_H_NCELL] 311 let lv: i64 = es_live_off(st) 312 var h: i64 = 0 313 var i: i64 = 0 314 while i < n { h = h * ES_HASH_MUL + st[lv + i] + i + 1; i = i + 1 } 315 return h 316}