@pilates/core 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/algorithm/index.d.ts +31 -0
- package/dist/algorithm/index.d.ts.map +1 -1
- package/dist/algorithm/index.js +85 -1
- package/dist/algorithm/index.js.map +1 -1
- package/dist/algorithm/round.d.ts +13 -0
- package/dist/algorithm/round.d.ts.map +1 -1
- package/dist/algorithm/round.js +17 -0
- package/dist/algorithm/round.js.map +1 -1
- package/dist/algorithm/spineless/flex-grammar.d.ts +394 -0
- package/dist/algorithm/spineless/flex-grammar.d.ts.map +1 -0
- package/dist/algorithm/spineless/flex-grammar.js +2373 -0
- package/dist/algorithm/spineless/flex-grammar.js.map +1 -0
- package/dist/algorithm/spineless/grammar.d.ts +150 -0
- package/dist/algorithm/spineless/grammar.d.ts.map +1 -0
- package/dist/algorithm/spineless/grammar.js +144 -0
- package/dist/algorithm/spineless/grammar.js.map +1 -0
- package/dist/algorithm/spineless/layout.d.ts +130 -0
- package/dist/algorithm/spineless/layout.d.ts.map +1 -0
- package/dist/algorithm/spineless/layout.js +755 -0
- package/dist/algorithm/spineless/layout.js.map +1 -0
- package/dist/algorithm/spineless/order-maintenance.bench.d.ts +25 -0
- package/dist/algorithm/spineless/order-maintenance.bench.d.ts.map +1 -0
- package/dist/algorithm/spineless/order-maintenance.bench.js +78 -0
- package/dist/algorithm/spineless/order-maintenance.bench.js.map +1 -0
- package/dist/algorithm/spineless/order-maintenance.d.ts +192 -0
- package/dist/algorithm/spineless/order-maintenance.d.ts.map +1 -0
- package/dist/algorithm/spineless/order-maintenance.js +294 -0
- package/dist/algorithm/spineless/order-maintenance.js.map +1 -0
- package/dist/algorithm/spineless/priority-queue.bench.d.ts +17 -0
- package/dist/algorithm/spineless/priority-queue.bench.d.ts.map +1 -0
- package/dist/algorithm/spineless/priority-queue.bench.js +57 -0
- package/dist/algorithm/spineless/priority-queue.bench.js.map +1 -0
- package/dist/algorithm/spineless/priority-queue.d.ts +73 -0
- package/dist/algorithm/spineless/priority-queue.d.ts.map +1 -0
- package/dist/algorithm/spineless/priority-queue.js +149 -0
- package/dist/algorithm/spineless/priority-queue.js.map +1 -0
- package/dist/algorithm/spineless/runtime.d.ts +239 -0
- package/dist/algorithm/spineless/runtime.d.ts.map +1 -0
- package/dist/algorithm/spineless/runtime.js +458 -0
- package/dist/algorithm/spineless/runtime.js.map +1 -0
- package/dist/algorithm/spineless/style-dirty.d.ts +65 -0
- package/dist/algorithm/spineless/style-dirty.d.ts.map +1 -0
- package/dist/algorithm/spineless/style-dirty.js +75 -0
- package/dist/algorithm/spineless/style-dirty.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +27 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +61 -0
- package/dist/inspect.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spineless Traversal runtime — incremental driver for an attribute
|
|
3
|
+
* grammar (Kirisame, Wang, Panchekha — PLDI 2025).
|
|
4
|
+
*
|
|
5
|
+
* Combines the three foundational primitives:
|
|
6
|
+
* - `BenderOrderMaintenance` (OM) — assigns a stable, totally-
|
|
7
|
+
* ordered timestamp to every field at first computation. Topo
|
|
8
|
+
* order at init time => OM order forever, even after relabel.
|
|
9
|
+
* - `OmPriorityQueue<Field>` — keyed on the field's OM node.
|
|
10
|
+
* Popping the minimum returns the next field to recompute in
|
|
11
|
+
* topological order, without any explicit DAG walks.
|
|
12
|
+
* - `Grammar` — declarative `field -> rule(deps, compute)` map.
|
|
13
|
+
* The runtime queries it on every recompute.
|
|
14
|
+
*
|
|
15
|
+
* ## Phases
|
|
16
|
+
*
|
|
17
|
+
* **Init** (`init()`): one full pass over the grammar from the
|
|
18
|
+
* supplied root fields. DFS in topological order; for each field,
|
|
19
|
+
* allocate an OM node (`om.insertAfter(prev)`), record the
|
|
20
|
+
* reverse-deps edges so dependents can be scheduled later, then run
|
|
21
|
+
* the field's compute and cache the value. After init, every
|
|
22
|
+
* reachable field has a (timestamp, value, dependents-list) triple
|
|
23
|
+
* in the runtime's storage.
|
|
24
|
+
*
|
|
25
|
+
* **Recompute** (`markDirty(field)` + `recompute()`): callers mark
|
|
26
|
+
* the fields whose inputs changed. Each dirty field is enqueued
|
|
27
|
+
* (`pq.push(field, omNode)`). `recompute()` loops: pop the
|
|
28
|
+
* OM-minimum field, re-run its rule. If the new value differs from
|
|
29
|
+
* the cached one, persist it and push every dependent. Process
|
|
30
|
+
* stops when the queue is empty.
|
|
31
|
+
*
|
|
32
|
+
* **Termination** rests on the dependency graph being acyclic: each
|
|
33
|
+
* field's value is a function of finitely many others, so the
|
|
34
|
+
* worklist reaches the DAG's unique fixpoint in finitely many
|
|
35
|
+
* steps. When the OM order is a true topological order — as it is
|
|
36
|
+
* after `init` and pure-additive `graft` — every field recomputes
|
|
37
|
+
* exactly once (a dependent always pops after its deps), giving the
|
|
38
|
+
* O(affected) Spineless bound. After a `rebindRule` an existing
|
|
39
|
+
* field may gain a dependency on a later-OM field; recompute stays
|
|
40
|
+
* correct and terminating, but such a field may recompute a bounded
|
|
41
|
+
* number of extra times. OM order is thus a performance property,
|
|
42
|
+
* not a correctness one.
|
|
43
|
+
*
|
|
44
|
+
* **Value preservation under no-op recompute:** if a field's rule
|
|
45
|
+
* produces the same value as before, its dependents are NOT
|
|
46
|
+
* scheduled. This is the key "skip work" property of Spineless.
|
|
47
|
+
*
|
|
48
|
+
* **Graft** (`graft(additions, newRoots)`): incremental structural
|
|
49
|
+
* growth (phase 5c). New fields whose topological position is at the
|
|
50
|
+
* tail — they may read existing fields, but no existing field reads
|
|
51
|
+
* them and no existing rule changes — are spliced in without a
|
|
52
|
+
* rebuild: each gets an OM node appended after the current tail, its
|
|
53
|
+
* reverse-dependency edges recorded, and its value computed once.
|
|
54
|
+
* This is exactly the shape of appending a child to a parent in the
|
|
55
|
+
* "simple" regime.
|
|
56
|
+
*
|
|
57
|
+
* **Detach** (`detach(fields)`): the inverse of `graft` — drops a
|
|
58
|
+
* removed subtree's fields, freeing their OM nodes and pruning the
|
|
59
|
+
* reverse-dependency edges into surviving fields. Valid when the
|
|
60
|
+
* removed set is closed under "is read by" (nothing outside reads
|
|
61
|
+
* in), which holds for removing a last child in the simple regime.
|
|
62
|
+
* Surviving leaf-input fields the removed set was the sole reader of
|
|
63
|
+
* are dropped too — the caller passes only the subtree's own fields.
|
|
64
|
+
*
|
|
65
|
+
* **Rebind** (`rebindRule(field, newRule)`): replace an existing
|
|
66
|
+
* field's rule — used when a structural change rewrites a surviving
|
|
67
|
+
* field (e.g. appending into a flex-distributing parent grows every
|
|
68
|
+
* sibling's flex-distribution dependency set). The reverse-
|
|
69
|
+
* dependency edges are updated to the new dep set and the field is
|
|
70
|
+
* marked dirty. New deps may have a later OM than the field — see
|
|
71
|
+
* the termination note above.
|
|
72
|
+
*
|
|
73
|
+
* ## What this runtime does NOT cover
|
|
74
|
+
*
|
|
75
|
+
* - Regime-changing structural mutation. `graft` only adds pure
|
|
76
|
+
* topological-tail fields; appending into a flex-distributing /
|
|
77
|
+
* justified / wrapping parent also rewrites existing siblings'
|
|
78
|
+
* rules, and removal / direction flips re-key subtrees — later
|
|
79
|
+
* phase-5c slices.
|
|
80
|
+
* - Differential mode against the imperative algorithm. The
|
|
81
|
+
* correctness oracle for the runtime is the `TopoInterpreter`
|
|
82
|
+
* running over the same grammar — once both agree, the grammar's
|
|
83
|
+
* existing differential coverage carries through.
|
|
84
|
+
*
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
import { BenderOrderMaintenance } from './order-maintenance.js';
|
|
88
|
+
import { OmPriorityQueue } from './priority-queue.js';
|
|
89
|
+
/**
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
export class SpinelessRuntime {
|
|
93
|
+
grammar;
|
|
94
|
+
rootFields;
|
|
95
|
+
om;
|
|
96
|
+
pq;
|
|
97
|
+
/** field -> cached value */
|
|
98
|
+
values = new Map();
|
|
99
|
+
/** field -> its OM timestamp (allocated in topo order at init) */
|
|
100
|
+
omNodes = new Map();
|
|
101
|
+
/** field -> fields that read this field (reverse of `rule.deps`) */
|
|
102
|
+
dependents = new Map();
|
|
103
|
+
/** The OM node at the topological tail — where `graft` appends. */
|
|
104
|
+
lastOm = null;
|
|
105
|
+
initDone = false;
|
|
106
|
+
/**
|
|
107
|
+
* Recompute counters — for observability (phase 9). Plain integer
|
|
108
|
+
* fields, bumped on the existing `integrate` / `recompute` loops,
|
|
109
|
+
* so they cost nothing measurable and need no enable flag.
|
|
110
|
+
*/
|
|
111
|
+
stats = {
|
|
112
|
+
/** Fields integrated so far — the `init` pass plus every `graft`. */
|
|
113
|
+
initFields: 0,
|
|
114
|
+
/** Fields popped from the PQ by the most recent `recompute()`. */
|
|
115
|
+
recomputeVisited: 0,
|
|
116
|
+
/** Of those, Fields whose value actually changed. */
|
|
117
|
+
recomputeChanged: 0,
|
|
118
|
+
/** Cumulative Fields visited across every `recompute()` since init. */
|
|
119
|
+
totalVisited: 0,
|
|
120
|
+
};
|
|
121
|
+
constructor(grammar, rootFields, om = new BenderOrderMaintenance()) {
|
|
122
|
+
this.grammar = grammar;
|
|
123
|
+
this.rootFields = rootFields;
|
|
124
|
+
this.om = om;
|
|
125
|
+
this.pq = new OmPriorityQueue(om);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Walk the grammar in topological order, allocate an OM node per
|
|
129
|
+
* field, cache initial values, and record reverse-dependents. Must
|
|
130
|
+
* be called once before any `evaluate` / `markDirty` / `recompute`.
|
|
131
|
+
*/
|
|
132
|
+
init() {
|
|
133
|
+
this.integrate(this.rootFields);
|
|
134
|
+
this.initDone = true;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Integrate new fields into an already-`init`ed runtime without a
|
|
138
|
+
* rebuild (phase 5c). `additions` holds the rules for the new
|
|
139
|
+
* fields only — it throws if any field is already present, since
|
|
140
|
+
* redefining an existing field is a rule *change*, not a graft.
|
|
141
|
+
* `newRoots` are the new fields to start the topological DFS from
|
|
142
|
+
* (their existing-field dependencies are reached as boundaries).
|
|
143
|
+
*
|
|
144
|
+
* Correct **iff** the new fields are pure topological-tail
|
|
145
|
+
* additions: no existing field reads a new field, and no existing
|
|
146
|
+
* rule needed to change. The caller guarantees this — it holds by
|
|
147
|
+
* construction when appending a last child to a parent in the
|
|
148
|
+
* simple regime (no flex distribution, default `justify`, no
|
|
149
|
+
* wrap). The new fields are computed with correct inputs during
|
|
150
|
+
* the graft, so no `markDirty` / `recompute` is needed afterward.
|
|
151
|
+
*/
|
|
152
|
+
graft(additions, newRoots) {
|
|
153
|
+
if (!this.initDone) {
|
|
154
|
+
throw new Error('[spineless-runtime] graft called before init()');
|
|
155
|
+
}
|
|
156
|
+
for (const [f, rule] of additions) {
|
|
157
|
+
if (this.omNodes.has(f)) {
|
|
158
|
+
throw new Error(`[spineless-runtime] graft: field "${f.name}" already exists — graft integrates NEW fields only`);
|
|
159
|
+
}
|
|
160
|
+
this.grammar.set(f, rule);
|
|
161
|
+
}
|
|
162
|
+
this.integrate(newRoots);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Remove fields from an already-`init`ed runtime without a rebuild
|
|
166
|
+
* (phase 5c) — the inverse of `graft`. `fields` is the exact set
|
|
167
|
+
* to drop (a removed subtree's fields). For each: its OM node is
|
|
168
|
+
* freed, its cached value and reverse-dependency list dropped, its
|
|
169
|
+
* rule deleted from the grammar, and it is pruned from the
|
|
170
|
+
* reverse-dependency list of every field it read.
|
|
171
|
+
*
|
|
172
|
+
* Throws if any removed field still has a dependent *outside* the
|
|
173
|
+
* removed set — detaching it would dangle that edge. The caller
|
|
174
|
+
* guarantees a clean cut: it holds by construction when removing a
|
|
175
|
+
* last child from a parent in the simple regime (nothing outside
|
|
176
|
+
* that subtree reads into it). No `recompute` is needed afterward —
|
|
177
|
+
* removing a topological-tail subtree changes no surviving field.
|
|
178
|
+
*
|
|
179
|
+
* After the cut, any surviving leaf-input field the removed set was
|
|
180
|
+
* the sole reader of is **orphaned** — nothing reads it and the
|
|
181
|
+
* grammar cannot re-read it without a fresh build. Such orphans are
|
|
182
|
+
* dropped too (a leaf has no dependencies, so this cannot cascade).
|
|
183
|
+
* That makes the caller's removed set just the subtree's own
|
|
184
|
+
* fields — orphan input fields, e.g. the previous last child's
|
|
185
|
+
* now-unread main-end margin, need not be enumerated.
|
|
186
|
+
*/
|
|
187
|
+
detach(fields) {
|
|
188
|
+
if (!this.initDone) {
|
|
189
|
+
throw new Error('[spineless-runtime] detach called before init()');
|
|
190
|
+
}
|
|
191
|
+
const removing = new Set(fields);
|
|
192
|
+
// Precondition: the removed set must be closed under "is read by"
|
|
193
|
+
// — no surviving field may depend on a removed one.
|
|
194
|
+
for (const f of removing) {
|
|
195
|
+
const revs = this.dependents.get(f);
|
|
196
|
+
if (revs === undefined)
|
|
197
|
+
continue;
|
|
198
|
+
for (const d of revs) {
|
|
199
|
+
if (!removing.has(d)) {
|
|
200
|
+
throw new Error(`[spineless-runtime] detach: field "${f.name}" still has dependent "${d.name}" outside the removed set`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Surviving fields the removed set read — candidates for orphan
|
|
205
|
+
// cleanup once their reverse-dependency lists are pruned.
|
|
206
|
+
const survivingDeps = new Set();
|
|
207
|
+
const drop = (f) => {
|
|
208
|
+
const omNode = this.omNodes.get(f);
|
|
209
|
+
if (omNode !== undefined)
|
|
210
|
+
this.om.delete(omNode);
|
|
211
|
+
this.omNodes.delete(f);
|
|
212
|
+
this.values.delete(f);
|
|
213
|
+
this.dependents.delete(f);
|
|
214
|
+
this.grammar.delete(f);
|
|
215
|
+
};
|
|
216
|
+
for (const f of removing) {
|
|
217
|
+
// Prune `f` from the reverse-dependency list of each field it
|
|
218
|
+
// read (a surviving dep must forget this removed dependent).
|
|
219
|
+
const rule = this.grammar.get(f);
|
|
220
|
+
if (rule !== undefined) {
|
|
221
|
+
for (const dep of rule.deps) {
|
|
222
|
+
const revs = this.dependents.get(dep);
|
|
223
|
+
if (revs !== undefined) {
|
|
224
|
+
const i = revs.indexOf(f);
|
|
225
|
+
if (i !== -1)
|
|
226
|
+
revs.splice(i, 1);
|
|
227
|
+
}
|
|
228
|
+
if (!removing.has(dep))
|
|
229
|
+
survivingDeps.add(dep);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
drop(f);
|
|
233
|
+
}
|
|
234
|
+
// Orphan cleanup: a surviving dep with no dependents left, whose
|
|
235
|
+
// own rule is a leaf (no dependencies), is now dead weight.
|
|
236
|
+
for (const dep of survivingDeps) {
|
|
237
|
+
const revs = this.dependents.get(dep);
|
|
238
|
+
if (revs !== undefined && revs.length > 0)
|
|
239
|
+
continue;
|
|
240
|
+
const rule = this.grammar.get(dep);
|
|
241
|
+
if (rule === undefined || rule.deps.length > 0)
|
|
242
|
+
continue;
|
|
243
|
+
drop(dep);
|
|
244
|
+
}
|
|
245
|
+
// The OM tail may have been among the removed fields; recompute
|
|
246
|
+
// it so a later `graft` still appends after every surviving node.
|
|
247
|
+
this.lastOm = null;
|
|
248
|
+
for (const omNode of this.omNodes.values()) {
|
|
249
|
+
if (this.lastOm === null || this.om.compare(omNode, this.lastOm) > 0) {
|
|
250
|
+
this.lastOm = omNode;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Replace the rule of an already-integrated field (phase 5c) — for
|
|
256
|
+
* a structural change that rewrites a *surviving* field rather than
|
|
257
|
+
* adding or removing one. Appending a child into a flex-distributing
|
|
258
|
+
* parent, for instance, grows every existing sibling's
|
|
259
|
+
* flex-distribution dependency set.
|
|
260
|
+
*
|
|
261
|
+
* The reverse-dependency edges are repaired to match the new dep
|
|
262
|
+
* set (the field is dropped from the lists of deps it no longer
|
|
263
|
+
* reads and added to the lists of deps it now reads), the new rule
|
|
264
|
+
* is installed, and the field is marked dirty so the next
|
|
265
|
+
* `recompute()` re-runs it. Every new dependency must already be
|
|
266
|
+
* integrated. The field keeps its OM node; if a new dependency has
|
|
267
|
+
* a later OM, recompute stays correct (see the termination note on
|
|
268
|
+
* the class) at the cost of a bounded number of extra recomputes.
|
|
269
|
+
*/
|
|
270
|
+
rebindRule(field, newRule) {
|
|
271
|
+
if (!this.initDone) {
|
|
272
|
+
throw new Error('[spineless-runtime] rebindRule called before init()');
|
|
273
|
+
}
|
|
274
|
+
if (!this.omNodes.has(field)) {
|
|
275
|
+
throw new Error(`[spineless-runtime] rebindRule: field "${field.name}" is not in this runtime`);
|
|
276
|
+
}
|
|
277
|
+
const oldRule = this.grammar.get(field);
|
|
278
|
+
const oldDeps = new Set(oldRule?.deps ?? []);
|
|
279
|
+
const newDeps = new Set(newRule.deps);
|
|
280
|
+
// Deps no longer read: drop `field` from their dependents list.
|
|
281
|
+
for (const d of oldDeps) {
|
|
282
|
+
if (newDeps.has(d))
|
|
283
|
+
continue;
|
|
284
|
+
const revs = this.dependents.get(d);
|
|
285
|
+
if (revs !== undefined) {
|
|
286
|
+
const i = revs.indexOf(field);
|
|
287
|
+
if (i !== -1)
|
|
288
|
+
revs.splice(i, 1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Newly read deps: register the reverse edge.
|
|
292
|
+
for (const d of newDeps) {
|
|
293
|
+
if (oldDeps.has(d))
|
|
294
|
+
continue;
|
|
295
|
+
if (!this.omNodes.has(d)) {
|
|
296
|
+
throw new Error(`[spineless-runtime] rebindRule: new dependency "${d.name}" of "${field.name}" is not integrated`);
|
|
297
|
+
}
|
|
298
|
+
let revs = this.dependents.get(d);
|
|
299
|
+
if (revs === undefined) {
|
|
300
|
+
revs = [];
|
|
301
|
+
this.dependents.set(d, revs);
|
|
302
|
+
}
|
|
303
|
+
revs.push(field);
|
|
304
|
+
}
|
|
305
|
+
this.grammar.set(field, newRule);
|
|
306
|
+
this.markDirty(field);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Topological DFS shared by `init` and `graft`. For every
|
|
310
|
+
* not-yet-integrated field reachable from `roots`: recurse into
|
|
311
|
+
* deps, record reverse-dependency edges, allocate an OM node after
|
|
312
|
+
* the current tail, run the rule, and cache the value. Fields that
|
|
313
|
+
* already have an OM node are boundaries — visited, edge recorded,
|
|
314
|
+
* not re-walked.
|
|
315
|
+
*/
|
|
316
|
+
integrate(roots) {
|
|
317
|
+
const visiting = new Set();
|
|
318
|
+
const visit = (f) => {
|
|
319
|
+
// A field has an OM node exactly once it is integrated, so
|
|
320
|
+
// `omNodes` doubles as the "already done" marker — which makes
|
|
321
|
+
// existing fields natural boundaries during a graft.
|
|
322
|
+
if (this.omNodes.has(f))
|
|
323
|
+
return;
|
|
324
|
+
if (visiting.has(f)) {
|
|
325
|
+
throw new Error(`[spineless-runtime] cycle detected: field "${f.name}" depends on itself transitively`);
|
|
326
|
+
}
|
|
327
|
+
visiting.add(f);
|
|
328
|
+
const rule = this.grammar.get(f);
|
|
329
|
+
if (rule === undefined) {
|
|
330
|
+
throw new Error(`[spineless-runtime] no rule for field "${f.name}". Register it in the grammar or remove the dep edge.`);
|
|
331
|
+
}
|
|
332
|
+
for (const dep of rule.deps) {
|
|
333
|
+
visit(dep);
|
|
334
|
+
let revs = this.dependents.get(dep);
|
|
335
|
+
if (revs === undefined) {
|
|
336
|
+
revs = [];
|
|
337
|
+
this.dependents.set(dep, revs);
|
|
338
|
+
}
|
|
339
|
+
revs.push(f);
|
|
340
|
+
}
|
|
341
|
+
// Allocate the OM node at the topological tail. The OM is empty
|
|
342
|
+
// before the very first field, then chains insertAfter.
|
|
343
|
+
const omNode = this.lastOm === null ? this.om.init() : this.om.insertAfter(this.lastOm);
|
|
344
|
+
this.lastOm = omNode;
|
|
345
|
+
this.omNodes.set(f, omNode);
|
|
346
|
+
this.stats.initFields++;
|
|
347
|
+
// Compute and cache.
|
|
348
|
+
this.values.set(f, this.runCompute(f, rule));
|
|
349
|
+
visiting.delete(f);
|
|
350
|
+
};
|
|
351
|
+
for (const root of roots)
|
|
352
|
+
visit(root);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Read the current cached value of a field. Throws if the field
|
|
356
|
+
* wasn't reachable from any root during `init` (so no cache entry
|
|
357
|
+
* exists).
|
|
358
|
+
*/
|
|
359
|
+
evaluate(field) {
|
|
360
|
+
if (!this.values.has(field)) {
|
|
361
|
+
throw new Error(`[spineless-runtime] field "${field.name}" was not computed in init() — it isn't reachable from any root`);
|
|
362
|
+
}
|
|
363
|
+
return this.values.get(field);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Whether `field` is currently tracked by the runtime — integrated
|
|
367
|
+
* and not since detached. A caller holding a possibly-stale field
|
|
368
|
+
* reference (e.g. an input field orphaned by a `detach`) can check
|
|
369
|
+
* this before `markDirty`.
|
|
370
|
+
*/
|
|
371
|
+
isTracked(field) {
|
|
372
|
+
return this.omNodes.has(field);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Mark every field tracked by this runtime as dirty. Useful as an
|
|
376
|
+
* escape hatch when callers don't have fine-grained style-mutation
|
|
377
|
+
* wiring yet: mutate styles, call `markAllDirty()`, then
|
|
378
|
+
* `recompute()`. Each field re-runs its rule once, but the "skip
|
|
379
|
+
* dependents when value unchanged" property still applies — so
|
|
380
|
+
* cost is one compute per field plus propagation only along
|
|
381
|
+
* actually-changed values, rather than a full re-init.
|
|
382
|
+
*/
|
|
383
|
+
markAllDirty() {
|
|
384
|
+
if (!this.initDone) {
|
|
385
|
+
throw new Error('[spineless-runtime] markAllDirty called before init()');
|
|
386
|
+
}
|
|
387
|
+
for (const [field, omNode] of this.omNodes) {
|
|
388
|
+
this.pq.push(field, omNode);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Mark a field as dirty. Its rule will be re-run on the next
|
|
393
|
+
* `recompute()`. If the new value differs from the cached one,
|
|
394
|
+
* dependents are scheduled in turn.
|
|
395
|
+
*
|
|
396
|
+
* Duplicate calls are a no-op (the priority queue dedupes via its
|
|
397
|
+
* internal membership set).
|
|
398
|
+
*/
|
|
399
|
+
markDirty(field) {
|
|
400
|
+
if (!this.initDone) {
|
|
401
|
+
throw new Error('[spineless-runtime] markDirty called before init()');
|
|
402
|
+
}
|
|
403
|
+
const om = this.omNodes.get(field);
|
|
404
|
+
if (om === undefined) {
|
|
405
|
+
throw new Error(`[spineless-runtime] field "${field.name}" is not in this runtime — call markDirty only on fields reachable from a root at init`);
|
|
406
|
+
}
|
|
407
|
+
this.pq.push(field, om);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Process all dirty fields. Pops in OM (= topological) order; runs
|
|
411
|
+
* each field's rule. If the result differs from the cached value,
|
|
412
|
+
* persists it and pushes every dependent so it gets re-run later in
|
|
413
|
+
* this same pass.
|
|
414
|
+
*
|
|
415
|
+
* Returns every field whose value actually changed — the caller
|
|
416
|
+
* uses it to scope an incremental write-back to the moved nodes.
|
|
417
|
+
*/
|
|
418
|
+
recompute() {
|
|
419
|
+
if (!this.initDone) {
|
|
420
|
+
throw new Error('[spineless-runtime] recompute called before init()');
|
|
421
|
+
}
|
|
422
|
+
const changed = [];
|
|
423
|
+
this.stats.recomputeVisited = 0;
|
|
424
|
+
this.stats.recomputeChanged = 0;
|
|
425
|
+
while (!this.pq.isEmpty()) {
|
|
426
|
+
const f = this.pq.popMin();
|
|
427
|
+
this.stats.recomputeVisited++;
|
|
428
|
+
this.stats.totalVisited++;
|
|
429
|
+
const rule = this.grammar.get(f);
|
|
430
|
+
const prev = this.values.get(f);
|
|
431
|
+
const next = this.runCompute(f, rule);
|
|
432
|
+
if (!Object.is(prev, next)) {
|
|
433
|
+
this.values.set(f, next);
|
|
434
|
+
this.stats.recomputeChanged++;
|
|
435
|
+
changed.push(f);
|
|
436
|
+
const deps = this.dependents.get(f);
|
|
437
|
+
if (deps !== undefined) {
|
|
438
|
+
for (const d of deps) {
|
|
439
|
+
const om = this.omNodes.get(d);
|
|
440
|
+
this.pq.push(d, om);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return changed;
|
|
446
|
+
}
|
|
447
|
+
runCompute(field, rule) {
|
|
448
|
+
const declaredDeps = new Set(rule.deps);
|
|
449
|
+
const read = (dep) => {
|
|
450
|
+
if (!declaredDeps.has(dep)) {
|
|
451
|
+
throw new Error(`[spineless-runtime] rule for "${field.name}" reads "${dep.name}" but did not declare it as a dependency`);
|
|
452
|
+
}
|
|
453
|
+
return this.values.get(dep);
|
|
454
|
+
};
|
|
455
|
+
return rule.compute(read);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/algorithm/spineless/runtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `markStyleDirty` — convenience for driving a precise incremental
|
|
3
|
+
* relayout after a style mutation.
|
|
4
|
+
*
|
|
5
|
+
* Every numeric style prop the flex grammar reads is modelled as a
|
|
6
|
+
* leaf input `Field` (see `buildFlexGrammar` / `StyleInputs`). After
|
|
7
|
+
* a `Node` setter call, the runtime needs the matching input Field
|
|
8
|
+
* marked dirty before `recompute()`. Looking that Field up through
|
|
9
|
+
* the `styleInputs` map by hand is repetitive and easy to get wrong;
|
|
10
|
+
* `createStyleDirtier` binds a runtime + its `styleInputs` into a
|
|
11
|
+
* single `(node, prop[, edge])` call that mirrors the `Node`
|
|
12
|
+
* setters:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* const { grammar, rootFields, styleInputs } = buildFlexGrammar(root);
|
|
16
|
+
* const rt = new SpinelessRuntime(grammar, [...rootFields...]);
|
|
17
|
+
* rt.init();
|
|
18
|
+
* const markStyleDirty = createStyleDirtier(rt, styleInputs);
|
|
19
|
+
*
|
|
20
|
+
* node.setWidth(50);
|
|
21
|
+
* markStyleDirty(node, 'width');
|
|
22
|
+
* rt.recompute();
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Value mutations only — a structural mutation (flex-direction,
|
|
26
|
+
* flex-wrap on/off, the justify / align category, `positionType`, a
|
|
27
|
+
* flex weight / `flexBasis` crossing the zero / numeric boundary)
|
|
28
|
+
* reshapes the dependency graph and still needs a fresh
|
|
29
|
+
* `buildFlexGrammar()`.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
import type { Node } from '../../node.js';
|
|
34
|
+
import type { StyleInputs } from './flex-grammar.js';
|
|
35
|
+
import type { SpinelessRuntime } from './runtime.js';
|
|
36
|
+
/** Style props addressed by a single input Field. */
|
|
37
|
+
export type ScalarStyleProp = 'width' | 'height' | 'flexBasis' | 'flexGrow' | 'flexShrink' | 'gapRow' | 'gapColumn' | 'minWidth' | 'minHeight' | 'maxWidth' | 'maxHeight';
|
|
38
|
+
/** Style props addressed per `[top, right, bottom, left]` edge. */
|
|
39
|
+
export type EdgeStyleProp = 'padding' | 'margin';
|
|
40
|
+
/**
|
|
41
|
+
* A bound `(node, prop[, edge])` callback that marks the input
|
|
42
|
+
* Field(s) for a mutated style prop dirty on its runtime.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export interface StyleDirtier {
|
|
47
|
+
(node: Node, prop: ScalarStyleProp): void;
|
|
48
|
+
(node: Node, prop: EdgeStyleProp, edge: number): void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Bind a `SpinelessRuntime` and the `styleInputs` map from the
|
|
52
|
+
* `buildFlexGrammar` output that produced its grammar into a
|
|
53
|
+
* `StyleDirtier`.
|
|
54
|
+
*
|
|
55
|
+
* The returned callback throws if `node` is not part of the
|
|
56
|
+
* grammar's `styleInputs` (a node from a different / stale build),
|
|
57
|
+
* or if an edge prop is called without an edge index. It is a no-op
|
|
58
|
+
* when the grammar emits no input Field for the `(node, prop)` —
|
|
59
|
+
* which happens precisely when that prop can't affect layout (e.g.
|
|
60
|
+
* `padding` on a childless leaf), so marking nothing is correct.
|
|
61
|
+
*
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
export declare function createStyleDirtier(runtime: SpinelessRuntime, styleInputs: ReadonlyMap<Node, StyleInputs>): StyleDirtier;
|
|
65
|
+
//# sourceMappingURL=style-dirty.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style-dirty.d.ts","sourceRoot":"","sources":["../../../src/algorithm/spineless/style-dirty.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,qDAAqD;AACrD,MAAM,MAAM,eAAe,GACvB,OAAO,GACP,QAAQ,GACR,WAAW,GACX,UAAU,GACV,YAAY,GACZ,QAAQ,GACR,WAAW,GACX,UAAU,GACV,WAAW,GACX,UAAU,GACV,WAAW,CAAC;AAEhB,mEAAmE;AACnE,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CAAC;IAC1C,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACvD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,gBAAgB,EACzB,WAAW,EAAE,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC,GAC1C,YAAY,CA8Bd"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `markStyleDirty` — convenience for driving a precise incremental
|
|
3
|
+
* relayout after a style mutation.
|
|
4
|
+
*
|
|
5
|
+
* Every numeric style prop the flex grammar reads is modelled as a
|
|
6
|
+
* leaf input `Field` (see `buildFlexGrammar` / `StyleInputs`). After
|
|
7
|
+
* a `Node` setter call, the runtime needs the matching input Field
|
|
8
|
+
* marked dirty before `recompute()`. Looking that Field up through
|
|
9
|
+
* the `styleInputs` map by hand is repetitive and easy to get wrong;
|
|
10
|
+
* `createStyleDirtier` binds a runtime + its `styleInputs` into a
|
|
11
|
+
* single `(node, prop[, edge])` call that mirrors the `Node`
|
|
12
|
+
* setters:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* const { grammar, rootFields, styleInputs } = buildFlexGrammar(root);
|
|
16
|
+
* const rt = new SpinelessRuntime(grammar, [...rootFields...]);
|
|
17
|
+
* rt.init();
|
|
18
|
+
* const markStyleDirty = createStyleDirtier(rt, styleInputs);
|
|
19
|
+
*
|
|
20
|
+
* node.setWidth(50);
|
|
21
|
+
* markStyleDirty(node, 'width');
|
|
22
|
+
* rt.recompute();
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Value mutations only — a structural mutation (flex-direction,
|
|
26
|
+
* flex-wrap on/off, the justify / align category, `positionType`, a
|
|
27
|
+
* flex weight / `flexBasis` crossing the zero / numeric boundary)
|
|
28
|
+
* reshapes the dependency graph and still needs a fresh
|
|
29
|
+
* `buildFlexGrammar()`.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Bind a `SpinelessRuntime` and the `styleInputs` map from the
|
|
35
|
+
* `buildFlexGrammar` output that produced its grammar into a
|
|
36
|
+
* `StyleDirtier`.
|
|
37
|
+
*
|
|
38
|
+
* The returned callback throws if `node` is not part of the
|
|
39
|
+
* grammar's `styleInputs` (a node from a different / stale build),
|
|
40
|
+
* or if an edge prop is called without an edge index. It is a no-op
|
|
41
|
+
* when the grammar emits no input Field for the `(node, prop)` —
|
|
42
|
+
* which happens precisely when that prop can't affect layout (e.g.
|
|
43
|
+
* `padding` on a childless leaf), so marking nothing is correct.
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
export function createStyleDirtier(runtime, styleInputs) {
|
|
48
|
+
return (node, prop, edge) => {
|
|
49
|
+
const entry = styleInputs.get(node);
|
|
50
|
+
if (entry === undefined) {
|
|
51
|
+
throw new Error('[spineless] markStyleDirty: node has no style inputs in this grammar — pass a node from the same buildFlexGrammar() tree');
|
|
52
|
+
}
|
|
53
|
+
let f;
|
|
54
|
+
if (prop === 'padding' || prop === 'margin') {
|
|
55
|
+
if (edge === undefined) {
|
|
56
|
+
throw new Error(`[spineless] markStyleDirty: '${prop}' requires an edge index`);
|
|
57
|
+
}
|
|
58
|
+
f = entry[prop]?.[edge];
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
f = entry[prop];
|
|
62
|
+
}
|
|
63
|
+
// Marking nothing is the correct, precise behaviour when the
|
|
64
|
+
// mutation cannot move any layout field. That is the case both
|
|
65
|
+
// when no input Field exists for the prop, and when one exists
|
|
66
|
+
// but has been orphaned by a `detach` — e.g. the previous last
|
|
67
|
+
// child's main-end margin after its follower was removed. An
|
|
68
|
+
// orphaned input is no longer tracked by the runtime; a stale
|
|
69
|
+
// reference to it may linger in a `styleInputs` map.
|
|
70
|
+
if (f !== undefined && runtime.isTracked(f)) {
|
|
71
|
+
runtime.markDirty(f);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=style-dirty.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style-dirty.js","sourceRoot":"","sources":["../../../src/algorithm/spineless/style-dirty.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAmCH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAyB,EACzB,WAA2C;IAE3C,OAAO,CAAC,IAAU,EAAE,IAAqC,EAAE,IAAa,EAAQ,EAAE;QAChF,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CACb,0HAA0H,CAC3H,CAAC;QACJ,CAAC;QAED,IAAI,CAA4B,CAAC;QACjC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC5C,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;gBACvB,MAAM,IAAI,KAAK,CAAC,gCAAgC,IAAI,0BAA0B,CAAC,CAAC;YAClF,CAAC;YACD,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;aAAM,CAAC;YACN,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC;QAED,6DAA6D;QAC7D,+DAA+D;QAC/D,+DAA+D;QAC/D,+DAA+D;QAC/D,6DAA6D;QAC7D,8DAA8D;QAC9D,qDAAqD;QACrD,IAAI,CAAC,KAAK,SAAS,IAAI,OAAO,CAAC,SAAS,CAAC,CAAmB,CAAC,EAAE,CAAC;YAC9D,OAAO,CAAC,SAAS,CAAC,CAAmB,CAAC,CAAC;QACzC,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
export declare const VERSION = "1.
|
|
1
|
+
export declare const VERSION = "1.1.0";
|
|
2
2
|
export { Node } from './node.js';
|
|
3
3
|
export { Edge } from './edge.js';
|
|
4
4
|
export type { Align, Display, FlexDirection, FlexWrap, Justify, Length, Overflow, PositionType, Style, } from './style.js';
|
|
5
5
|
export { MeasureMode } from './measure-func.js';
|
|
6
6
|
export type { MeasureFunc, MeasureSize } from './measure-func.js';
|
|
7
7
|
export type { ComputedLayout } from './layout.js';
|
|
8
|
+
export { setLayoutProfiler, type LayoutProfiler, type LayoutTrace } from './algorithm/index.js';
|
|
9
|
+
export { inspectLayout } from './inspect.js';
|
|
8
10
|
export { cellWidth, graphemes, stringWidth, stripAnsi, type Grapheme } from './measure/index.js';
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,UAAU,CAAC;AAG/B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EACV,KAAK,EACL,OAAO,EACP,aAAa,EACb,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,KAAK,GACN,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGlE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIlD,OAAO,EAAE,iBAAiB,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGhG,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAI7C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
export const VERSION = '1.
|
|
1
|
+
export const VERSION = '1.1.0';
|
|
2
2
|
// Tree
|
|
3
3
|
export { Node } from './node.js';
|
|
4
4
|
// Style values
|
|
5
5
|
export { Edge } from './edge.js';
|
|
6
6
|
// Measure callback
|
|
7
7
|
export { MeasureMode } from './measure-func.js';
|
|
8
|
+
// Layout profiling — observe what the incremental engine did per
|
|
9
|
+
// `calculateLayout` call (phase 9).
|
|
10
|
+
export { setLayoutProfiler } from './algorithm/index.js';
|
|
11
|
+
// Layout inspection — a console dump of a computed-layout subtree.
|
|
12
|
+
export { inspectLayout } from './inspect.js';
|
|
8
13
|
// Text measurement (re-exported from the measure module so consumers can
|
|
9
14
|
// build measure functions on top of our width tables).
|
|
10
15
|
export { cellWidth, graphemes, stringWidth, stripAnsi } from './measure/index.js';
|