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}