forma-dsl 0.1.1 → 0.1.2
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/package.json +1 -1
- package/src/core/context.js +1 -1
- package/src/core/digest.js +27 -11
- package/src/core/node.js +19 -2
- package/src/lang/ast.js +5 -5
- package/src/lang/evaluate.js +33 -21
- package/src/lang/parser.js +10 -9
package/package.json
CHANGED
package/src/core/context.js
CHANGED
|
@@ -233,7 +233,7 @@ export class EvaluationContext {
|
|
|
233
233
|
case 'union': return Concrete.union(parts);
|
|
234
234
|
case 'difference': return Concrete.difference(parts);
|
|
235
235
|
case 'intersection': return Concrete.intersection(parts);
|
|
236
|
-
default: throw new Error(`unknown boolean operation ${props.op}`);
|
|
236
|
+
default: throw new Error(`unknown boolean operation ${JSON.stringify(props.op)}`);
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
package/src/core/digest.js
CHANGED
|
@@ -18,18 +18,34 @@ function rotl(x, r) {
|
|
|
18
18
|
return (x << r) | (x >>> (32 - r));
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Wraps a number to a signed 32-bit integer, the way the algorithm's additions are defined.
|
|
23
|
+
*
|
|
24
|
+
* `Math.imul(x, 1)` rather than `x | 0`: the two are identical — both coerce through
|
|
25
|
+
* ToInt32 — but this one says *wrap*, which is what MurmurHash means here. `Math.trunc` is
|
|
26
|
+
* emphatically not a substitute: it drops the fraction without wrapping, so `2**31 + 5`
|
|
27
|
+
* stays positive instead of becoming negative, and every digest downstream changes.
|
|
28
|
+
*
|
|
29
|
+
* @param {number} x The value to wrap.
|
|
30
|
+
* @returns {number} The low 32 bits, signed.
|
|
31
|
+
*/
|
|
32
|
+
function int32(x) {
|
|
33
|
+
return Math.imul(x, 1);
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
/**
|
|
22
37
|
* Multiplies two 32-bit words, keeping the low bits.
|
|
23
38
|
*
|
|
24
39
|
* `Math.imul`, not `*`: a 32-bit product above 2^53 loses its low bits to the double, and
|
|
25
|
-
* those low bits are most of the mixing.
|
|
40
|
+
* those low bits are most of the mixing. It is also already defined to return a signed
|
|
41
|
+
* 32-bit result, so nothing here has to wrap it again.
|
|
26
42
|
*
|
|
27
43
|
* @param {number} a The left operand.
|
|
28
44
|
* @param {number} b The right operand.
|
|
29
45
|
* @returns {number} The low 32 bits of the product.
|
|
30
46
|
*/
|
|
31
47
|
function mul(a, b) {
|
|
32
|
-
return Math.imul(a, b)
|
|
48
|
+
return Math.imul(a, b);
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
/**
|
|
@@ -90,16 +106,16 @@ export function digestBytes(bytes) {
|
|
|
90
106
|
let k4 = view.getUint32(o + 12, true);
|
|
91
107
|
|
|
92
108
|
k1 = mul(k1, C1); k1 = rotl(k1, 15); k1 = mul(k1, C2); h1 ^= k1;
|
|
93
|
-
h1 = rotl(h1, 19); h1 = (h1 + h2)
|
|
109
|
+
h1 = rotl(h1, 19); h1 = int32(h1 + h2); h1 = int32(mul(h1, 5) + 0x561ccd1b);
|
|
94
110
|
|
|
95
111
|
k2 = mul(k2, C2); k2 = rotl(k2, 16); k2 = mul(k2, C3); h2 ^= k2;
|
|
96
|
-
h2 = rotl(h2, 17); h2 = (h2 + h3)
|
|
112
|
+
h2 = rotl(h2, 17); h2 = int32(h2 + h3); h2 = int32(mul(h2, 5) + 0x0bcaa747);
|
|
97
113
|
|
|
98
114
|
k3 = mul(k3, C3); k3 = rotl(k3, 17); k3 = mul(k3, C4); h3 ^= k3;
|
|
99
|
-
h3 = rotl(h3, 15); h3 = (h3 + h4)
|
|
115
|
+
h3 = rotl(h3, 15); h3 = int32(h3 + h4); h3 = int32(mul(h3, 5) + 0x96cd1c35);
|
|
100
116
|
|
|
101
117
|
k4 = mul(k4, C4); k4 = rotl(k4, 18); k4 = mul(k4, C1); h4 ^= k4;
|
|
102
|
-
h4 = rotl(h4, 13); h4 = (h4 + h1)
|
|
118
|
+
h4 = rotl(h4, 13); h4 = int32(h4 + h1); h4 = int32(mul(h4, 5) + 0x32ac3b17);
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
// The trailing bytes that did not fill a 16-byte block, folded in a word at a time.
|
|
@@ -115,11 +131,11 @@ export function digestBytes(bytes) {
|
|
|
115
131
|
if (rem >= 13) { let x = mul(k[3], C4); x = rotl(x, 18); h4 ^= mul(x, C1); }
|
|
116
132
|
|
|
117
133
|
h1 ^= len; h2 ^= len; h3 ^= len; h4 ^= len;
|
|
118
|
-
h1 = (h1 + h2)
|
|
119
|
-
h2 = (h2 + h1)
|
|
134
|
+
h1 = int32(h1 + h2); h1 = int32(h1 + h3); h1 = int32(h1 + h4);
|
|
135
|
+
h2 = int32(h2 + h1); h3 = int32(h3 + h1); h4 = int32(h4 + h1);
|
|
120
136
|
h1 = fmix(h1); h2 = fmix(h2); h3 = fmix(h3); h4 = fmix(h4);
|
|
121
|
-
h1 = (h1 + h2)
|
|
122
|
-
h2 = (h2 + h1)
|
|
137
|
+
h1 = int32(h1 + h2); h1 = int32(h1 + h3); h1 = int32(h1 + h4);
|
|
138
|
+
h2 = int32(h2 + h1); h3 = int32(h3 + h1); h4 = int32(h4 + h1);
|
|
123
139
|
|
|
124
140
|
return [h1 >>> 0, h2 >>> 0, h3 >>> 0, h4 >>> 0]
|
|
125
141
|
.map((w) => w.toString(16).padStart(8, '0'))
|
|
@@ -170,7 +186,7 @@ export class DigestWriter {
|
|
|
170
186
|
*/
|
|
171
187
|
int(value) {
|
|
172
188
|
const bytes = new Uint8Array(4);
|
|
173
|
-
new DataView(bytes.buffer).setInt32(0, value
|
|
189
|
+
new DataView(bytes.buffer).setInt32(0, int32(value), true);
|
|
174
190
|
this.#push(bytes);
|
|
175
191
|
return this;
|
|
176
192
|
}
|
package/src/core/node.js
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
import { DigestWriter } from './digest.js';
|
|
4
4
|
import { Transform } from '../values/transform.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Orders strings by UTF-16 code unit, which is what a bare `sort()` does.
|
|
8
|
+
*
|
|
9
|
+
* Written out rather than left implicit — and deliberately *not* `localeCompare`, which is
|
|
10
|
+
* what a linter will suggest. Collation is locale-dependent, and this ordering feeds the
|
|
11
|
+
* digest: two machines with different locales would have to agree, or the cache would miss
|
|
12
|
+
* across them and a model would re-solve from scratch on someone else's laptop.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} a The left string.
|
|
15
|
+
* @param {string} b The right string.
|
|
16
|
+
* @returns {number} Negative, zero or positive, as `sort` wants.
|
|
17
|
+
*/
|
|
18
|
+
function byCodeUnit(a, b) {
|
|
19
|
+
if (a < b) return -1;
|
|
20
|
+
return a > b ? 1 : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
/**
|
|
7
24
|
* Canonically encodes an attribute value into the digest.
|
|
8
25
|
*
|
|
@@ -29,7 +46,7 @@ function writeValue(writer, value) {
|
|
|
29
46
|
} else if (value instanceof Transform) {
|
|
30
47
|
writer.string('t').numbers(value.toArray());
|
|
31
48
|
} else {
|
|
32
|
-
const keys = Object.keys(value).sort();
|
|
49
|
+
const keys = Object.keys(value).sort(byCodeUnit);
|
|
33
50
|
writer.string('o').int(keys.length);
|
|
34
51
|
for (const key of keys) {
|
|
35
52
|
writer.string(key);
|
|
@@ -113,7 +130,7 @@ export class GeometryNode {
|
|
|
113
130
|
throw new TypeError(`cannot combine 2D and 3D geometry in a ${op}`);
|
|
114
131
|
}
|
|
115
132
|
const ordered = op === 'union'
|
|
116
|
-
? [...live].sort((a, b) => a.subtreeSize - b.subtreeSize || a.digest
|
|
133
|
+
? [...live].sort((a, b) => a.subtreeSize - b.subtreeSize || byCodeUnit(a.digest, b.digest))
|
|
117
134
|
: live;
|
|
118
135
|
return new GeometryNode(dim, 'boolean', { op }, ordered);
|
|
119
136
|
}
|
package/src/lang/ast.js
CHANGED
|
@@ -100,13 +100,13 @@ export {};
|
|
|
100
100
|
*/
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
* A `condition ?
|
|
103
|
+
* A `condition ? consequent : alternate`.
|
|
104
104
|
*
|
|
105
105
|
* @typedef {object} ConditionalExpression
|
|
106
106
|
* @property {'conditional'} kind Discriminant.
|
|
107
107
|
* @property {Expression} condition What decides.
|
|
108
|
-
* @property {Expression}
|
|
109
|
-
* @property {Expression}
|
|
108
|
+
* @property {Expression} consequent The value when truthy.
|
|
109
|
+
* @property {Expression} alternate The value when not.
|
|
110
110
|
* @property {SourceLocation} [loc] Where it was written.
|
|
111
111
|
*/
|
|
112
112
|
|
|
@@ -176,8 +176,8 @@ export {};
|
|
|
176
176
|
* @typedef {object} IfBlock
|
|
177
177
|
* @property {'if'} kind Discriminant.
|
|
178
178
|
* @property {Expression} condition What decides.
|
|
179
|
-
* @property {Body}
|
|
180
|
-
* @property {Body | null}
|
|
179
|
+
* @property {Body} consequent The branch taken when truthy.
|
|
180
|
+
* @property {Body | null} alternate The branch taken otherwise, if there is one.
|
|
181
181
|
* @property {SourceLocation} loc Where the `if` was.
|
|
182
182
|
*/
|
|
183
183
|
|
package/src/lang/evaluate.js
CHANGED
|
@@ -18,6 +18,9 @@ import { Transform } from '../values/transform.js';
|
|
|
18
18
|
* @property {ScenePart[]} parts The scene parts declared below here.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
/** Which index `.x`, `.y` and `.z` stand for when read off a list. */
|
|
22
|
+
const AXES = { x: 0, y: 1, z: 2 };
|
|
23
|
+
|
|
21
24
|
/** A lexical scope, chained to its parent. */
|
|
22
25
|
class Scope {
|
|
23
26
|
/**
|
|
@@ -36,13 +39,8 @@ class Scope {
|
|
|
36
39
|
* @returns {unknown} Its value, or undefined if nothing binds it.
|
|
37
40
|
*/
|
|
38
41
|
lookup(name) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
while (scope) {
|
|
42
|
-
if (scope.values.has(name)) return scope.values.get(name);
|
|
43
|
-
scope = scope.parent;
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
42
|
+
if (this.values.has(name)) return this.values.get(name);
|
|
43
|
+
return this.parent ? this.parent.lookup(name) : undefined;
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
/**
|
|
@@ -52,13 +50,8 @@ class Scope {
|
|
|
52
50
|
* @returns {boolean} Whether anything binds it.
|
|
53
51
|
*/
|
|
54
52
|
has(name) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
while (scope) {
|
|
58
|
-
if (scope.values.has(name)) return true;
|
|
59
|
-
scope = scope.parent;
|
|
60
|
-
}
|
|
61
|
-
return false;
|
|
53
|
+
if (this.values.has(name)) return true;
|
|
54
|
+
return this.parent ? this.parent.has(name) : false;
|
|
62
55
|
}
|
|
63
56
|
|
|
64
57
|
/**
|
|
@@ -303,8 +296,8 @@ export class Evaluator {
|
|
|
303
296
|
|
|
304
297
|
case 'conditional':
|
|
305
298
|
return truthy(this.expression(node.condition, scope))
|
|
306
|
-
? this.expression(node.
|
|
307
|
-
: this.expression(node.
|
|
299
|
+
? this.expression(node.consequent, scope)
|
|
300
|
+
: this.expression(node.alternate, scope);
|
|
308
301
|
|
|
309
302
|
case 'call': return this.call(node, scope);
|
|
310
303
|
|
|
@@ -329,8 +322,7 @@ export class Evaluator {
|
|
|
329
322
|
if (object === null || object === undefined) {
|
|
330
323
|
throw new FormaError(`cannot read "${node.property}" of nothing`, node.loc);
|
|
331
324
|
}
|
|
332
|
-
|
|
333
|
-
if (Array.isArray(object) && axis !== undefined) return object[axis];
|
|
325
|
+
if (Array.isArray(object) && node.property in AXES) return object[AXES[node.property]];
|
|
334
326
|
if (!(node.property in object)) {
|
|
335
327
|
throw new FormaError(`no attribute "${node.property}"`, node.loc);
|
|
336
328
|
}
|
|
@@ -442,7 +434,7 @@ export class Evaluator {
|
|
|
442
434
|
|
|
443
435
|
if (block.kind === 'for') return this.forBlock(block, scope);
|
|
444
436
|
if (block.kind === 'if') {
|
|
445
|
-
const taken = truthy(this.expression(block.condition, scope)) ? block.
|
|
437
|
+
const taken = truthy(this.expression(block.condition, scope)) ? block.consequent : block.alternate;
|
|
446
438
|
return taken ? this.body(taken, scope) : { nodes: [], parts: [] };
|
|
447
439
|
}
|
|
448
440
|
|
|
@@ -581,7 +573,7 @@ export class Evaluator {
|
|
|
581
573
|
// An unlabelled part still needs a name to show in the viewer; its line is the one
|
|
582
574
|
// thing guaranteed to differ between two of them.
|
|
583
575
|
name: block.labels.length
|
|
584
|
-
?
|
|
576
|
+
? formatValue(this.expression(block.labels[0], scope))
|
|
585
577
|
: `part_${block.loc.line}`,
|
|
586
578
|
color: args.string('color', '#b8c4d0'),
|
|
587
579
|
opacity: args.number('opacity', 1),
|
|
@@ -760,7 +752,27 @@ function truthy(value) {
|
|
|
760
752
|
function formatValue(value) {
|
|
761
753
|
if (Array.isArray(value)) return `[${value.map(formatValue).join(', ')}]`;
|
|
762
754
|
if (value === null || value === undefined) return '';
|
|
763
|
-
|
|
755
|
+
// An object reaching here is almost always a mistake in the model, so it is shown the way
|
|
756
|
+
// it was written rather than as JavaScript's `[object Object]`, which says nothing about
|
|
757
|
+
// which object went wrong.
|
|
758
|
+
if (typeof value === 'object') return `{${Object.entries(value).map(formatEntry).join(', ')}}`;
|
|
759
|
+
if (typeof value === 'string') return value;
|
|
760
|
+
// A forma expression yields a number, a string, a bool, null, a list or an object, so by
|
|
761
|
+
// here it is a number or a bool. Spelling that out keeps the coercion off `unknown`, where
|
|
762
|
+
// it could silently produce "[object Object]" for something this does not handle.
|
|
763
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
764
|
+
return '';
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Renders one object entry. Separate from {@link formatValue} only so the template literals
|
|
769
|
+
* do not nest.
|
|
770
|
+
*
|
|
771
|
+
* @param {[string, unknown]} entry The key and its value.
|
|
772
|
+
* @returns {string} `key: value`.
|
|
773
|
+
*/
|
|
774
|
+
function formatEntry([key, value]) {
|
|
775
|
+
return `${key}: ${formatValue(value)}`;
|
|
764
776
|
}
|
|
765
777
|
|
|
766
778
|
/**
|
package/src/lang/parser.js
CHANGED
|
@@ -75,7 +75,8 @@ class Parser {
|
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* @param {TokenType} type The kind required.
|
|
78
|
-
* @param {
|
|
78
|
+
* @param {string} [value] The exact value required, when it matters. Always a keyword or a
|
|
79
|
+
* piece of punctuation, which is why it can go straight into the message.
|
|
79
80
|
* @returns {Token} The consumed token.
|
|
80
81
|
* @throws {FormaError} If the current token is not what was required.
|
|
81
82
|
*/
|
|
@@ -271,17 +272,17 @@ class Parser {
|
|
|
271
272
|
parseIf() {
|
|
272
273
|
const loc = this.expect('ident', 'if').loc;
|
|
273
274
|
const condition = this.parseExpression();
|
|
274
|
-
const
|
|
275
|
-
let
|
|
275
|
+
const consequent = this.parseBracedBody();
|
|
276
|
+
let alternate = null;
|
|
276
277
|
if (this.isIdent('else')) {
|
|
277
278
|
this.next();
|
|
278
279
|
if (this.isIdent('if')) {
|
|
279
|
-
|
|
280
|
+
alternate = { attributes: [], params: [], locals: [], blocks: [this.parseIf()] };
|
|
280
281
|
} else {
|
|
281
|
-
|
|
282
|
+
alternate = this.parseBracedBody();
|
|
282
283
|
}
|
|
283
284
|
}
|
|
284
|
-
return { kind: 'if', condition,
|
|
285
|
+
return { kind: 'if', condition, consequent, alternate, loc };
|
|
285
286
|
}
|
|
286
287
|
|
|
287
288
|
// --- expressions ----------------------------------------------------------------
|
|
@@ -298,10 +299,10 @@ class Parser {
|
|
|
298
299
|
const condition = this.parseBinary(0);
|
|
299
300
|
if (this.isPunct('?') && !this.breaksLine()) {
|
|
300
301
|
this.next();
|
|
301
|
-
const
|
|
302
|
+
const consequent = this.parseExpression();
|
|
302
303
|
this.expect('punct', ':');
|
|
303
|
-
const
|
|
304
|
-
return { kind: 'conditional', condition,
|
|
304
|
+
const alternate = this.parseExpression();
|
|
305
|
+
return { kind: 'conditional', condition, consequent, alternate };
|
|
305
306
|
}
|
|
306
307
|
return condition;
|
|
307
308
|
}
|