@loadmap/dsl 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +15 -2
- package/dist/compile-6wNrbYqA.js +3065 -0
- package/dist/compile-C2Kdi-Ul.d.ts +212 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +262 -0
- package/dist/internal.d.ts +94 -0
- package/dist/internal.js +44 -0
- package/package.json +40 -3
|
@@ -0,0 +1,3065 @@
|
|
|
1
|
+
import { check, defineModel, estimated, fanout, measured, p, parseAsOf, pass, queue, sink, store, workers } from "@loadmap/core";
|
|
2
|
+
import { fitLinear } from "@loadmap/core/calibrate";
|
|
3
|
+
import { defineView, labelsAt } from "@loadmap/layout";
|
|
4
|
+
import { compact, fmt, pct, secs } from "@loadmap/core/format";
|
|
5
|
+
/** Collects diagnostics and turns offsets into lines and columns. */
|
|
6
|
+
var Reporter = class {
|
|
7
|
+
source;
|
|
8
|
+
list = [];
|
|
9
|
+
seen = /* @__PURE__ */ new Set();
|
|
10
|
+
starts;
|
|
11
|
+
constructor(source) {
|
|
12
|
+
this.source = source;
|
|
13
|
+
this.starts = [0];
|
|
14
|
+
for (let i = 0; i < source.length; i++) if (source[i] === "\n") this.starts.push(i + 1);
|
|
15
|
+
}
|
|
16
|
+
error(at, message, hint) {
|
|
17
|
+
this.add("error", at, message, hint);
|
|
18
|
+
}
|
|
19
|
+
warning(at, message, hint) {
|
|
20
|
+
this.add("warning", at, message, hint);
|
|
21
|
+
}
|
|
22
|
+
/** The 1-based line and column of an offset. */
|
|
23
|
+
position(offset) {
|
|
24
|
+
return position(this.starts, offset);
|
|
25
|
+
}
|
|
26
|
+
get failed() {
|
|
27
|
+
return this.list.some((d) => d.severity === "error");
|
|
28
|
+
}
|
|
29
|
+
add(severity, at, message, hint) {
|
|
30
|
+
const key = `${at.from}:${message}`;
|
|
31
|
+
if (this.seen.has(key)) return;
|
|
32
|
+
this.seen.add(key);
|
|
33
|
+
if (this.list.length >= 200) {
|
|
34
|
+
if (this.list.length === 200) this.list.push({
|
|
35
|
+
...this.list.at(-1),
|
|
36
|
+
severity: "error",
|
|
37
|
+
message: `more than 200 problems; fix these first`,
|
|
38
|
+
hint: void 0
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const { line, column } = position(this.starts, at.from);
|
|
43
|
+
this.list.push({
|
|
44
|
+
severity,
|
|
45
|
+
message,
|
|
46
|
+
from: at.from,
|
|
47
|
+
to: Math.max(at.to, at.from),
|
|
48
|
+
line,
|
|
49
|
+
column,
|
|
50
|
+
hint
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function position(starts, offset) {
|
|
55
|
+
let lo = 0;
|
|
56
|
+
let hi = starts.length - 1;
|
|
57
|
+
while (lo < hi) {
|
|
58
|
+
const mid = lo + hi + 1 >> 1;
|
|
59
|
+
if (starts[mid] <= offset) lo = mid;
|
|
60
|
+
else hi = mid - 1;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
line: lo + 1,
|
|
64
|
+
column: offset - starts[lo] + 1
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* One diagnostic as a terminal shows it:
|
|
69
|
+
*
|
|
70
|
+
* checkout.loadmap:24:17 error: unknown flow "order"
|
|
71
|
+
* 24 | demand: flows.order + flows.events
|
|
72
|
+
* | ^^^^^ did you mean "orders"?
|
|
73
|
+
*/
|
|
74
|
+
function formatDiagnostic(d, source, file = "input.loadmap") {
|
|
75
|
+
const text = source.split("\n")[d.line - 1] ?? "";
|
|
76
|
+
const n = String(d.line);
|
|
77
|
+
const pad = " ".repeat(n.length);
|
|
78
|
+
const width = Math.max(1, Math.min(d.to - d.from, text.length - d.column + 1));
|
|
79
|
+
const mark = `${" ".repeat(d.column - 1)}${"^".repeat(width)}${d.hint ? ` ${d.hint}` : ""}`;
|
|
80
|
+
return `${file}:${d.line}:${d.column} ${d.severity}: ${d.message}\n ${n} | ${text}\n ${pad} | ${mark}`;
|
|
81
|
+
}
|
|
82
|
+
/** The closest of `options` to `name`, when it is close enough to be a typo. */
|
|
83
|
+
function closest(name, options) {
|
|
84
|
+
let best;
|
|
85
|
+
let bestD = Number.POSITIVE_INFINITY;
|
|
86
|
+
const lower = name.toLowerCase();
|
|
87
|
+
for (const o of options) {
|
|
88
|
+
const d = o.toLowerCase() === lower ? 0 : distance(lower, o.toLowerCase());
|
|
89
|
+
if (d < bestD) {
|
|
90
|
+
best = o;
|
|
91
|
+
bestD = d;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return best != null && bestD <= Math.max(1, Math.floor(name.length / 3)) ? best : void 0;
|
|
95
|
+
}
|
|
96
|
+
function didYouMean(name, options) {
|
|
97
|
+
const c = closest(name, options);
|
|
98
|
+
return c ? `did you mean "${c}"?` : void 0;
|
|
99
|
+
}
|
|
100
|
+
function distance(a, b) {
|
|
101
|
+
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
102
|
+
for (let i = 1; i <= a.length; i++) {
|
|
103
|
+
let prev = row[0];
|
|
104
|
+
row[0] = i;
|
|
105
|
+
for (let j = 1; j <= b.length; j++) {
|
|
106
|
+
const cur = row[j];
|
|
107
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
108
|
+
prev = cur;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return row[b.length];
|
|
112
|
+
}
|
|
113
|
+
// src/schema.ts
|
|
114
|
+
/**
|
|
115
|
+
* Every statement of the language: what follows its keyword, its properties and what it may hold. The checker,
|
|
116
|
+
* autocomplete and the language reference (docs/dsl.md) all read this table, so it is the one place to add a feature.
|
|
117
|
+
*/
|
|
118
|
+
/** `o[k]` for an own entry only: a name like "constructor" must not find what every object inherits. */
|
|
119
|
+
function own(o, k) {
|
|
120
|
+
return Object.hasOwn(o, k) ? o[k] : void 0;
|
|
121
|
+
}
|
|
122
|
+
const STAGE_LABEL = [{
|
|
123
|
+
name: "id",
|
|
124
|
+
kind: "id",
|
|
125
|
+
optional: true
|
|
126
|
+
}, {
|
|
127
|
+
name: "label",
|
|
128
|
+
kind: "text",
|
|
129
|
+
optional: true
|
|
130
|
+
}];
|
|
131
|
+
const STAGE_COMPONENT = {
|
|
132
|
+
value: "component",
|
|
133
|
+
scope: "const",
|
|
134
|
+
doc: "The component whose capacity this stage uses. Defaults to its box's component."
|
|
135
|
+
};
|
|
136
|
+
/** A component's properties: on a box, or in a `component` block. */
|
|
137
|
+
const COMPONENT_PROPS = {
|
|
138
|
+
name: {
|
|
139
|
+
value: "string",
|
|
140
|
+
scope: "const",
|
|
141
|
+
doc: "The component's name in the limits list. Defaults to the label."
|
|
142
|
+
},
|
|
143
|
+
short: {
|
|
144
|
+
value: "string",
|
|
145
|
+
scope: "const",
|
|
146
|
+
doc: "The name inside a sentence, with its article: \"the database\"."
|
|
147
|
+
},
|
|
148
|
+
plural: {
|
|
149
|
+
value: "boolean",
|
|
150
|
+
scope: "const",
|
|
151
|
+
doc: "\"are past capacity\" instead of \"is\"."
|
|
152
|
+
},
|
|
153
|
+
demand: {
|
|
154
|
+
value: "number",
|
|
155
|
+
scope: "ctx",
|
|
156
|
+
doc: "What the component must handle, in its capacity's unit."
|
|
157
|
+
},
|
|
158
|
+
capacity: {
|
|
159
|
+
value: "capacity",
|
|
160
|
+
scope: "ctx",
|
|
161
|
+
doc: "What it can handle: measured(value, unit, source) or estimated(value, unit, source); `value ± 30%` says how sure it is."
|
|
162
|
+
},
|
|
163
|
+
when: {
|
|
164
|
+
value: "boolean",
|
|
165
|
+
scope: "ctx",
|
|
166
|
+
doc: "The component is switched off (no utilization) while this is false."
|
|
167
|
+
},
|
|
168
|
+
over: {
|
|
169
|
+
value: "string",
|
|
170
|
+
scope: "ctx",
|
|
171
|
+
doc: "What happens past 100 %, as a clause: \"clients get 429s\"."
|
|
172
|
+
},
|
|
173
|
+
describe: {
|
|
174
|
+
value: "string",
|
|
175
|
+
scope: "ctx",
|
|
176
|
+
doc: "One line under the name in the limits list."
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
const BOX_ARGS = [
|
|
180
|
+
{
|
|
181
|
+
name: "id",
|
|
182
|
+
kind: "id"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "label",
|
|
186
|
+
kind: "text",
|
|
187
|
+
optional: true,
|
|
188
|
+
dynamic: true
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "sub",
|
|
192
|
+
kind: "text",
|
|
193
|
+
optional: true,
|
|
194
|
+
dynamic: true
|
|
195
|
+
}
|
|
196
|
+
];
|
|
197
|
+
const SUB = {
|
|
198
|
+
value: "string",
|
|
199
|
+
scope: "ctx",
|
|
200
|
+
doc: "The second line of the box."
|
|
201
|
+
};
|
|
202
|
+
const OVERLOAD_WHEN = {
|
|
203
|
+
value: "boolean",
|
|
204
|
+
scope: "ctx",
|
|
205
|
+
doc: "The rule applies only while this is true."
|
|
206
|
+
};
|
|
207
|
+
const COLORS = [
|
|
208
|
+
"accent",
|
|
209
|
+
"alt",
|
|
210
|
+
"ok",
|
|
211
|
+
"warn",
|
|
212
|
+
"drop",
|
|
213
|
+
"muted"
|
|
214
|
+
];
|
|
215
|
+
const SCHEMA = {
|
|
216
|
+
"": {
|
|
217
|
+
doc: "A loadmap definition.",
|
|
218
|
+
example: "",
|
|
219
|
+
args: [],
|
|
220
|
+
props: {
|
|
221
|
+
title: {
|
|
222
|
+
value: "string",
|
|
223
|
+
scope: "const",
|
|
224
|
+
doc: "The heading above the diagram."
|
|
225
|
+
},
|
|
226
|
+
layout: {
|
|
227
|
+
value: "enum",
|
|
228
|
+
scope: "const",
|
|
229
|
+
values: ["right", "down"],
|
|
230
|
+
doc: "Which way the diagram runs. Narrow screens always run down."
|
|
231
|
+
},
|
|
232
|
+
speed: {
|
|
233
|
+
value: "number",
|
|
234
|
+
scope: "const",
|
|
235
|
+
doc: "Simulated seconds per real second. Default 10."
|
|
236
|
+
},
|
|
237
|
+
assume: {
|
|
238
|
+
value: "string",
|
|
239
|
+
scope: "const",
|
|
240
|
+
repeat: true,
|
|
241
|
+
doc: "A line added before the generated assumptions."
|
|
242
|
+
},
|
|
243
|
+
searchMax: {
|
|
244
|
+
value: "number",
|
|
245
|
+
scope: "const",
|
|
246
|
+
doc: "The highest load multiple the limits search. Default 2000."
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
children: [
|
|
250
|
+
"loadmap",
|
|
251
|
+
"load",
|
|
252
|
+
"param",
|
|
253
|
+
"preset",
|
|
254
|
+
"scenario",
|
|
255
|
+
"growth",
|
|
256
|
+
"expect",
|
|
257
|
+
"step",
|
|
258
|
+
"let",
|
|
259
|
+
"fit",
|
|
260
|
+
"flow",
|
|
261
|
+
"component",
|
|
262
|
+
"node",
|
|
263
|
+
"cluster",
|
|
264
|
+
"pool",
|
|
265
|
+
"thresholds"
|
|
266
|
+
],
|
|
267
|
+
edges: "edges"
|
|
268
|
+
},
|
|
269
|
+
loadmap: {
|
|
270
|
+
doc: "The first line: the version of the language this definition is written in.",
|
|
271
|
+
example: "loadmap 1",
|
|
272
|
+
args: [{
|
|
273
|
+
name: "version",
|
|
274
|
+
kind: "number"
|
|
275
|
+
}],
|
|
276
|
+
props: {},
|
|
277
|
+
children: []
|
|
278
|
+
},
|
|
279
|
+
load: {
|
|
280
|
+
doc: "The load, as a multiple of today. It drives the limits.",
|
|
281
|
+
example: "load \"Users, × today\" { max: 100; default: 10 }",
|
|
282
|
+
args: [{
|
|
283
|
+
name: "label",
|
|
284
|
+
kind: "text",
|
|
285
|
+
optional: true
|
|
286
|
+
}],
|
|
287
|
+
props: {
|
|
288
|
+
min: {
|
|
289
|
+
value: "number",
|
|
290
|
+
scope: "const",
|
|
291
|
+
doc: "The lowest multiple. Default 1."
|
|
292
|
+
},
|
|
293
|
+
max: {
|
|
294
|
+
value: "number",
|
|
295
|
+
scope: "const",
|
|
296
|
+
doc: "The highest multiple on the slider.",
|
|
297
|
+
required: true
|
|
298
|
+
},
|
|
299
|
+
step: {
|
|
300
|
+
value: "number",
|
|
301
|
+
scope: "const",
|
|
302
|
+
doc: "The slider step. Default 1."
|
|
303
|
+
},
|
|
304
|
+
default: {
|
|
305
|
+
value: "number",
|
|
306
|
+
scope: "const",
|
|
307
|
+
doc: "Where the slider starts. Default the minimum."
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
children: []
|
|
311
|
+
},
|
|
312
|
+
param: {
|
|
313
|
+
doc: "A control: options make a select, min and max a slider, a true or false default a switch.",
|
|
314
|
+
example: "param pods \"Worker pods\" { options: [2, 3, 4, 6]; default: 3 }",
|
|
315
|
+
args: [{
|
|
316
|
+
name: "id",
|
|
317
|
+
kind: "id"
|
|
318
|
+
}, {
|
|
319
|
+
name: "label",
|
|
320
|
+
kind: "text",
|
|
321
|
+
optional: true
|
|
322
|
+
}],
|
|
323
|
+
props: {
|
|
324
|
+
options: {
|
|
325
|
+
value: "options",
|
|
326
|
+
scope: "const",
|
|
327
|
+
doc: "The choices of a select: [2, 3, 4] or [\"a\", \"b\"]."
|
|
328
|
+
},
|
|
329
|
+
min: {
|
|
330
|
+
value: "number",
|
|
331
|
+
scope: "const",
|
|
332
|
+
doc: "The lowest value of a slider."
|
|
333
|
+
},
|
|
334
|
+
max: {
|
|
335
|
+
value: "number",
|
|
336
|
+
scope: "const",
|
|
337
|
+
doc: "The highest value of a slider."
|
|
338
|
+
},
|
|
339
|
+
step: {
|
|
340
|
+
value: "number",
|
|
341
|
+
scope: "const",
|
|
342
|
+
doc: "The step of a slider. Default 1."
|
|
343
|
+
},
|
|
344
|
+
default: {
|
|
345
|
+
value: "value",
|
|
346
|
+
scope: "const",
|
|
347
|
+
doc: "The starting value.",
|
|
348
|
+
required: true
|
|
349
|
+
},
|
|
350
|
+
hint: {
|
|
351
|
+
value: "string",
|
|
352
|
+
scope: "const",
|
|
353
|
+
doc: "One line under the control."
|
|
354
|
+
},
|
|
355
|
+
tag: {
|
|
356
|
+
value: "string",
|
|
357
|
+
scope: "const",
|
|
358
|
+
doc: "A short tag next to the label, e.g. \"estimate\"."
|
|
359
|
+
},
|
|
360
|
+
enabled: {
|
|
361
|
+
value: "boolean",
|
|
362
|
+
scope: "values",
|
|
363
|
+
doc: "Greys the control out while this is false; it reads only parameters."
|
|
364
|
+
},
|
|
365
|
+
format: {
|
|
366
|
+
value: "string",
|
|
367
|
+
scope: "value",
|
|
368
|
+
doc: "How a value is shown, from `value`: `${value} pods`."
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
children: []
|
|
372
|
+
},
|
|
373
|
+
preset: {
|
|
374
|
+
doc: "A named set of values, offered as a button.",
|
|
375
|
+
example: "preset \"Black Friday\" { load: 40; pods: 6 }",
|
|
376
|
+
args: [{
|
|
377
|
+
name: "label",
|
|
378
|
+
kind: "text"
|
|
379
|
+
}],
|
|
380
|
+
props: {},
|
|
381
|
+
children: [],
|
|
382
|
+
open: {
|
|
383
|
+
names: "values",
|
|
384
|
+
spec: {
|
|
385
|
+
value: "value",
|
|
386
|
+
scope: "const",
|
|
387
|
+
doc: "A value of the load or a parameter."
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
scenario: {
|
|
392
|
+
doc: "A named design to compare: the defaults plus these values, never the load. The first is the baseline.",
|
|
393
|
+
example: "scenario \"Proposed\" { pods: 6; gateways: 2 }",
|
|
394
|
+
args: [{
|
|
395
|
+
name: "label",
|
|
396
|
+
kind: "text"
|
|
397
|
+
}],
|
|
398
|
+
props: {},
|
|
399
|
+
children: [],
|
|
400
|
+
open: {
|
|
401
|
+
names: "params",
|
|
402
|
+
spec: {
|
|
403
|
+
value: "value",
|
|
404
|
+
scope: "const",
|
|
405
|
+
doc: "A value of a parameter."
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
growth: {
|
|
410
|
+
doc: "How fast the load grows, compounded from asOf, where it is 1×: load multiples become dates.",
|
|
411
|
+
example: "growth { rate: 0.06; per: month; asOf: \"2026-11-01\" }",
|
|
412
|
+
args: [],
|
|
413
|
+
props: {
|
|
414
|
+
rate: {
|
|
415
|
+
value: "number",
|
|
416
|
+
scope: "const",
|
|
417
|
+
required: true,
|
|
418
|
+
doc: "Growth per period, as a share: 0.06 is 6 %."
|
|
419
|
+
},
|
|
420
|
+
per: {
|
|
421
|
+
value: "enum",
|
|
422
|
+
scope: "const",
|
|
423
|
+
values: ["month", "year"],
|
|
424
|
+
required: true,
|
|
425
|
+
doc: "month or year."
|
|
426
|
+
},
|
|
427
|
+
asOf: {
|
|
428
|
+
value: "string",
|
|
429
|
+
scope: "const",
|
|
430
|
+
required: true,
|
|
431
|
+
doc: "The day the load is 1×: \"2026-11-01\"."
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
children: []
|
|
435
|
+
},
|
|
436
|
+
expect: {
|
|
437
|
+
doc: "What must keep holding, on the pessimistic edge (every capacity at its low bound): `loadmap check` fails when it does not.",
|
|
438
|
+
example: "expect \"Today keeps 7×\" { target: 7 }",
|
|
439
|
+
args: [{
|
|
440
|
+
name: "label",
|
|
441
|
+
kind: "text",
|
|
442
|
+
optional: true
|
|
443
|
+
}],
|
|
444
|
+
props: {
|
|
445
|
+
scenario: {
|
|
446
|
+
value: "scenario",
|
|
447
|
+
scope: "const",
|
|
448
|
+
doc: "The scenario whose values it checks; the defaults without one."
|
|
449
|
+
},
|
|
450
|
+
component: {
|
|
451
|
+
value: "component",
|
|
452
|
+
scope: "const",
|
|
453
|
+
doc: "One component; the first limit without one."
|
|
454
|
+
},
|
|
455
|
+
target: {
|
|
456
|
+
value: "number",
|
|
457
|
+
scope: "const",
|
|
458
|
+
doc: "The target holds up to at least this load multiple."
|
|
459
|
+
},
|
|
460
|
+
full: {
|
|
461
|
+
value: "number",
|
|
462
|
+
scope: "const",
|
|
463
|
+
doc: "Nothing (or the component) runs out before this load multiple."
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
children: []
|
|
467
|
+
},
|
|
468
|
+
step: {
|
|
469
|
+
doc: "A step of the story the page walks through, in order: its text, and the values to show it at. `loadmap record --story` plays it.",
|
|
470
|
+
example: "step \"At 30×, the workers run out first.\" { load: 30; focus: workers }",
|
|
471
|
+
args: [{
|
|
472
|
+
name: "label",
|
|
473
|
+
kind: "text"
|
|
474
|
+
}],
|
|
475
|
+
props: {
|
|
476
|
+
scenario: {
|
|
477
|
+
value: "scenario",
|
|
478
|
+
scope: "const",
|
|
479
|
+
doc: "Start from this scenario's values; without one, from the defaults."
|
|
480
|
+
},
|
|
481
|
+
focus: {
|
|
482
|
+
value: "component",
|
|
483
|
+
scope: "const",
|
|
484
|
+
doc: "The component to point at: its box is outlined."
|
|
485
|
+
},
|
|
486
|
+
seconds: {
|
|
487
|
+
value: "number",
|
|
488
|
+
scope: "const",
|
|
489
|
+
doc: "How long a recording holds the step. Default 4."
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
children: [],
|
|
493
|
+
open: {
|
|
494
|
+
names: "values",
|
|
495
|
+
spec: {
|
|
496
|
+
value: "value",
|
|
497
|
+
scope: "const",
|
|
498
|
+
doc: "A value of the load or a parameter."
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
let: {
|
|
503
|
+
doc: "A named formula other formulas can use by its name.",
|
|
504
|
+
example: "let perPosition = watchOnly ? 0.3 : 1.2",
|
|
505
|
+
args: [{
|
|
506
|
+
name: "id",
|
|
507
|
+
kind: "id"
|
|
508
|
+
}],
|
|
509
|
+
init: true,
|
|
510
|
+
props: {},
|
|
511
|
+
children: []
|
|
512
|
+
},
|
|
513
|
+
fit: {
|
|
514
|
+
doc: "cost = a + b × x, fitted from measured points. Call it like a function: workerCores(rate).",
|
|
515
|
+
example: "fit workerCores \"Worker CPU per request\" { source: \"load test\"; points rate cores { 200 0.26 } }",
|
|
516
|
+
args: [{
|
|
517
|
+
name: "id",
|
|
518
|
+
kind: "id"
|
|
519
|
+
}, {
|
|
520
|
+
name: "label",
|
|
521
|
+
kind: "text",
|
|
522
|
+
optional: true
|
|
523
|
+
}],
|
|
524
|
+
props: {
|
|
525
|
+
source: {
|
|
526
|
+
value: "string",
|
|
527
|
+
scope: "const",
|
|
528
|
+
doc: "Where the points came from."
|
|
529
|
+
},
|
|
530
|
+
through: {
|
|
531
|
+
value: "enum",
|
|
532
|
+
scope: "const",
|
|
533
|
+
values: ["origin"],
|
|
534
|
+
doc: "origin: a cost with no idle part (a = 0)."
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
children: ["points"]
|
|
538
|
+
},
|
|
539
|
+
points: {
|
|
540
|
+
doc: "The measured points of a fit: the names of x and y, then one \"x y\" row per line.",
|
|
541
|
+
example: "points rate cores {\n 200 0.2599\n 350 0.3061\n}",
|
|
542
|
+
args: [{
|
|
543
|
+
name: "x",
|
|
544
|
+
kind: "id"
|
|
545
|
+
}, {
|
|
546
|
+
name: "y",
|
|
547
|
+
kind: "id"
|
|
548
|
+
}],
|
|
549
|
+
props: {},
|
|
550
|
+
children: [],
|
|
551
|
+
rows: true
|
|
552
|
+
},
|
|
553
|
+
flow: {
|
|
554
|
+
doc: "Something that moves through the system, with its rate and its route.",
|
|
555
|
+
example: "flow orders \"orders\" { unit: \"order\"; rate: 40 * load; route: gateway -> topic -> workers }",
|
|
556
|
+
args: [{
|
|
557
|
+
name: "id",
|
|
558
|
+
kind: "id"
|
|
559
|
+
}, {
|
|
560
|
+
name: "label",
|
|
561
|
+
kind: "text",
|
|
562
|
+
optional: true
|
|
563
|
+
}],
|
|
564
|
+
props: {
|
|
565
|
+
unit: {
|
|
566
|
+
value: "string",
|
|
567
|
+
scope: "const",
|
|
568
|
+
doc: "One unit of the flow, singular: \"order\"."
|
|
569
|
+
},
|
|
570
|
+
rate: {
|
|
571
|
+
value: "number",
|
|
572
|
+
scope: "ctx",
|
|
573
|
+
doc: "Units per second.",
|
|
574
|
+
required: true
|
|
575
|
+
},
|
|
576
|
+
priority: {
|
|
577
|
+
value: "number",
|
|
578
|
+
scope: "const",
|
|
579
|
+
doc: "1 is served first. Flows of the same priority share."
|
|
580
|
+
},
|
|
581
|
+
route: {
|
|
582
|
+
value: "route",
|
|
583
|
+
scope: "const",
|
|
584
|
+
doc: "The stages it passes, in order: a -> b -> c.",
|
|
585
|
+
required: true
|
|
586
|
+
},
|
|
587
|
+
from: {
|
|
588
|
+
value: "node",
|
|
589
|
+
scope: "const",
|
|
590
|
+
doc: "The node its dots start from.",
|
|
591
|
+
required: true
|
|
592
|
+
},
|
|
593
|
+
color: {
|
|
594
|
+
value: "enum",
|
|
595
|
+
scope: "const",
|
|
596
|
+
values: COLORS,
|
|
597
|
+
doc: "A theme colour (accent, alt, ok, warn, drop, muted) or a CSS colour in quotes."
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
children: ["drop", "merge"]
|
|
601
|
+
},
|
|
602
|
+
drop: {
|
|
603
|
+
doc: "Units older than olderThan simulated seconds are dropped before they are handled.",
|
|
604
|
+
example: "drop { olderThan: 60; reason: \"too old\" }",
|
|
605
|
+
args: [],
|
|
606
|
+
props: {
|
|
607
|
+
olderThan: {
|
|
608
|
+
value: "number",
|
|
609
|
+
scope: "ctx",
|
|
610
|
+
doc: "Seconds.",
|
|
611
|
+
required: true
|
|
612
|
+
},
|
|
613
|
+
reason: {
|
|
614
|
+
value: "string",
|
|
615
|
+
scope: "ctx",
|
|
616
|
+
doc: "Shown with the drop meter."
|
|
617
|
+
},
|
|
618
|
+
when: OVERLOAD_WHEN
|
|
619
|
+
},
|
|
620
|
+
children: []
|
|
621
|
+
},
|
|
622
|
+
merge: {
|
|
623
|
+
doc: "Under backlog, this share of what is handled is superseded by a newer unit: keep the newest per key.",
|
|
624
|
+
example: "merge { share: 0.37; reason: \"newest kept\" }",
|
|
625
|
+
args: [],
|
|
626
|
+
props: {
|
|
627
|
+
share: {
|
|
628
|
+
value: "number",
|
|
629
|
+
scope: "ctx",
|
|
630
|
+
doc: "The share superseded, 0 to 1.",
|
|
631
|
+
required: true
|
|
632
|
+
},
|
|
633
|
+
reason: {
|
|
634
|
+
value: "string",
|
|
635
|
+
scope: "ctx",
|
|
636
|
+
doc: "Shown with the meter."
|
|
637
|
+
},
|
|
638
|
+
when: OVERLOAD_WHEN
|
|
639
|
+
},
|
|
640
|
+
children: []
|
|
641
|
+
},
|
|
642
|
+
component: {
|
|
643
|
+
doc: "Something that can run out: its demand against its capacity. On a box, write these lines on the box.",
|
|
644
|
+
example: "component cache \"Cache\" { demand: flows.reads; capacity: estimated(9000, \"req/s\") }",
|
|
645
|
+
args: [{
|
|
646
|
+
name: "id",
|
|
647
|
+
kind: "id",
|
|
648
|
+
optional: true
|
|
649
|
+
}, {
|
|
650
|
+
name: "label",
|
|
651
|
+
kind: "text",
|
|
652
|
+
optional: true
|
|
653
|
+
}],
|
|
654
|
+
props: COMPONENT_PROPS,
|
|
655
|
+
children: []
|
|
656
|
+
},
|
|
657
|
+
node: {
|
|
658
|
+
doc: "A box. It may run a stage (pass, store, sink, fanout) and own a component (demand and capacity lines).",
|
|
659
|
+
example: "node gateway \"API gateway\" { pass; demand: flows.orders; capacity: estimated(20000, \"req/s\") }",
|
|
660
|
+
args: BOX_ARGS,
|
|
661
|
+
props: {
|
|
662
|
+
sub: SUB,
|
|
663
|
+
shape: {
|
|
664
|
+
value: "enum",
|
|
665
|
+
scope: "const",
|
|
666
|
+
values: ["box", "cylinder"],
|
|
667
|
+
doc: "box (the default) or cylinder."
|
|
668
|
+
},
|
|
669
|
+
...COMPONENT_PROPS
|
|
670
|
+
},
|
|
671
|
+
children: [
|
|
672
|
+
"pass",
|
|
673
|
+
"store",
|
|
674
|
+
"sink",
|
|
675
|
+
"fanout",
|
|
676
|
+
"component"
|
|
677
|
+
]
|
|
678
|
+
},
|
|
679
|
+
cluster: {
|
|
680
|
+
doc: "Members (brokers) holding the partitions of one or more queues (topics).",
|
|
681
|
+
example: "cluster kafka \"Kafka\" { members: 3; queue topic { partitions { orders: 12 } } }",
|
|
682
|
+
args: BOX_ARGS,
|
|
683
|
+
props: {
|
|
684
|
+
sub: SUB,
|
|
685
|
+
members: {
|
|
686
|
+
value: "number",
|
|
687
|
+
scope: "ctx",
|
|
688
|
+
doc: "How many members.",
|
|
689
|
+
required: true
|
|
690
|
+
},
|
|
691
|
+
memberLabel: {
|
|
692
|
+
value: "string",
|
|
693
|
+
scope: "i",
|
|
694
|
+
doc: "Each member's label, from its index i: `broker ${i}`."
|
|
695
|
+
},
|
|
696
|
+
...COMPONENT_PROPS
|
|
697
|
+
},
|
|
698
|
+
children: ["queue", "component"]
|
|
699
|
+
},
|
|
700
|
+
pool: {
|
|
701
|
+
doc: "A workers stage drawn as pods holding a grid of workers.",
|
|
702
|
+
example: "pool workers \"Workers\" { workers { pods: pods; perPod: 8 } }",
|
|
703
|
+
args: BOX_ARGS,
|
|
704
|
+
props: {
|
|
705
|
+
sub: SUB,
|
|
706
|
+
podLabel: {
|
|
707
|
+
value: "string",
|
|
708
|
+
scope: "i",
|
|
709
|
+
doc: "Each pod's label, from its index i: `pod ${i}`."
|
|
710
|
+
},
|
|
711
|
+
...COMPONENT_PROPS
|
|
712
|
+
},
|
|
713
|
+
children: ["workers", "component"]
|
|
714
|
+
},
|
|
715
|
+
pass: {
|
|
716
|
+
doc: "Flows pass through; past capacity the share above 100 % is rejected.",
|
|
717
|
+
example: "pass \"the gateway\"",
|
|
718
|
+
args: STAGE_LABEL,
|
|
719
|
+
props: { component: STAGE_COMPONENT },
|
|
720
|
+
children: []
|
|
721
|
+
},
|
|
722
|
+
store: {
|
|
723
|
+
doc: "Where a flow ends: written to its component.",
|
|
724
|
+
example: "store",
|
|
725
|
+
args: STAGE_LABEL,
|
|
726
|
+
props: { component: STAGE_COMPONENT },
|
|
727
|
+
children: []
|
|
728
|
+
},
|
|
729
|
+
sink: {
|
|
730
|
+
doc: "Where a flow ends without a component: a receiver, a viewer.",
|
|
731
|
+
example: "sink \"contacts\"",
|
|
732
|
+
args: STAGE_LABEL,
|
|
733
|
+
props: {},
|
|
734
|
+
children: []
|
|
735
|
+
},
|
|
736
|
+
queue: {
|
|
737
|
+
doc: "Flows wait here; the next stage of each route drains it.",
|
|
738
|
+
example: "queue topic \"orders topic\" { partitions { orders: 12 } }",
|
|
739
|
+
args: STAGE_LABEL,
|
|
740
|
+
props: { component: STAGE_COMPONENT },
|
|
741
|
+
children: ["partitions"]
|
|
742
|
+
},
|
|
743
|
+
partitions: {
|
|
744
|
+
doc: "Partitions per flow, drawn as cells across the members.",
|
|
745
|
+
example: "partitions { orders: 12; events: 24 }",
|
|
746
|
+
args: [],
|
|
747
|
+
props: {},
|
|
748
|
+
children: [],
|
|
749
|
+
open: {
|
|
750
|
+
names: "flows",
|
|
751
|
+
spec: {
|
|
752
|
+
value: "number",
|
|
753
|
+
scope: "ctx",
|
|
754
|
+
doc: "Partitions of this flow."
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
workers: {
|
|
759
|
+
doc: "A pool of workers: pods × workers per pod.",
|
|
760
|
+
example: "workers { pods: pods; perPod: 8 }",
|
|
761
|
+
args: STAGE_LABEL,
|
|
762
|
+
props: {
|
|
763
|
+
pods: {
|
|
764
|
+
value: "number",
|
|
765
|
+
scope: "ctx",
|
|
766
|
+
doc: "How many pods.",
|
|
767
|
+
required: true
|
|
768
|
+
},
|
|
769
|
+
perPod: {
|
|
770
|
+
value: "number",
|
|
771
|
+
scope: "ctx",
|
|
772
|
+
doc: "Workers per pod.",
|
|
773
|
+
required: true
|
|
774
|
+
},
|
|
775
|
+
fill: {
|
|
776
|
+
value: "enum",
|
|
777
|
+
scope: "const",
|
|
778
|
+
values: ["round-robin", "in-order"],
|
|
779
|
+
doc: "round-robin spreads work across pods (the default); in-order fills pod 0 first."
|
|
780
|
+
},
|
|
781
|
+
component: STAGE_COMPONENT
|
|
782
|
+
},
|
|
783
|
+
children: []
|
|
784
|
+
},
|
|
785
|
+
fanout: {
|
|
786
|
+
doc: "Each unit is copied to several stages; one \"flow -> stage: copies\" line per branch. Last on a route.",
|
|
787
|
+
example: "fanout \"push broker\" {\n alerts -> contacts: 1\n}",
|
|
788
|
+
args: STAGE_LABEL,
|
|
789
|
+
props: { component: STAGE_COMPONENT },
|
|
790
|
+
children: [],
|
|
791
|
+
edges: "branches"
|
|
792
|
+
},
|
|
793
|
+
thresholds: {
|
|
794
|
+
doc: "The utilization of the target and of \"runs out\".",
|
|
795
|
+
example: "thresholds { target: 0.5; full: 1 }",
|
|
796
|
+
args: [],
|
|
797
|
+
props: {
|
|
798
|
+
target: {
|
|
799
|
+
value: "number",
|
|
800
|
+
scope: "const",
|
|
801
|
+
doc: "Default 0.5."
|
|
802
|
+
},
|
|
803
|
+
full: {
|
|
804
|
+
value: "number",
|
|
805
|
+
scope: "const",
|
|
806
|
+
doc: "Default 1."
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
children: []
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
/** The properties of an edge's block: `a -> b { label: …; when: … }`. */
|
|
813
|
+
const EDGE_PROPS = {
|
|
814
|
+
label: {
|
|
815
|
+
value: "string",
|
|
816
|
+
scope: "ctx",
|
|
817
|
+
doc: "The text on the edge."
|
|
818
|
+
},
|
|
819
|
+
when: {
|
|
820
|
+
value: "boolean",
|
|
821
|
+
scope: "ctx",
|
|
822
|
+
doc: "The edge is left out while this is false."
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
const BOXES = [
|
|
826
|
+
"node",
|
|
827
|
+
"cluster",
|
|
828
|
+
"pool"
|
|
829
|
+
];
|
|
830
|
+
const STAGES = [
|
|
831
|
+
"pass",
|
|
832
|
+
"store",
|
|
833
|
+
"sink",
|
|
834
|
+
"queue",
|
|
835
|
+
"workers",
|
|
836
|
+
"fanout"
|
|
837
|
+
];
|
|
838
|
+
// src/expr.ts
|
|
839
|
+
/** A formula failed while the model ran: where, and why. */
|
|
840
|
+
var FormulaError = class extends Error {
|
|
841
|
+
span;
|
|
842
|
+
constructor(span, message) {
|
|
843
|
+
super(message);
|
|
844
|
+
this.span = span;
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
const num = (f) => f;
|
|
848
|
+
/** Functions renamed before 0.1: a definition from before says what to write instead. */
|
|
849
|
+
const RENAMED = { kfmt: "compact" };
|
|
850
|
+
const BUILTINS = {
|
|
851
|
+
min: {
|
|
852
|
+
args: ["number"],
|
|
853
|
+
variadic: true,
|
|
854
|
+
min: 1,
|
|
855
|
+
result: "number",
|
|
856
|
+
fn: num(Math.min)
|
|
857
|
+
},
|
|
858
|
+
max: {
|
|
859
|
+
args: ["number"],
|
|
860
|
+
variadic: true,
|
|
861
|
+
min: 1,
|
|
862
|
+
result: "number",
|
|
863
|
+
fn: num(Math.max)
|
|
864
|
+
},
|
|
865
|
+
round: {
|
|
866
|
+
args: ["number"],
|
|
867
|
+
result: "number",
|
|
868
|
+
fn: num(Math.round)
|
|
869
|
+
},
|
|
870
|
+
floor: {
|
|
871
|
+
args: ["number"],
|
|
872
|
+
result: "number",
|
|
873
|
+
fn: num(Math.floor)
|
|
874
|
+
},
|
|
875
|
+
ceil: {
|
|
876
|
+
args: ["number"],
|
|
877
|
+
result: "number",
|
|
878
|
+
fn: num(Math.ceil)
|
|
879
|
+
},
|
|
880
|
+
abs: {
|
|
881
|
+
args: ["number"],
|
|
882
|
+
result: "number",
|
|
883
|
+
fn: num(Math.abs)
|
|
884
|
+
},
|
|
885
|
+
sqrt: {
|
|
886
|
+
args: ["number"],
|
|
887
|
+
result: "number",
|
|
888
|
+
fn: num(Math.sqrt)
|
|
889
|
+
},
|
|
890
|
+
log: {
|
|
891
|
+
args: ["number"],
|
|
892
|
+
result: "number",
|
|
893
|
+
fn: num(Math.log)
|
|
894
|
+
},
|
|
895
|
+
exp: {
|
|
896
|
+
args: ["number"],
|
|
897
|
+
result: "number",
|
|
898
|
+
fn: num(Math.exp)
|
|
899
|
+
},
|
|
900
|
+
pow: {
|
|
901
|
+
args: ["number", "number"],
|
|
902
|
+
result: "number",
|
|
903
|
+
fn: num(Math.pow)
|
|
904
|
+
},
|
|
905
|
+
fmt: {
|
|
906
|
+
args: ["number"],
|
|
907
|
+
result: "string",
|
|
908
|
+
fn: fmt
|
|
909
|
+
},
|
|
910
|
+
compact: {
|
|
911
|
+
args: ["number"],
|
|
912
|
+
result: "string",
|
|
913
|
+
fn: compact
|
|
914
|
+
},
|
|
915
|
+
pct: {
|
|
916
|
+
args: ["number"],
|
|
917
|
+
result: "string",
|
|
918
|
+
fn: pct
|
|
919
|
+
},
|
|
920
|
+
secs: {
|
|
921
|
+
args: ["number"],
|
|
922
|
+
result: "string",
|
|
923
|
+
fn: secs
|
|
924
|
+
},
|
|
925
|
+
measured: {
|
|
926
|
+
args: [
|
|
927
|
+
"number",
|
|
928
|
+
"string",
|
|
929
|
+
"string"
|
|
930
|
+
],
|
|
931
|
+
min: 2,
|
|
932
|
+
result: "capacity",
|
|
933
|
+
fn: measured
|
|
934
|
+
},
|
|
935
|
+
estimated: {
|
|
936
|
+
args: [
|
|
937
|
+
"number",
|
|
938
|
+
"string",
|
|
939
|
+
"string"
|
|
940
|
+
],
|
|
941
|
+
min: 2,
|
|
942
|
+
result: "capacity",
|
|
943
|
+
fn: estimated
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
/** The tables a formula may read with a ".": flows.orders, demand.db, util.db, derived.x. */
|
|
947
|
+
const TABLES = [
|
|
948
|
+
"flows",
|
|
949
|
+
"demand",
|
|
950
|
+
"util",
|
|
951
|
+
"derived"
|
|
952
|
+
];
|
|
953
|
+
/** Names a declaration cannot take: the tables, the locals, the literals and what plain objects already have. */
|
|
954
|
+
const RESERVED = /* @__PURE__ */ new Set([
|
|
955
|
+
"load",
|
|
956
|
+
...TABLES,
|
|
957
|
+
"i",
|
|
958
|
+
"value",
|
|
959
|
+
"true",
|
|
960
|
+
"false",
|
|
961
|
+
"Math",
|
|
962
|
+
...Object.keys(BUILTINS),
|
|
963
|
+
...Object.getOwnPropertyNames(Object.prototype),
|
|
964
|
+
"prototype"
|
|
965
|
+
]);
|
|
966
|
+
const WORD = {
|
|
967
|
+
number: "a number",
|
|
968
|
+
boolean: "true or false",
|
|
969
|
+
string: "a text",
|
|
970
|
+
capacity: "a capacity",
|
|
971
|
+
any: "a value"
|
|
972
|
+
};
|
|
973
|
+
const typeWord = (t) => WORD[t];
|
|
974
|
+
/**
|
|
975
|
+
* Checks a formula against what it may read (`scope`) and compiles it. Errors are reported; the result then has type
|
|
976
|
+
* `any` and returns undefined, so checking goes on.
|
|
977
|
+
*/
|
|
978
|
+
function compileFormula(e, names, scope, report) {
|
|
979
|
+
return new Compiler$1(names, scope, report).compile(e);
|
|
980
|
+
}
|
|
981
|
+
const bad = {
|
|
982
|
+
type: "any",
|
|
983
|
+
constant: true,
|
|
984
|
+
run: () => void 0
|
|
985
|
+
};
|
|
986
|
+
var Compiler$1 = class {
|
|
987
|
+
names;
|
|
988
|
+
scope;
|
|
989
|
+
report;
|
|
990
|
+
constructor(names, scope, report) {
|
|
991
|
+
this.names = names;
|
|
992
|
+
this.scope = scope;
|
|
993
|
+
this.report = report;
|
|
994
|
+
}
|
|
995
|
+
canRead(e, what) {
|
|
996
|
+
if (this.scope === "ctx" || this.scope === "values" && what === "a parameter") return true;
|
|
997
|
+
const here = this.scope === "const" ? "this value must be a constant" : this.scope === "values" ? "this formula reads only parameters" : `this formula reads only "${this.scope}"`;
|
|
998
|
+
this.report.error(e, `${here}; it cannot read ${what}`);
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
compile(e) {
|
|
1002
|
+
switch (e.t) {
|
|
1003
|
+
case "error": return bad;
|
|
1004
|
+
case "num":
|
|
1005
|
+
case "str":
|
|
1006
|
+
case "bool": {
|
|
1007
|
+
const v = e.value;
|
|
1008
|
+
return {
|
|
1009
|
+
type: e.t === "num" ? "number" : e.t === "str" ? "string" : "boolean",
|
|
1010
|
+
constant: true,
|
|
1011
|
+
run: () => v
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
case "tpl": {
|
|
1015
|
+
const parts = e.parts.map((p) => this.compile(p));
|
|
1016
|
+
const { quasis } = e;
|
|
1017
|
+
const runs = parts.map((p) => p.run);
|
|
1018
|
+
return {
|
|
1019
|
+
type: "string",
|
|
1020
|
+
constant: parts.every((p) => p.constant),
|
|
1021
|
+
run: (c, l) => {
|
|
1022
|
+
let s = quasis[0];
|
|
1023
|
+
for (let i = 0; i < runs.length; i++) s += String(runs[i](c, l)) + quasis[i + 1];
|
|
1024
|
+
return s;
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
case "id": return this.name(e, e.name);
|
|
1029
|
+
case "member": return this.member(e);
|
|
1030
|
+
case "call": return this.call(e);
|
|
1031
|
+
case "unary": {
|
|
1032
|
+
const a = this.compile(e.arg);
|
|
1033
|
+
const run = a.run;
|
|
1034
|
+
if (e.op === "!") return {
|
|
1035
|
+
type: "boolean",
|
|
1036
|
+
constant: a.constant,
|
|
1037
|
+
run: (c, l) => !run(c, l)
|
|
1038
|
+
};
|
|
1039
|
+
this.want(e.arg, a, "number");
|
|
1040
|
+
return {
|
|
1041
|
+
type: "number",
|
|
1042
|
+
constant: a.constant,
|
|
1043
|
+
run: e.op === "-" ? (c, l) => -run(c, l) : run
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
case "bin": return this.binary(e);
|
|
1047
|
+
case "cond": {
|
|
1048
|
+
const test = this.compile(e.test);
|
|
1049
|
+
const then = this.compile(e.then);
|
|
1050
|
+
const other = this.compile(e.else);
|
|
1051
|
+
const [t, a, b] = [
|
|
1052
|
+
test.run,
|
|
1053
|
+
then.run,
|
|
1054
|
+
other.run
|
|
1055
|
+
];
|
|
1056
|
+
return {
|
|
1057
|
+
type: then.type === other.type ? then.type : "any",
|
|
1058
|
+
constant: test.constant && then.constant && other.constant,
|
|
1059
|
+
run: (c, l) => t(c, l) ? a(c, l) : b(c, l)
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
case "array":
|
|
1063
|
+
this.report.error(e, "a list is only allowed as the options of a parameter");
|
|
1064
|
+
return bad;
|
|
1065
|
+
case "pm":
|
|
1066
|
+
this.report.error(e, "\"±\" only goes in the first value of measured(…) or estimated(…)");
|
|
1067
|
+
return bad;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
name(e, name) {
|
|
1071
|
+
if (name === "i" || name === "value") {
|
|
1072
|
+
if (this.scope !== name) {
|
|
1073
|
+
this.report.error(e, `"${name}" is only known in ${name === "i" ? "memberLabel and podLabel" : "format"}`);
|
|
1074
|
+
return bad;
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
type: name === "i" ? "number" : "any",
|
|
1078
|
+
constant: false,
|
|
1079
|
+
run: (_, l) => l
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
const param = this.names.params.get(name);
|
|
1083
|
+
if (param) {
|
|
1084
|
+
if (!this.canRead(e, "a parameter")) return bad;
|
|
1085
|
+
return {
|
|
1086
|
+
type: param,
|
|
1087
|
+
constant: false,
|
|
1088
|
+
run: (c) => c[name]
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
const let_ = this.names.lets.get(name);
|
|
1092
|
+
if (let_) {
|
|
1093
|
+
if (!this.canRead(e, `"${name}"`)) return bad;
|
|
1094
|
+
return {
|
|
1095
|
+
type: let_,
|
|
1096
|
+
constant: false,
|
|
1097
|
+
run: (c) => c.derived[name]
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
if (TABLES.includes(name)) {
|
|
1101
|
+
this.report.error(e, `"${name}" is a table: name one of its entries, like ${name}.${this.example(name)}`);
|
|
1102
|
+
return bad;
|
|
1103
|
+
}
|
|
1104
|
+
if (this.names.fits.has(name) || own(BUILTINS, name) || own(this.names.functions, name)) {
|
|
1105
|
+
this.report.error(e, `"${name}" is a function: call it, ${name}(…)`);
|
|
1106
|
+
return bad;
|
|
1107
|
+
}
|
|
1108
|
+
const known = [...this.names.params.keys(), ...this.names.lets.keys()];
|
|
1109
|
+
this.report.error(e, `unknown name "${name}"`, didYouMean(name, known));
|
|
1110
|
+
return bad;
|
|
1111
|
+
}
|
|
1112
|
+
example(table) {
|
|
1113
|
+
return [...table === "flows" ? this.names.flows : table === "derived" ? this.names.lets.keys() : this.names.components][0] ?? "x";
|
|
1114
|
+
}
|
|
1115
|
+
member(e) {
|
|
1116
|
+
const table = e.object.name;
|
|
1117
|
+
const key = e.prop.name;
|
|
1118
|
+
const pool = table === "flows" ? this.names.flows : table === "demand" || table === "util" ? this.names.components : table === "derived" ? this.names.lets.keys() : void 0;
|
|
1119
|
+
if (!pool) {
|
|
1120
|
+
this.report.error(e.object, `"${table}" has no entries: only flows., demand., util. and derived. do`);
|
|
1121
|
+
return bad;
|
|
1122
|
+
}
|
|
1123
|
+
if (!(table === "derived" ? this.names.lets.has(key) : pool.has(key))) {
|
|
1124
|
+
const what = table === "flows" ? "flow" : table === "derived" ? "let" : "component";
|
|
1125
|
+
this.report.error(e.prop, `unknown ${what} "${key}"`, didYouMean(key, pool));
|
|
1126
|
+
return bad;
|
|
1127
|
+
}
|
|
1128
|
+
if (!this.canRead(e, `${table}.${key}`)) return bad;
|
|
1129
|
+
return {
|
|
1130
|
+
type: table === "derived" ? this.names.lets.get(key) : "number",
|
|
1131
|
+
constant: false,
|
|
1132
|
+
run: (c) => c[table][key]
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
call(e) {
|
|
1136
|
+
const name = e.callee.name;
|
|
1137
|
+
const pm = e.args[0]?.t === "pm" ? e.args[0] : null;
|
|
1138
|
+
if (pm && (name === "measured" || name === "estimated")) return this.capacity(e, pm);
|
|
1139
|
+
const args = e.args.map((a) => this.compile(a));
|
|
1140
|
+
const runs = args.map((a) => a.run);
|
|
1141
|
+
const fit = this.names.fits.get(name);
|
|
1142
|
+
if (this.names.fits.has(name)) {
|
|
1143
|
+
if (args.length !== 1) this.report.error(e, `the fit "${name}" takes one number`);
|
|
1144
|
+
else this.want(e.args[0], args[0], "number");
|
|
1145
|
+
const a = runs[0] ?? (() => void 0);
|
|
1146
|
+
return {
|
|
1147
|
+
type: "number",
|
|
1148
|
+
constant: args.every((x) => x.constant),
|
|
1149
|
+
run: (c, l) => fit?.(a(c, l))
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
const b = own(BUILTINS, name);
|
|
1153
|
+
if (b) {
|
|
1154
|
+
const max = b.variadic ? Number.POSITIVE_INFINITY : b.args.length;
|
|
1155
|
+
const min = b.min ?? b.args.length;
|
|
1156
|
+
if (args.length < min || args.length > max) this.report.error(e, `${name}() takes ${min === max ? min : `${min} to ${max}`} values, not ${args.length}`);
|
|
1157
|
+
for (const [i, a] of args.entries()) this.want(e.args[i], a, b.args[Math.min(i, b.args.length - 1)]);
|
|
1158
|
+
const fn = b.fn;
|
|
1159
|
+
return {
|
|
1160
|
+
type: b.result,
|
|
1161
|
+
constant: args.every((a) => a.constant),
|
|
1162
|
+
run: (c, l) => fn(...runs.map((r) => r(c, l)))
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
const host = own(this.names.functions, name);
|
|
1166
|
+
if (host) return {
|
|
1167
|
+
type: "any",
|
|
1168
|
+
constant: false,
|
|
1169
|
+
run: (c, l) => host(...runs.map((r) => r(c, l)))
|
|
1170
|
+
};
|
|
1171
|
+
const known = [
|
|
1172
|
+
...Object.keys(BUILTINS),
|
|
1173
|
+
...this.names.fits.keys(),
|
|
1174
|
+
...Object.keys(this.names.functions)
|
|
1175
|
+
];
|
|
1176
|
+
this.report.error(e.callee, `unknown function "${name}"`, (own(RENAMED, name) ? `renamed to ${own(RENAMED, name)}()` : void 0) ?? didYouMean(name, known) ?? "a host page can provide it in compile(…, { functions })");
|
|
1177
|
+
return bad;
|
|
1178
|
+
}
|
|
1179
|
+
/** measured(value ± within[%], unit, source): the ± becomes the capacity's `within`, a share of the value. */
|
|
1180
|
+
capacity(e, pm) {
|
|
1181
|
+
const fn = e.callee.name === "measured" ? measured : estimated;
|
|
1182
|
+
const [value, within] = [this.compile(pm.value), this.compile(pm.within)];
|
|
1183
|
+
const rest = e.args.slice(1).map((a) => this.compile(a));
|
|
1184
|
+
this.want(pm.value, value, "number");
|
|
1185
|
+
this.want(pm.within, within, "number");
|
|
1186
|
+
if (rest.length < 1 || rest.length > 2) this.report.error(e, `${e.callee.name}() takes 2 to 3 values, not ${rest.length + 1}`);
|
|
1187
|
+
for (const [i, a] of rest.entries()) this.want(e.args[i + 1], a, "string");
|
|
1188
|
+
const [v, w] = [value.run, within.run];
|
|
1189
|
+
const [unit, source] = rest.map((a) => a.run);
|
|
1190
|
+
const { percent } = pm;
|
|
1191
|
+
return {
|
|
1192
|
+
type: "capacity",
|
|
1193
|
+
constant: [
|
|
1194
|
+
value,
|
|
1195
|
+
within,
|
|
1196
|
+
...rest
|
|
1197
|
+
].every((a) => a.constant),
|
|
1198
|
+
run: (c, l) => {
|
|
1199
|
+
const x = v(c, l);
|
|
1200
|
+
const pmv = w(c, l);
|
|
1201
|
+
const share = percent ? pmv / 100 : x ? pmv / x : 0;
|
|
1202
|
+
if (!(share >= 0 && share < 1)) {
|
|
1203
|
+
const said = percent ? `${pmv} %` : `${pmv} of ${x}`;
|
|
1204
|
+
throw new Error(`± ${said} is not a share from 0 to below 100 %`);
|
|
1205
|
+
}
|
|
1206
|
+
return fn(x, unit?.(c, l), {
|
|
1207
|
+
source: source?.(c, l),
|
|
1208
|
+
within: share
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
binary(e) {
|
|
1214
|
+
const l = this.compile(e.left);
|
|
1215
|
+
const r = this.compile(e.right);
|
|
1216
|
+
const [a, b] = [l.run, r.run];
|
|
1217
|
+
const constant = l.constant && r.constant;
|
|
1218
|
+
const f = (type, run) => ({
|
|
1219
|
+
type,
|
|
1220
|
+
constant,
|
|
1221
|
+
run
|
|
1222
|
+
});
|
|
1223
|
+
switch (e.op) {
|
|
1224
|
+
case "||": return f(l.type === r.type ? l.type : "any", (c, x) => a(c, x) || b(c, x));
|
|
1225
|
+
case "&&": return f(l.type === r.type ? l.type : "any", (c, x) => a(c, x) && b(c, x));
|
|
1226
|
+
case "==": return f("boolean", (c, x) => a(c, x) === b(c, x));
|
|
1227
|
+
case "!=": return f("boolean", (c, x) => a(c, x) !== b(c, x));
|
|
1228
|
+
case "<": return f("boolean", (c, x) => a(c, x) < b(c, x));
|
|
1229
|
+
case "<=": return f("boolean", (c, x) => a(c, x) <= b(c, x));
|
|
1230
|
+
case ">": return f("boolean", (c, x) => a(c, x) > b(c, x));
|
|
1231
|
+
case ">=": return f("boolean", (c, x) => a(c, x) >= b(c, x));
|
|
1232
|
+
case "+": {
|
|
1233
|
+
const type = l.type === "string" || r.type === "string" ? "string" : l.type === "number" && r.type === "number" ? "number" : "any";
|
|
1234
|
+
if (type !== "string") {
|
|
1235
|
+
this.want(e.left, l, "number");
|
|
1236
|
+
this.want(e.right, r, "number");
|
|
1237
|
+
}
|
|
1238
|
+
return f(type, (c, x) => a(c, x) + b(c, x));
|
|
1239
|
+
}
|
|
1240
|
+
default: {
|
|
1241
|
+
this.want(e.left, l, "number");
|
|
1242
|
+
this.want(e.right, r, "number");
|
|
1243
|
+
const op = e.op;
|
|
1244
|
+
return f("number", (c, x) => {
|
|
1245
|
+
const u = a(c, x);
|
|
1246
|
+
const v = b(c, x);
|
|
1247
|
+
return op === "-" ? u - v : op === "*" ? u * v : op === "/" ? u / v : u % v;
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
want(at, f, type) {
|
|
1253
|
+
if (f.type !== type && f.type !== "any" && type !== "any") this.report.error(at, `expected ${typeWord(type)}, this is ${typeWord(f.type)}`);
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
/**
|
|
1257
|
+
* Wraps a formula for the model: it takes the context, and any error it throws carries the formula's place. An error
|
|
1258
|
+
* already placed (from a formula this one read) keeps its place: the innermost formula is the one to fix.
|
|
1259
|
+
*/
|
|
1260
|
+
function placed(span, run) {
|
|
1261
|
+
return (ctx, local) => {
|
|
1262
|
+
try {
|
|
1263
|
+
return run(ctx, local);
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
if (err instanceof FormulaError) throw err;
|
|
1266
|
+
throw new FormulaError(span, err instanceof Error ? err.message.replace(/^loadmap: /, "") : String(err));
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
const isCapacity = (v) => typeof v === "object" && v != null && "provenance" in v && "value" in v;
|
|
1271
|
+
// src/lexer.ts
|
|
1272
|
+
const PUNCT = [
|
|
1273
|
+
"===",
|
|
1274
|
+
"!==",
|
|
1275
|
+
"+/-",
|
|
1276
|
+
"->",
|
|
1277
|
+
"==",
|
|
1278
|
+
"!=",
|
|
1279
|
+
"<=",
|
|
1280
|
+
">=",
|
|
1281
|
+
"&&",
|
|
1282
|
+
"||",
|
|
1283
|
+
..."{}()[],;:.?+-*/%<>!=±"
|
|
1284
|
+
];
|
|
1285
|
+
/** The token kinds and marks an editor needs to highlight the language like the parser reads it. */
|
|
1286
|
+
const LEXICON = {
|
|
1287
|
+
punct: PUNCT,
|
|
1288
|
+
comment: "#"
|
|
1289
|
+
};
|
|
1290
|
+
/**
|
|
1291
|
+
* Splits `source[from, to)` into tokens. Newlines end statements, except inside parentheses and brackets, so a long
|
|
1292
|
+
* formula can wrap there. `#` starts a comment to the end of the line.
|
|
1293
|
+
*/
|
|
1294
|
+
function lex(source, report, from = 0, to = source.length) {
|
|
1295
|
+
const out = [];
|
|
1296
|
+
let depth = 0;
|
|
1297
|
+
let i = from;
|
|
1298
|
+
const push = (t) => out.push(t);
|
|
1299
|
+
while (i < to) {
|
|
1300
|
+
const c = source[i];
|
|
1301
|
+
if (c === "\n") {
|
|
1302
|
+
if (depth === 0 && out.at(-1)?.kind !== "newline") push({
|
|
1303
|
+
kind: "newline",
|
|
1304
|
+
text: "\n",
|
|
1305
|
+
from: i,
|
|
1306
|
+
to: i + 1
|
|
1307
|
+
});
|
|
1308
|
+
i++;
|
|
1309
|
+
} else if (c === " " || c === " " || c === "\r") i++;
|
|
1310
|
+
else if (c === "#") while (i < to && source[i] !== "\n") i++;
|
|
1311
|
+
else if (isIdStart(c)) {
|
|
1312
|
+
const s = i;
|
|
1313
|
+
while (i < to && isIdPart(source[i])) i++;
|
|
1314
|
+
push({
|
|
1315
|
+
kind: "ident",
|
|
1316
|
+
text: source.slice(s, i),
|
|
1317
|
+
from: s,
|
|
1318
|
+
to: i
|
|
1319
|
+
});
|
|
1320
|
+
} else if (isDigit(c) || c === "." && isDigit(source[i + 1] ?? "")) {
|
|
1321
|
+
const s = i;
|
|
1322
|
+
while (i < to && (isDigit(source[i]) || source[i] === "_")) i++;
|
|
1323
|
+
if (source[i] === "." && isDigit(source[i + 1] ?? "")) {
|
|
1324
|
+
i++;
|
|
1325
|
+
while (i < to && (isDigit(source[i]) || source[i] === "_")) i++;
|
|
1326
|
+
}
|
|
1327
|
+
const sign = source[i + 1] === "+" || source[i + 1] === "-" ? 1 : 0;
|
|
1328
|
+
if ((source[i] === "e" || source[i] === "E") && isDigit(source[i + 1 + sign] ?? "")) {
|
|
1329
|
+
i += 1 + sign;
|
|
1330
|
+
while (i < to && isDigit(source[i])) i++;
|
|
1331
|
+
}
|
|
1332
|
+
const text = source.slice(s, i);
|
|
1333
|
+
if (text.endsWith("_") || text.includes("__")) report.error({
|
|
1334
|
+
from: s,
|
|
1335
|
+
to: i
|
|
1336
|
+
}, `"${text}" is not a number`);
|
|
1337
|
+
push({
|
|
1338
|
+
kind: "number",
|
|
1339
|
+
text,
|
|
1340
|
+
value: Number(text.replaceAll("_", "")),
|
|
1341
|
+
from: s,
|
|
1342
|
+
to: i
|
|
1343
|
+
});
|
|
1344
|
+
} else if (c === "\"" || c === "'") {
|
|
1345
|
+
const s = i;
|
|
1346
|
+
const r = readString(source, i + 1, to, c);
|
|
1347
|
+
if (!r.closed) report.error({
|
|
1348
|
+
from: s,
|
|
1349
|
+
to: r.end
|
|
1350
|
+
}, "this text has no closing quote", `end it with ${c}`);
|
|
1351
|
+
i = r.end;
|
|
1352
|
+
push({
|
|
1353
|
+
kind: "string",
|
|
1354
|
+
text: source.slice(s, i),
|
|
1355
|
+
value: r.value,
|
|
1356
|
+
from: s,
|
|
1357
|
+
to: i
|
|
1358
|
+
});
|
|
1359
|
+
} else if (c === "`") {
|
|
1360
|
+
const s = i;
|
|
1361
|
+
const t = readTemplate(source, i + 1, to, report);
|
|
1362
|
+
if (!t.closed) report.error({
|
|
1363
|
+
from: s,
|
|
1364
|
+
to: t.end
|
|
1365
|
+
}, "this template has no closing backtick", "end it with `");
|
|
1366
|
+
i = t.end;
|
|
1367
|
+
push({
|
|
1368
|
+
kind: "template",
|
|
1369
|
+
text: source.slice(s, i),
|
|
1370
|
+
quasis: t.quasis,
|
|
1371
|
+
parts: t.parts,
|
|
1372
|
+
from: s,
|
|
1373
|
+
to: i
|
|
1374
|
+
});
|
|
1375
|
+
} else {
|
|
1376
|
+
const p = PUNCT.find((m) => source.startsWith(m, i));
|
|
1377
|
+
if (!p) {
|
|
1378
|
+
report.error({
|
|
1379
|
+
from: i,
|
|
1380
|
+
to: i + 1
|
|
1381
|
+
}, `unexpected character "${c}"`);
|
|
1382
|
+
i++;
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
if (p === "(" || p === "[") depth++;
|
|
1386
|
+
else if ((p === ")" || p === "]") && depth > 0) depth--;
|
|
1387
|
+
else if (p === "{" || p === "}") depth = 0;
|
|
1388
|
+
push({
|
|
1389
|
+
kind: "punct",
|
|
1390
|
+
text: p,
|
|
1391
|
+
from: i,
|
|
1392
|
+
to: i + p.length
|
|
1393
|
+
});
|
|
1394
|
+
i += p.length;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
push({
|
|
1398
|
+
kind: "eof",
|
|
1399
|
+
text: "",
|
|
1400
|
+
from: to,
|
|
1401
|
+
to
|
|
1402
|
+
});
|
|
1403
|
+
return out;
|
|
1404
|
+
}
|
|
1405
|
+
const isIdStart = (c) => /[A-Za-z_]/.test(c);
|
|
1406
|
+
const isIdPart = (c) => /[A-Za-z0-9_]/.test(c);
|
|
1407
|
+
const isDigit = (c) => c >= "0" && c <= "9";
|
|
1408
|
+
const ESCAPES = {
|
|
1409
|
+
n: "\n",
|
|
1410
|
+
t: " ",
|
|
1411
|
+
"\\": "\\",
|
|
1412
|
+
"\"": "\"",
|
|
1413
|
+
"'": "'",
|
|
1414
|
+
"`": "`",
|
|
1415
|
+
$: "$"
|
|
1416
|
+
};
|
|
1417
|
+
function readString(src, i, to, q) {
|
|
1418
|
+
let value = "";
|
|
1419
|
+
while (i < to && src[i] !== q && src[i] !== "\n") if (src[i] === "\\" && i + 1 < to) {
|
|
1420
|
+
value += ESCAPES[src[i + 1]] ?? src[i + 1];
|
|
1421
|
+
i += 2;
|
|
1422
|
+
} else value += src[i++];
|
|
1423
|
+
const closed = src[i] === q;
|
|
1424
|
+
return {
|
|
1425
|
+
value,
|
|
1426
|
+
end: closed ? i + 1 : i,
|
|
1427
|
+
closed
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
/** Templates nested deeper than this are refused: the lexer reads them recursively. */
|
|
1431
|
+
const MAX_TEMPLATE_DEPTH = 16;
|
|
1432
|
+
function readTemplate(src, i, to, report, depth = 0) {
|
|
1433
|
+
const quasis = [];
|
|
1434
|
+
const parts = [];
|
|
1435
|
+
let cur = "";
|
|
1436
|
+
while (i < to && src[i] !== "`") if (src[i] === "\\" && i + 1 < to) {
|
|
1437
|
+
cur += ESCAPES[src[i + 1]] ?? src[i + 1];
|
|
1438
|
+
i += 2;
|
|
1439
|
+
} else if (src[i] === "$" && src[i + 1] === "{") {
|
|
1440
|
+
quasis.push(cur);
|
|
1441
|
+
cur = "";
|
|
1442
|
+
const start = i + 2;
|
|
1443
|
+
const end = skipExpression(src, start, to, report, depth);
|
|
1444
|
+
parts.push({
|
|
1445
|
+
from: start,
|
|
1446
|
+
to: end
|
|
1447
|
+
});
|
|
1448
|
+
if (src[end] === "}") i = end + 1;
|
|
1449
|
+
else {
|
|
1450
|
+
report.error({
|
|
1451
|
+
from: i,
|
|
1452
|
+
to: end
|
|
1453
|
+
}, "this \"${\" has no closing \"}\"");
|
|
1454
|
+
i = end;
|
|
1455
|
+
}
|
|
1456
|
+
} else cur += src[i++];
|
|
1457
|
+
quasis.push(cur);
|
|
1458
|
+
const closed = src[i] === "`";
|
|
1459
|
+
return {
|
|
1460
|
+
quasis,
|
|
1461
|
+
parts,
|
|
1462
|
+
end: closed ? i + 1 : i,
|
|
1463
|
+
closed
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
/** The offset of the `}` that closes a template's `${`, skipping nested braces, strings and templates. */
|
|
1467
|
+
function skipExpression(src, i, to, report, depth) {
|
|
1468
|
+
let braces = 0;
|
|
1469
|
+
while (i < to) {
|
|
1470
|
+
const c = src[i];
|
|
1471
|
+
if (c === "}" && braces === 0) return i;
|
|
1472
|
+
if (c === "{") braces++;
|
|
1473
|
+
else if (c === "}") braces--;
|
|
1474
|
+
else if (c === "\"" || c === "'") {
|
|
1475
|
+
i = readString(src, i + 1, to, c).end;
|
|
1476
|
+
continue;
|
|
1477
|
+
} else if (c === "`") {
|
|
1478
|
+
if (depth + 1 >= MAX_TEMPLATE_DEPTH) {
|
|
1479
|
+
report.error({
|
|
1480
|
+
from: i,
|
|
1481
|
+
to: i + 1
|
|
1482
|
+
}, `templates are nested more than ${MAX_TEMPLATE_DEPTH} deep`);
|
|
1483
|
+
return to;
|
|
1484
|
+
}
|
|
1485
|
+
i = readTemplate(src, i + 1, to, report, depth + 1).end;
|
|
1486
|
+
continue;
|
|
1487
|
+
} else if (c === "\n") return i;
|
|
1488
|
+
i++;
|
|
1489
|
+
}
|
|
1490
|
+
return i;
|
|
1491
|
+
}
|
|
1492
|
+
// src/parser.ts
|
|
1493
|
+
/** Deeper formulas are refused: a shared link must not be able to exhaust the stack. */
|
|
1494
|
+
const MAX_DEPTH = 64;
|
|
1495
|
+
/** Longer sources are refused before parsing. */
|
|
1496
|
+
const MAX_SOURCE = 2e5;
|
|
1497
|
+
const PREC = {
|
|
1498
|
+
"||": 1,
|
|
1499
|
+
"&&": 2,
|
|
1500
|
+
"==": 3,
|
|
1501
|
+
"!=": 3,
|
|
1502
|
+
"===": 3,
|
|
1503
|
+
"!==": 3,
|
|
1504
|
+
"<": 4,
|
|
1505
|
+
"<=": 4,
|
|
1506
|
+
">": 4,
|
|
1507
|
+
">=": 4,
|
|
1508
|
+
"+": 5,
|
|
1509
|
+
"-": 5,
|
|
1510
|
+
"*": 6,
|
|
1511
|
+
"/": 6,
|
|
1512
|
+
"%": 6
|
|
1513
|
+
};
|
|
1514
|
+
/** Thrown after a syntax error has been reported, to skip to the next statement. */
|
|
1515
|
+
var Bail$1 = class {};
|
|
1516
|
+
/** Parses a definition into statements. Never throws: errors go to the reporter, and parsing goes on at the next line. */
|
|
1517
|
+
function parse(source, report = new Reporter(source)) {
|
|
1518
|
+
if (source.length > 2e5) {
|
|
1519
|
+
report.error({
|
|
1520
|
+
from: 0,
|
|
1521
|
+
to: 0
|
|
1522
|
+
}, `the definition is ${source.length} characters long; at most ${MAX_SOURCE}`);
|
|
1523
|
+
return {
|
|
1524
|
+
doc: { body: [] },
|
|
1525
|
+
report
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
return {
|
|
1529
|
+
doc: { body: new Parser(lex(source, report), source, report).block(false) },
|
|
1530
|
+
report
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
/** Parses one formula on its own, e.g. a template's `${…}` part. */
|
|
1534
|
+
function parseExpression(source, from, to, report, depth = 0) {
|
|
1535
|
+
const p = new Parser(lex(source, report, from, to), source, report);
|
|
1536
|
+
p.depth = depth;
|
|
1537
|
+
try {
|
|
1538
|
+
const e = p.expr();
|
|
1539
|
+
if (p.peek().kind !== "eof") p.fail(p.peek(), `unexpected ${describe(p.peek())} in the formula`);
|
|
1540
|
+
return e;
|
|
1541
|
+
} catch (e) {
|
|
1542
|
+
if (e instanceof Bail$1) return {
|
|
1543
|
+
t: "error",
|
|
1544
|
+
from,
|
|
1545
|
+
to
|
|
1546
|
+
};
|
|
1547
|
+
throw e;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
var Parser = class {
|
|
1551
|
+
toks;
|
|
1552
|
+
source;
|
|
1553
|
+
report;
|
|
1554
|
+
i = 0;
|
|
1555
|
+
depth = 0;
|
|
1556
|
+
/** Blocks open around the statement being read. */
|
|
1557
|
+
blocks = 0;
|
|
1558
|
+
constructor(toks, source, report) {
|
|
1559
|
+
this.toks = toks;
|
|
1560
|
+
this.source = source;
|
|
1561
|
+
this.report = report;
|
|
1562
|
+
}
|
|
1563
|
+
peek(k = 0) {
|
|
1564
|
+
return this.toks[Math.min(this.i + k, this.toks.length - 1)];
|
|
1565
|
+
}
|
|
1566
|
+
next() {
|
|
1567
|
+
const t = this.peek();
|
|
1568
|
+
if (t.kind !== "eof") this.i++;
|
|
1569
|
+
return t;
|
|
1570
|
+
}
|
|
1571
|
+
is(text, k = 0) {
|
|
1572
|
+
const t = this.peek(k);
|
|
1573
|
+
return t.kind === "punct" && t.text === text;
|
|
1574
|
+
}
|
|
1575
|
+
eat(text) {
|
|
1576
|
+
if (!this.is(text)) return false;
|
|
1577
|
+
this.i++;
|
|
1578
|
+
return true;
|
|
1579
|
+
}
|
|
1580
|
+
expect(text, what) {
|
|
1581
|
+
if (!this.is(text)) this.fail(this.peek(), `expected ${what}, found ${describe(this.peek())}`);
|
|
1582
|
+
return this.next();
|
|
1583
|
+
}
|
|
1584
|
+
fail(at, message, hint) {
|
|
1585
|
+
this.report.error(at, message, hint);
|
|
1586
|
+
throw new Bail$1();
|
|
1587
|
+
}
|
|
1588
|
+
/** Statements up to the end of the file, or up to the `}` that closes this block (left for the caller). */
|
|
1589
|
+
block(inBraces) {
|
|
1590
|
+
const out = [];
|
|
1591
|
+
for (;;) {
|
|
1592
|
+
while (this.peek().kind === "newline" || this.is(";")) this.next();
|
|
1593
|
+
const t = this.peek();
|
|
1594
|
+
if (t.kind === "eof") break;
|
|
1595
|
+
if (this.is("}")) {
|
|
1596
|
+
if (inBraces) break;
|
|
1597
|
+
this.report.error(t, "this \"}\" closes no block");
|
|
1598
|
+
this.next();
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
try {
|
|
1602
|
+
out.push(this.statement());
|
|
1603
|
+
const end = this.peek();
|
|
1604
|
+
if (!(end.kind === "newline" || end.kind === "eof" || this.is(";") || this.is("}"))) this.fail(end, `expected the end of the line, found ${describe(end)}`);
|
|
1605
|
+
} catch (e) {
|
|
1606
|
+
if (!(e instanceof Bail$1)) throw e;
|
|
1607
|
+
this.recover();
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return out;
|
|
1611
|
+
}
|
|
1612
|
+
/** Skips to the end of the broken statement: the next line, `;` or the `}` of the enclosing block. */
|
|
1613
|
+
recover() {
|
|
1614
|
+
let depth = 0;
|
|
1615
|
+
for (;;) {
|
|
1616
|
+
const t = this.peek();
|
|
1617
|
+
if (t.kind === "eof") return;
|
|
1618
|
+
if (depth === 0 && (t.kind === "newline" || this.is(";") || this.is("}"))) return;
|
|
1619
|
+
if (this.is("{")) depth++;
|
|
1620
|
+
else if (this.is("}")) depth--;
|
|
1621
|
+
this.next();
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
statement() {
|
|
1625
|
+
const t = this.peek();
|
|
1626
|
+
if (t.kind === "number" || this.is("-") && this.peek(1).kind === "number") return this.row();
|
|
1627
|
+
if (t.kind !== "ident") this.fail(t, `expected a statement, found ${describe(t)}`);
|
|
1628
|
+
if (this.is(":", 1)) return this.property();
|
|
1629
|
+
if (this.is("->", 1)) return this.edge();
|
|
1630
|
+
return this.decl();
|
|
1631
|
+
}
|
|
1632
|
+
ident(what = "a name") {
|
|
1633
|
+
const t = this.peek();
|
|
1634
|
+
if (t.kind !== "ident") this.fail(t, `expected ${what}, found ${describe(t)}`);
|
|
1635
|
+
this.next();
|
|
1636
|
+
return {
|
|
1637
|
+
name: t.text,
|
|
1638
|
+
from: t.from,
|
|
1639
|
+
to: t.to
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
property() {
|
|
1643
|
+
const key = this.ident();
|
|
1644
|
+
this.expect(":", "\":\"");
|
|
1645
|
+
if (this.endsStatement()) this.fail(this.peek(), `"${key.name}" has no value`);
|
|
1646
|
+
const value = this.expr();
|
|
1647
|
+
if (this.is("->")) {
|
|
1648
|
+
if (value.t !== "id") this.fail(value, "a route is a list of names joined by \"->\"");
|
|
1649
|
+
const chain = [{
|
|
1650
|
+
name: value.name,
|
|
1651
|
+
from: value.from,
|
|
1652
|
+
to: value.to
|
|
1653
|
+
}];
|
|
1654
|
+
while (this.eat("->")) chain.push(this.ident("a stage"));
|
|
1655
|
+
return {
|
|
1656
|
+
kind: "prop",
|
|
1657
|
+
key,
|
|
1658
|
+
value,
|
|
1659
|
+
chain,
|
|
1660
|
+
from: key.from,
|
|
1661
|
+
to: chain.at(-1).to
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
return {
|
|
1665
|
+
kind: "prop",
|
|
1666
|
+
key,
|
|
1667
|
+
value,
|
|
1668
|
+
from: key.from,
|
|
1669
|
+
to: value.to
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
edge() {
|
|
1673
|
+
const chain = [this.ident()];
|
|
1674
|
+
while (this.eat("->")) chain.push(this.ident("a name after \"->\""));
|
|
1675
|
+
const e = {
|
|
1676
|
+
kind: "edge",
|
|
1677
|
+
chain,
|
|
1678
|
+
from: chain[0].from,
|
|
1679
|
+
to: chain.at(-1).to
|
|
1680
|
+
};
|
|
1681
|
+
if (this.eat(":")) {
|
|
1682
|
+
if (this.endsStatement()) this.fail(this.peek(), "expected a value after \":\"");
|
|
1683
|
+
e.value = this.expr();
|
|
1684
|
+
e.to = e.value.to;
|
|
1685
|
+
}
|
|
1686
|
+
if (this.is("{")) this.body(e);
|
|
1687
|
+
return e;
|
|
1688
|
+
}
|
|
1689
|
+
decl() {
|
|
1690
|
+
const k = this.next();
|
|
1691
|
+
const d = {
|
|
1692
|
+
kind: "decl",
|
|
1693
|
+
keyword: {
|
|
1694
|
+
name: k.text,
|
|
1695
|
+
from: k.from,
|
|
1696
|
+
to: k.to
|
|
1697
|
+
},
|
|
1698
|
+
args: [],
|
|
1699
|
+
from: k.from,
|
|
1700
|
+
to: k.to
|
|
1701
|
+
};
|
|
1702
|
+
for (;;) {
|
|
1703
|
+
const t = this.peek();
|
|
1704
|
+
if (t.kind === "ident" || t.kind === "string" || t.kind === "template" || t.kind === "number") d.args.push(this.primary());
|
|
1705
|
+
else if (this.is("-") && this.peek(1).kind === "number") d.args.push(this.unary());
|
|
1706
|
+
else break;
|
|
1707
|
+
d.to = d.args.at(-1).to;
|
|
1708
|
+
}
|
|
1709
|
+
if (this.eat("=")) {
|
|
1710
|
+
if (this.endsStatement()) this.fail(this.peek(), "expected a formula after \"=\"");
|
|
1711
|
+
d.init = this.expr();
|
|
1712
|
+
d.to = d.init.to;
|
|
1713
|
+
}
|
|
1714
|
+
if (this.is("{")) this.body(d);
|
|
1715
|
+
else if (!this.endsStatement()) this.fail(this.peek(), `expected "{" or the end of the line, found ${describe(this.peek())}`);
|
|
1716
|
+
return d;
|
|
1717
|
+
}
|
|
1718
|
+
body(d) {
|
|
1719
|
+
if (this.blocks >= 64) {
|
|
1720
|
+
const open = this.next();
|
|
1721
|
+
this.report.error(open, `blocks are nested more than 64 deep`);
|
|
1722
|
+
for (let depth = 1; depth > 0 && this.peek().kind !== "eof";) {
|
|
1723
|
+
const t = this.next();
|
|
1724
|
+
if (t.kind === "punct" && t.text === "{") depth++;
|
|
1725
|
+
else if (t.kind === "punct" && t.text === "}") depth--;
|
|
1726
|
+
}
|
|
1727
|
+
d.to = this.peek().from;
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
this.blocks++;
|
|
1731
|
+
try {
|
|
1732
|
+
this.bodyInside(d);
|
|
1733
|
+
} finally {
|
|
1734
|
+
this.blocks--;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
bodyInside(d) {
|
|
1738
|
+
const open = this.next();
|
|
1739
|
+
d.body = this.block(true);
|
|
1740
|
+
const close = this.peek();
|
|
1741
|
+
if (!this.is("}")) {
|
|
1742
|
+
this.report.error(open, `this "{" of "${"keyword" in d ? d.keyword.name : "edge"}" is never closed`);
|
|
1743
|
+
d.bodySpan = {
|
|
1744
|
+
from: open.from,
|
|
1745
|
+
to: close.from + 1
|
|
1746
|
+
};
|
|
1747
|
+
d.to = close.from;
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
this.next();
|
|
1751
|
+
d.bodySpan = {
|
|
1752
|
+
from: open.from,
|
|
1753
|
+
to: close.to
|
|
1754
|
+
};
|
|
1755
|
+
d.to = close.to;
|
|
1756
|
+
}
|
|
1757
|
+
row() {
|
|
1758
|
+
const values = [];
|
|
1759
|
+
while (this.peek().kind === "number" || this.is("-") && this.peek(1).kind === "number") {
|
|
1760
|
+
values.push(this.unary());
|
|
1761
|
+
this.eat(",");
|
|
1762
|
+
}
|
|
1763
|
+
return {
|
|
1764
|
+
kind: "row",
|
|
1765
|
+
values,
|
|
1766
|
+
from: values[0].from,
|
|
1767
|
+
to: values.at(-1).to
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
endsStatement() {
|
|
1771
|
+
const t = this.peek();
|
|
1772
|
+
return t.kind === "newline" || t.kind === "eof" || this.is(";") || this.is("}");
|
|
1773
|
+
}
|
|
1774
|
+
expr() {
|
|
1775
|
+
if (++this.depth > 64) this.fail(this.peek(), `the formula is nested more than 64 deep`);
|
|
1776
|
+
try {
|
|
1777
|
+
const test = this.binary(1);
|
|
1778
|
+
if (this.is("±") || this.is("+/-")) return this.pm(test);
|
|
1779
|
+
if (!this.eat("?")) return test;
|
|
1780
|
+
const then = this.expr();
|
|
1781
|
+
this.expect(":", "\":\" of the \"?\"");
|
|
1782
|
+
const otherwise = this.expr();
|
|
1783
|
+
return {
|
|
1784
|
+
t: "cond",
|
|
1785
|
+
test,
|
|
1786
|
+
then,
|
|
1787
|
+
else: otherwise,
|
|
1788
|
+
from: test.from,
|
|
1789
|
+
to: otherwise.to
|
|
1790
|
+
};
|
|
1791
|
+
} finally {
|
|
1792
|
+
this.depth--;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
/** `value ± within` or `value ± within%`: parsed anywhere, so the checker can say where it belongs. */
|
|
1796
|
+
pm(value) {
|
|
1797
|
+
this.next();
|
|
1798
|
+
const within = this.unary();
|
|
1799
|
+
const percent = this.is("%") ? this.next() : null;
|
|
1800
|
+
return {
|
|
1801
|
+
t: "pm",
|
|
1802
|
+
value,
|
|
1803
|
+
within,
|
|
1804
|
+
percent: !!percent,
|
|
1805
|
+
from: value.from,
|
|
1806
|
+
to: percent?.to ?? within.to
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
binary(min) {
|
|
1810
|
+
let left = this.unary();
|
|
1811
|
+
for (;;) {
|
|
1812
|
+
const t = this.peek();
|
|
1813
|
+
const prec = t.kind === "punct" ? PREC[t.text] : void 0;
|
|
1814
|
+
if (prec == null || prec < min) return left;
|
|
1815
|
+
this.next();
|
|
1816
|
+
const right = this.binary(prec + 1);
|
|
1817
|
+
left = {
|
|
1818
|
+
t: "bin",
|
|
1819
|
+
op: t.text === "===" ? "==" : t.text === "!==" ? "!=" : t.text,
|
|
1820
|
+
left,
|
|
1821
|
+
right,
|
|
1822
|
+
from: left.from,
|
|
1823
|
+
to: right.to
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
unary() {
|
|
1828
|
+
const t = this.peek();
|
|
1829
|
+
if (this.is("-") || this.is("+") || this.is("!")) {
|
|
1830
|
+
this.next();
|
|
1831
|
+
if (++this.depth > 64) this.fail(t, `the formula is nested more than 64 deep`);
|
|
1832
|
+
try {
|
|
1833
|
+
const arg = this.unary();
|
|
1834
|
+
return {
|
|
1835
|
+
t: "unary",
|
|
1836
|
+
op: t.text,
|
|
1837
|
+
arg,
|
|
1838
|
+
from: t.from,
|
|
1839
|
+
to: arg.to
|
|
1840
|
+
};
|
|
1841
|
+
} finally {
|
|
1842
|
+
this.depth--;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return this.primary();
|
|
1846
|
+
}
|
|
1847
|
+
primary() {
|
|
1848
|
+
const t = this.peek();
|
|
1849
|
+
if (!(t.kind !== "punct" ? t.kind !== "newline" && t.kind !== "eof" : t.text === "(" || t.text === "[")) this.fail(t, `expected a value, found ${describe(t)}`);
|
|
1850
|
+
this.next();
|
|
1851
|
+
switch (t.kind) {
|
|
1852
|
+
case "number": return {
|
|
1853
|
+
t: "num",
|
|
1854
|
+
value: t.value,
|
|
1855
|
+
from: t.from,
|
|
1856
|
+
to: t.to
|
|
1857
|
+
};
|
|
1858
|
+
case "string": return {
|
|
1859
|
+
t: "str",
|
|
1860
|
+
value: t.value,
|
|
1861
|
+
from: t.from,
|
|
1862
|
+
to: t.to
|
|
1863
|
+
};
|
|
1864
|
+
case "template": return {
|
|
1865
|
+
t: "tpl",
|
|
1866
|
+
quasis: t.quasis,
|
|
1867
|
+
parts: t.parts.map((s) => this.part(s)),
|
|
1868
|
+
from: t.from,
|
|
1869
|
+
to: t.to
|
|
1870
|
+
};
|
|
1871
|
+
case "ident": {
|
|
1872
|
+
if (t.text === "true" || t.text === "false") return {
|
|
1873
|
+
t: "bool",
|
|
1874
|
+
value: t.text === "true",
|
|
1875
|
+
from: t.from,
|
|
1876
|
+
to: t.to
|
|
1877
|
+
};
|
|
1878
|
+
let callee = {
|
|
1879
|
+
name: t.text,
|
|
1880
|
+
from: t.from,
|
|
1881
|
+
to: t.to
|
|
1882
|
+
};
|
|
1883
|
+
if (this.is(".")) {
|
|
1884
|
+
this.next();
|
|
1885
|
+
const prop = this.ident("a name after \".\"");
|
|
1886
|
+
if (!this.is("(")) {
|
|
1887
|
+
if (this.is(".")) this.fail(this.peek(), "only one \".\" is allowed: flows.orders, util.db");
|
|
1888
|
+
return {
|
|
1889
|
+
t: "member",
|
|
1890
|
+
object: callee,
|
|
1891
|
+
prop,
|
|
1892
|
+
from: t.from,
|
|
1893
|
+
to: prop.to
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
if (callee.name !== "Math") this.fail(callee, `"${callee.name}.${prop.name}(…)" is not a function`);
|
|
1897
|
+
callee = prop;
|
|
1898
|
+
}
|
|
1899
|
+
if (this.is("(")) {
|
|
1900
|
+
this.next();
|
|
1901
|
+
const args = [];
|
|
1902
|
+
while (!this.is(")")) {
|
|
1903
|
+
args.push(this.expr());
|
|
1904
|
+
if (!this.eat(",")) break;
|
|
1905
|
+
}
|
|
1906
|
+
const close = this.expect(")", "\")\" or \",\"");
|
|
1907
|
+
return {
|
|
1908
|
+
t: "call",
|
|
1909
|
+
callee,
|
|
1910
|
+
args,
|
|
1911
|
+
from: t.from,
|
|
1912
|
+
to: close.to
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
return {
|
|
1916
|
+
t: "id",
|
|
1917
|
+
name: t.text,
|
|
1918
|
+
from: t.from,
|
|
1919
|
+
to: t.to
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
case "punct":
|
|
1923
|
+
if (t.text === "(") {
|
|
1924
|
+
const e = this.expr();
|
|
1925
|
+
const close = this.expect(")", "\")\"");
|
|
1926
|
+
return {
|
|
1927
|
+
...e,
|
|
1928
|
+
from: t.from,
|
|
1929
|
+
to: close.to
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
if (t.text === "[") {
|
|
1933
|
+
const items = [];
|
|
1934
|
+
while (!this.is("]")) {
|
|
1935
|
+
items.push(this.expr());
|
|
1936
|
+
if (!this.eat(",")) break;
|
|
1937
|
+
}
|
|
1938
|
+
const close = this.expect("]", "\"]\" or \",\"");
|
|
1939
|
+
return {
|
|
1940
|
+
t: "array",
|
|
1941
|
+
items,
|
|
1942
|
+
from: t.from,
|
|
1943
|
+
to: close.to
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
this.fail(t, `expected a value, found ${describe(t)}`);
|
|
1948
|
+
}
|
|
1949
|
+
/** One `${…}` of a template, parsed from its place in the source so errors point into it. */
|
|
1950
|
+
part(s) {
|
|
1951
|
+
if (s.to > s.from) return parseExpression(this.source, s.from, s.to, this.report, this.depth);
|
|
1952
|
+
this.report.error({
|
|
1953
|
+
from: s.from - 2,
|
|
1954
|
+
to: s.to + 1
|
|
1955
|
+
}, "an empty part in a template");
|
|
1956
|
+
return {
|
|
1957
|
+
t: "error",
|
|
1958
|
+
from: s.from,
|
|
1959
|
+
to: s.to
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
function describe(t) {
|
|
1964
|
+
switch (t.kind) {
|
|
1965
|
+
case "eof": return "the end of the file";
|
|
1966
|
+
case "newline": return "the end of the line";
|
|
1967
|
+
case "string":
|
|
1968
|
+
case "template": return "a text";
|
|
1969
|
+
case "number": return `the number ${t.text}`;
|
|
1970
|
+
default: return `"${t.text}"`;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
// src/compile.ts
|
|
1974
|
+
/** The version of the language this package reads: the number on a definition's first line. */
|
|
1975
|
+
const LANGUAGE_VERSION = 1;
|
|
1976
|
+
/** Counts that size the drawing; larger ones would hang the layout, so a shared link cannot ask for them. */
|
|
1977
|
+
const LIMITS = {
|
|
1978
|
+
members: 64,
|
|
1979
|
+
pods: 256,
|
|
1980
|
+
perPod: 256,
|
|
1981
|
+
partitions: 1024
|
|
1982
|
+
};
|
|
1983
|
+
/** Reads a definition: parses, checks, and builds the model and view `<Loadmap>` takes. Never throws. */
|
|
1984
|
+
function compile(source, o = {}) {
|
|
1985
|
+
const report = new Reporter(source);
|
|
1986
|
+
const c = new Compiler(source, report, o.functions ?? {});
|
|
1987
|
+
let doc = { body: [] };
|
|
1988
|
+
let definition;
|
|
1989
|
+
let expectations = [];
|
|
1990
|
+
try {
|
|
1991
|
+
doc = parse(source, report).doc;
|
|
1992
|
+
definition = c.run(doc);
|
|
1993
|
+
if (definition) expectations = check(definition.model).map((result, i) => {
|
|
1994
|
+
const at = c.expectSpans[i];
|
|
1995
|
+
return {
|
|
1996
|
+
at,
|
|
1997
|
+
...report.position(at.from),
|
|
1998
|
+
result
|
|
1999
|
+
};
|
|
2000
|
+
});
|
|
2001
|
+
} catch (err) {
|
|
2002
|
+
if (err instanceof FormulaError) report.error(err.span, err.message);
|
|
2003
|
+
else if (!(err instanceof Bail)) report.error({
|
|
2004
|
+
from: 0,
|
|
2005
|
+
to: 0
|
|
2006
|
+
}, `internal error: ${err?.message ?? err}`);
|
|
2007
|
+
}
|
|
2008
|
+
const diagnostics = [...report.list].sort((a, b) => a.from - b.from);
|
|
2009
|
+
return {
|
|
2010
|
+
ok: !report.failed && !!definition,
|
|
2011
|
+
diagnostics,
|
|
2012
|
+
definition,
|
|
2013
|
+
expectations,
|
|
2014
|
+
symbols: c.symbols,
|
|
2015
|
+
doc
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* The diagnostics and every failing expectation, in source order: an editor marks a failure as a warning (the
|
|
2020
|
+
* diagram still shows), `loadmap check` as an error.
|
|
2021
|
+
*/
|
|
2022
|
+
function problems(r, failed = "warning") {
|
|
2023
|
+
const out = [...r.diagnostics];
|
|
2024
|
+
for (const e of r.expectations) if (!e.result.ok) {
|
|
2025
|
+
const name = e.result.expectation.label ? `"${e.result.expectation.label}" fails: ` : "";
|
|
2026
|
+
out.push({
|
|
2027
|
+
severity: failed,
|
|
2028
|
+
message: `${name}${e.result.message}`,
|
|
2029
|
+
...e.at,
|
|
2030
|
+
line: e.line,
|
|
2031
|
+
column: e.column
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
return out.sort((a, b) => a.from - b.from);
|
|
2035
|
+
}
|
|
2036
|
+
/** Stops compiling after an error that leaves nothing sensible to check. */
|
|
2037
|
+
var Bail = class {};
|
|
2038
|
+
var Compiler = class {
|
|
2039
|
+
source;
|
|
2040
|
+
report;
|
|
2041
|
+
functions;
|
|
2042
|
+
symbols = [];
|
|
2043
|
+
/** Where each expectation of the model is written, in order. */
|
|
2044
|
+
expectSpans = [];
|
|
2045
|
+
names;
|
|
2046
|
+
fits = /* @__PURE__ */ new Map();
|
|
2047
|
+
lets = /* @__PURE__ */ new Map();
|
|
2048
|
+
constructor(source, report, functions) {
|
|
2049
|
+
this.source = source;
|
|
2050
|
+
this.report = report;
|
|
2051
|
+
this.functions = functions;
|
|
2052
|
+
}
|
|
2053
|
+
run(doc) {
|
|
2054
|
+
const root = this.shape("", SCHEMA[""], doc.body, {
|
|
2055
|
+
from: 0,
|
|
2056
|
+
to: 0
|
|
2057
|
+
});
|
|
2058
|
+
this.header(doc.body);
|
|
2059
|
+
const byKind = (k) => root.children.filter((b) => b.keyword === k);
|
|
2060
|
+
const loads = byKind("load");
|
|
2061
|
+
if (!loads.length) this.report.error({
|
|
2062
|
+
from: 0,
|
|
2063
|
+
to: 0
|
|
2064
|
+
}, "a definition needs a load line", "load { max: 100 }");
|
|
2065
|
+
for (const extra of loads.slice(1)) this.report.error(extra.at, "only one load line is allowed");
|
|
2066
|
+
const params = byKind("param");
|
|
2067
|
+
const lets = byKind("let");
|
|
2068
|
+
const fits = byKind("fit");
|
|
2069
|
+
const flows = byKind("flow");
|
|
2070
|
+
const boxes = root.children.filter((b) => BOXES.includes(b.keyword) && b.args.id).map((b) => this.box(b));
|
|
2071
|
+
const loose = byKind("component");
|
|
2072
|
+
const formulaNames = /* @__PURE__ */ new Map();
|
|
2073
|
+
const own = (kind, b, taken) => {
|
|
2074
|
+
const id = b.args.id;
|
|
2075
|
+
if (!id) return;
|
|
2076
|
+
this.declare(kind, id.name, id, b, taken);
|
|
2077
|
+
};
|
|
2078
|
+
for (const b of [
|
|
2079
|
+
...params,
|
|
2080
|
+
...lets,
|
|
2081
|
+
...fits
|
|
2082
|
+
]) {
|
|
2083
|
+
own(b.keyword === "param" ? "param" : b.keyword === "let" ? "let" : "fit", b, formulaNames);
|
|
2084
|
+
if (b.args.id && RESERVED.has(b.args.id.name)) this.report.error(b.args.id, `"${b.args.id.name}" is a reserved name; pick another`);
|
|
2085
|
+
}
|
|
2086
|
+
const flowNames = /* @__PURE__ */ new Map();
|
|
2087
|
+
for (const b of flows) own("flow", b, flowNames);
|
|
2088
|
+
const nodeNames = /* @__PURE__ */ new Map();
|
|
2089
|
+
const stageNames = /* @__PURE__ */ new Map();
|
|
2090
|
+
const componentNames = /* @__PURE__ */ new Map();
|
|
2091
|
+
for (const box of boxes) {
|
|
2092
|
+
this.declare("node", box.id, box.block.args.id, box.block, nodeNames, box.block.keyword);
|
|
2093
|
+
for (const s of box.stages) this.declare("stage", s.id, s.block.args.id ?? s.block.at, s.block, stageNames, box.id);
|
|
2094
|
+
if (box.component) {
|
|
2095
|
+
const c = box.component;
|
|
2096
|
+
this.declare("component", c.id, c.own ? box.block.args.id : c.block.args.id ?? c.block.at, c.block, componentNames);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
for (const b of loose) if (!b.args.id) this.report.error(b.at, "a component outside a box needs an id: component cache \"Cache\"");
|
|
2100
|
+
else own("component", b, componentNames);
|
|
2101
|
+
const paramTypes = /* @__PURE__ */ new Map([["load", "number"]]);
|
|
2102
|
+
for (const b of params) if (b.args.id) paramTypes.set(b.args.id.name, this.paramType(b));
|
|
2103
|
+
for (const f of fits) this.fit(f);
|
|
2104
|
+
this.names = {
|
|
2105
|
+
params: paramTypes,
|
|
2106
|
+
lets: this.lets,
|
|
2107
|
+
flows: new Set(flowNames.keys()),
|
|
2108
|
+
components: new Set(componentNames.keys()),
|
|
2109
|
+
fits: this.fits,
|
|
2110
|
+
functions: this.functions
|
|
2111
|
+
};
|
|
2112
|
+
for (const b of lets) if (b.args.id) this.lets.set(b.args.id.name, "any");
|
|
2113
|
+
for (let changed = true, pass = 0; changed && pass <= lets.length; pass++) {
|
|
2114
|
+
changed = false;
|
|
2115
|
+
for (const b of lets) {
|
|
2116
|
+
if (!b.args.id || !b.decl?.init) continue;
|
|
2117
|
+
const type = compileFormula(b.decl.init, this.names, "ctx", new Reporter(this.source)).type;
|
|
2118
|
+
if (type !== this.lets.get(b.args.id.name)) changed = true;
|
|
2119
|
+
this.lets.set(b.args.id.name, type);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
const stageOf = /* @__PURE__ */ new Map();
|
|
2123
|
+
for (const box of boxes) for (const s of box.stages) stageOf.set(s.id, {
|
|
2124
|
+
block: s.block,
|
|
2125
|
+
box
|
|
2126
|
+
});
|
|
2127
|
+
const boxById = new Map(boxes.map((b) => [b.id, b]));
|
|
2128
|
+
const load = loads[0] && this.load(loads[0]);
|
|
2129
|
+
const paramDefs = {};
|
|
2130
|
+
for (const b of params) if (b.args.id) paramDefs[b.args.id.name] = this.param(b);
|
|
2131
|
+
const presets = {};
|
|
2132
|
+
for (const b of byKind("preset")) {
|
|
2133
|
+
const label = this.text(b.args.label);
|
|
2134
|
+
if (label != null) presets[label] = this.preset(b, params);
|
|
2135
|
+
}
|
|
2136
|
+
const scenarios = {};
|
|
2137
|
+
for (const b of byKind("scenario")) {
|
|
2138
|
+
const label = this.text(b.args.label);
|
|
2139
|
+
if (label == null) continue;
|
|
2140
|
+
if (Object.hasOwn(scenarios, label)) this.report.error(b.args.label, `scenario "${label}" is declared twice`);
|
|
2141
|
+
else scenarios[label] = this.preset(b, params, "scenario");
|
|
2142
|
+
this.symbols.push({
|
|
2143
|
+
kind: "scenario",
|
|
2144
|
+
id: label,
|
|
2145
|
+
span: b.args.label,
|
|
2146
|
+
decl: b.decl ?? b.at
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
const expects = [];
|
|
2150
|
+
for (const b of byKind("expect")) {
|
|
2151
|
+
const e = this.expectation(b, scenarios, componentNames);
|
|
2152
|
+
if (!e) continue;
|
|
2153
|
+
expects.push(e);
|
|
2154
|
+
this.expectSpans.push({
|
|
2155
|
+
from: b.at.from,
|
|
2156
|
+
to: b.args.label?.to ?? b.at.to
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
const story = [];
|
|
2160
|
+
for (const b of byKind("step")) {
|
|
2161
|
+
const st = this.step(b, scenarios, componentNames, params, load);
|
|
2162
|
+
if (st) story.push(st);
|
|
2163
|
+
}
|
|
2164
|
+
const growths = byKind("growth");
|
|
2165
|
+
for (const extra of growths.slice(1)) this.report.error(extra.at, "only one growth line is allowed");
|
|
2166
|
+
const growth = growths[0] && this.growth(growths[0]);
|
|
2167
|
+
const derived = {};
|
|
2168
|
+
for (const b of lets) if (b.args.id && b.decl?.init) derived[b.args.id.name] = placed(b.decl.init, this.formula(b.decl.init, "ctx").run);
|
|
2169
|
+
const resolveStage = (n, what) => {
|
|
2170
|
+
if (stageOf.has(n.name)) return n.name;
|
|
2171
|
+
const box = boxById.get(n.name);
|
|
2172
|
+
if (box?.stages.length === 1) return box.stages[0].id;
|
|
2173
|
+
if (box) this.report.error(n, box.stages.length ? `"${n.name}" holds ${box.stages.length} stages; name one: ${box.stages.map((s) => s.id).join(", ")}` : `the box "${n.name}" runs no stage`);
|
|
2174
|
+
else this.report.error(n, `unknown ${what} "${n.name}"`, didYouMean(n.name, [...stageOf.keys(), ...boxById.keys()]));
|
|
2175
|
+
};
|
|
2176
|
+
const flowDefs = {};
|
|
2177
|
+
const flowViews = {};
|
|
2178
|
+
const routes = {};
|
|
2179
|
+
const reached = /* @__PURE__ */ new Set();
|
|
2180
|
+
for (const b of flows) {
|
|
2181
|
+
const id = b.args.id;
|
|
2182
|
+
if (!id) continue;
|
|
2183
|
+
const label = this.text(b.args.label);
|
|
2184
|
+
const def = {};
|
|
2185
|
+
if (label != null) def.label = label;
|
|
2186
|
+
const unit = this.constProp(b, "unit");
|
|
2187
|
+
if (unit != null) def.unit = unit;
|
|
2188
|
+
def.rate = this.dynProp(b, "rate", "number");
|
|
2189
|
+
const priority = this.constProp(b, "priority");
|
|
2190
|
+
if (priority != null) def.priority = priority;
|
|
2191
|
+
const overload = this.overload(b);
|
|
2192
|
+
if (overload) def.overload = overload;
|
|
2193
|
+
flowDefs[id.name] = def;
|
|
2194
|
+
const route = this.prop(b, "route");
|
|
2195
|
+
const stages = [];
|
|
2196
|
+
for (const [i, n] of (route?.chain ?? (route?.value.t === "id" ? [route.value] : [])).entries()) {
|
|
2197
|
+
const s = resolveStage(n, "stage");
|
|
2198
|
+
if (!s) continue;
|
|
2199
|
+
if (stageOf.get(s).block.keyword === "fanout" && i < (route?.chain?.length ?? 1) - 1) this.report.error(n, `the fan-out "${s}" must be the last stage of the route`);
|
|
2200
|
+
stages.push(s);
|
|
2201
|
+
reached.add(s);
|
|
2202
|
+
}
|
|
2203
|
+
if (route && route.value.t !== "id") this.report.error(route.value, "a route is a list of stages: a -> b -> c");
|
|
2204
|
+
routes[id.name] = stages;
|
|
2205
|
+
const from = this.prop(b, "from");
|
|
2206
|
+
const view = {
|
|
2207
|
+
from: "",
|
|
2208
|
+
color: "accent"
|
|
2209
|
+
};
|
|
2210
|
+
if (from) {
|
|
2211
|
+
if (from.value.t === "id" && boxById.has(from.value.name)) view.from = from.value.name;
|
|
2212
|
+
else this.report.error(from.value, `unknown node "${this.slice(from.value)}"`, didYouMean(this.slice(from.value), boxById.keys()));
|
|
2213
|
+
}
|
|
2214
|
+
const color = this.prop(b, "color");
|
|
2215
|
+
if (color) view.color = this.color(color.value);
|
|
2216
|
+
if (label != null) view.label = label;
|
|
2217
|
+
flowViews[id.name] = view;
|
|
2218
|
+
}
|
|
2219
|
+
const componentDefs = {};
|
|
2220
|
+
for (const box of boxes) if (box.component) componentDefs[box.component.id] = this.component(box.component.block, box.block);
|
|
2221
|
+
for (const b of loose) if (b.args.id) componentDefs[b.args.id.name] = this.component(b);
|
|
2222
|
+
const stageDefs = {};
|
|
2223
|
+
for (const box of boxes) for (const s of box.stages) {
|
|
2224
|
+
const def = this.stage(s.block, box, componentNames, resolveStage, flowNames, reached);
|
|
2225
|
+
if (def) stageDefs[s.id] = def;
|
|
2226
|
+
}
|
|
2227
|
+
for (const [id, s] of stageOf) if (!reached.has(id) && s.block.keyword !== "fanout") this.report.warning(s.block.args.id ?? s.block.at, `no flow passes the stage "${id}"`);
|
|
2228
|
+
const calibration = {};
|
|
2229
|
+
for (const f of fits) {
|
|
2230
|
+
const fit = f.args.id && this.fits.get(f.args.id.name);
|
|
2231
|
+
if (fit) calibration[this.text(f.args.label) ?? f.args.id.name] = fit;
|
|
2232
|
+
}
|
|
2233
|
+
const model = {
|
|
2234
|
+
load,
|
|
2235
|
+
params: paramDefs,
|
|
2236
|
+
flows: flowDefs,
|
|
2237
|
+
components: componentDefs,
|
|
2238
|
+
stages: stageDefs,
|
|
2239
|
+
routes
|
|
2240
|
+
};
|
|
2241
|
+
const title = this.constProp(root, "title");
|
|
2242
|
+
if (title != null) model.title = title;
|
|
2243
|
+
if (Object.keys(presets).length) model.presets = presets;
|
|
2244
|
+
if (Object.keys(scenarios).length) model.scenarios = scenarios;
|
|
2245
|
+
if (growth) model.growth = growth;
|
|
2246
|
+
if (expects.length) model.expect = expects;
|
|
2247
|
+
if (story.length) model.story = story;
|
|
2248
|
+
if (Object.keys(derived).length) model.derived = derived;
|
|
2249
|
+
if (Object.keys(calibration).length) model.calibration = calibration;
|
|
2250
|
+
const th = byKind("thresholds")[0];
|
|
2251
|
+
if (th) {
|
|
2252
|
+
const t = {};
|
|
2253
|
+
for (const k of ["target", "full"]) {
|
|
2254
|
+
const v = this.constProp(th, k);
|
|
2255
|
+
if (v != null) t[k] = v;
|
|
2256
|
+
}
|
|
2257
|
+
model.thresholds = t;
|
|
2258
|
+
}
|
|
2259
|
+
const searchMax = this.constProp(root, "searchMax");
|
|
2260
|
+
if (searchMax != null) model.searchMax = searchMax;
|
|
2261
|
+
const nodes = {};
|
|
2262
|
+
for (const box of boxes) nodes[box.id] = this.node(box);
|
|
2263
|
+
const edges = [];
|
|
2264
|
+
for (const e of root.edges) edges.push(...this.edges(e, boxById));
|
|
2265
|
+
if (!flows.length) this.report.error({
|
|
2266
|
+
from: 0,
|
|
2267
|
+
to: 0
|
|
2268
|
+
}, "a definition needs at least one flow");
|
|
2269
|
+
const options = { assumptions: [] };
|
|
2270
|
+
const speed = this.constProp(root, "speed");
|
|
2271
|
+
if (speed != null) options.speed = speed;
|
|
2272
|
+
const layout = this.prop(root, "layout");
|
|
2273
|
+
if (layout) options.direction = this.enumValue(layout, SCHEMA[""].props.layout) === "down" ? "DOWN" : "RIGHT";
|
|
2274
|
+
for (const a of root.props.get("assume") ?? []) {
|
|
2275
|
+
const v = this.constant(a.value, "string");
|
|
2276
|
+
if (typeof v === "string") options.assumptions.push(v);
|
|
2277
|
+
}
|
|
2278
|
+
if (this.report.failed) return void 0;
|
|
2279
|
+
return this.build(model, {
|
|
2280
|
+
nodes,
|
|
2281
|
+
edges,
|
|
2282
|
+
flows: flowViews
|
|
2283
|
+
}, options, byKind("preset"), byKind("scenario"));
|
|
2284
|
+
}
|
|
2285
|
+
/** Hands the parts to core and layout, then runs the model once per preset and scenario so formula errors show now. */
|
|
2286
|
+
build(modelDef, viewDef, options, presets, scenarios) {
|
|
2287
|
+
const top = {
|
|
2288
|
+
from: 0,
|
|
2289
|
+
to: 0
|
|
2290
|
+
};
|
|
2291
|
+
let model;
|
|
2292
|
+
let view;
|
|
2293
|
+
try {
|
|
2294
|
+
model = defineModel(modelDef);
|
|
2295
|
+
view = defineView(model, viewDef);
|
|
2296
|
+
} catch (err) {
|
|
2297
|
+
this.report.error(top, err instanceof Error ? err.message.replace(/^loadmap: /, "") : String(err));
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
const runs = [{
|
|
2301
|
+
values: {},
|
|
2302
|
+
at: top
|
|
2303
|
+
}];
|
|
2304
|
+
for (const b of presets) {
|
|
2305
|
+
const label = this.text(b.args.label);
|
|
2306
|
+
const v = label != null && modelDef.presets?.[label];
|
|
2307
|
+
if (v) runs.push({
|
|
2308
|
+
values: v,
|
|
2309
|
+
at: b.at
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
for (const b of scenarios) {
|
|
2313
|
+
const label = this.text(b.args.label);
|
|
2314
|
+
const v = label != null && model.scenarios.find((s) => s.label === label)?.values;
|
|
2315
|
+
if (v) runs.push({
|
|
2316
|
+
values: v,
|
|
2317
|
+
at: b.at
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
for (const { values, at } of runs) try {
|
|
2321
|
+
const ev = model.evaluate(values);
|
|
2322
|
+
labelsAt(view, ev);
|
|
2323
|
+
for (const k of Object.keys(modelDef.derived ?? {})) ev.ctx.derived[k];
|
|
2324
|
+
for (const n of Object.values(viewDef.nodes)) {
|
|
2325
|
+
if (n.kind === "cluster") {
|
|
2326
|
+
const members = typeof n.members === "function" ? n.members(ev.ctx) : n.members;
|
|
2327
|
+
n.memberLabel?.(Math.max(0, members - 1));
|
|
2328
|
+
}
|
|
2329
|
+
if (n.kind === "pool") n.podLabel?.(0);
|
|
2330
|
+
}
|
|
2331
|
+
for (const e of viewDef.edges) e.when?.(ev.ctx);
|
|
2332
|
+
for (const f of model.flowIds) if (!Number.isFinite(ev.flows[f])) this.report.warning(this.flowRate.get(f) ?? at, `the rate of "${f}" is ${ev.flows[f]} here`);
|
|
2333
|
+
} catch (err) {
|
|
2334
|
+
if (err instanceof FormulaError) this.report.error(err.span, err.message);
|
|
2335
|
+
else this.report.error(at, err instanceof Error ? err.message.replace(/^loadmap: /, "") : String(err));
|
|
2336
|
+
}
|
|
2337
|
+
if (this.report.failed) return void 0;
|
|
2338
|
+
return {
|
|
2339
|
+
model,
|
|
2340
|
+
view,
|
|
2341
|
+
options
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
flowRate = /* @__PURE__ */ new Map();
|
|
2345
|
+
shape(keyword, spec, stmts, at, decl) {
|
|
2346
|
+
const b = {
|
|
2347
|
+
keyword,
|
|
2348
|
+
spec,
|
|
2349
|
+
at,
|
|
2350
|
+
decl,
|
|
2351
|
+
args: {},
|
|
2352
|
+
props: /* @__PURE__ */ new Map(),
|
|
2353
|
+
children: [],
|
|
2354
|
+
edges: [],
|
|
2355
|
+
rows: []
|
|
2356
|
+
};
|
|
2357
|
+
if (decl) this.args(b, decl);
|
|
2358
|
+
const here = keyword ? `in ${keyword}` : "at the top";
|
|
2359
|
+
for (const s of stmts) if (s.kind === "prop") {
|
|
2360
|
+
const k = s.key.name;
|
|
2361
|
+
if (!spec.open && !own(spec.props, k)) {
|
|
2362
|
+
const hint = spec.children.includes(k) ? `write it as a block: ${k} { … }` : didYouMean(k, [...Object.keys(spec.props), ...spec.children]);
|
|
2363
|
+
this.report.error(s.key, `unknown property "${k}" ${here}`, hint);
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2366
|
+
const list = b.props.get(k) ?? [];
|
|
2367
|
+
if (list.length && !(own(spec.props, k)?.repeat ?? false)) this.report.error(s.key, `"${k}" is set twice ${here}`, "remove one of them");
|
|
2368
|
+
list.push(s);
|
|
2369
|
+
b.props.set(k, list);
|
|
2370
|
+
} else if (s.kind === "decl") {
|
|
2371
|
+
const k = s.keyword.name;
|
|
2372
|
+
const childSpec = own(SCHEMA, k);
|
|
2373
|
+
if (!spec.children.includes(k) || !childSpec || k === "") {
|
|
2374
|
+
if (own(spec.props, k)) this.report.error(s.keyword, `"${k}" is a property: write "${k}: …"`);
|
|
2375
|
+
else if (childSpec) this.report.error(s.keyword, `"${k}" cannot go ${here}`, where(k));
|
|
2376
|
+
else this.report.error(s.keyword, `unknown statement "${k}" ${here}`, didYouMean(k, [...spec.children, ...Object.keys(spec.props)]));
|
|
2377
|
+
continue;
|
|
2378
|
+
}
|
|
2379
|
+
b.children.push(this.shape(k, childSpec, s.body ?? [], s.keyword, s));
|
|
2380
|
+
} else if (s.kind === "edge") {
|
|
2381
|
+
if (!spec.edges) this.report.error(s.chain[0], `an "a -> b" line cannot go ${here}`);
|
|
2382
|
+
else b.edges.push(s);
|
|
2383
|
+
} else if (!spec.rows) this.report.error(s, `a row of numbers can only go in "points"`);
|
|
2384
|
+
else b.rows.push(s);
|
|
2385
|
+
if (decl) {
|
|
2386
|
+
for (const [k, p] of Object.entries(spec.props)) if (p.required && !b.props.has(k)) this.report.error(at, `"${keyword}" needs "${k}: …"`, p.doc);
|
|
2387
|
+
}
|
|
2388
|
+
return b;
|
|
2389
|
+
}
|
|
2390
|
+
args(b, d) {
|
|
2391
|
+
const slots = [...b.spec.args];
|
|
2392
|
+
for (const a of d.args) {
|
|
2393
|
+
const kind = a.t === "id" ? "id" : a.t === "str" || a.t === "tpl" ? "text" : a.t === "num" ? "number" : null;
|
|
2394
|
+
const i = slots.findIndex((s) => s.kind === kind);
|
|
2395
|
+
if (kind == null || i < 0 || slots.slice(0, i).some((s) => !s.optional)) {
|
|
2396
|
+
this.report.error(a, `unexpected ${this.slice(a)} after "${b.keyword}"`, usage(b.keyword));
|
|
2397
|
+
continue;
|
|
2398
|
+
}
|
|
2399
|
+
const slot = slots[i];
|
|
2400
|
+
slots.splice(0, i + 1);
|
|
2401
|
+
if (a.t === "tpl" && !slot.dynamic && !a.parts.length) {
|
|
2402
|
+
b.args[slot.name] = {
|
|
2403
|
+
t: "str",
|
|
2404
|
+
value: a.quasis[0],
|
|
2405
|
+
from: a.from,
|
|
2406
|
+
to: a.to
|
|
2407
|
+
};
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
if (a.t === "tpl" && !slot.dynamic) this.report.error(a, `the ${slot.name} of "${b.keyword}" must be a plain text`);
|
|
2411
|
+
if (a.t === "id") b.args[slot.name] = {
|
|
2412
|
+
name: a.name,
|
|
2413
|
+
from: a.from,
|
|
2414
|
+
to: a.to
|
|
2415
|
+
};
|
|
2416
|
+
else b.args[slot.name] = a;
|
|
2417
|
+
}
|
|
2418
|
+
const missing = slots.find((s) => !s.optional);
|
|
2419
|
+
if (missing) this.report.error(b.at, `"${b.keyword}" needs its ${missing.name}`, usage(b.keyword));
|
|
2420
|
+
if (b.spec.init && !d.init) this.report.error(b.at, `"${b.keyword}" needs "= formula"`, usage(b.keyword));
|
|
2421
|
+
if (!b.spec.init && d.init) this.report.error(d.init, `"${b.keyword}" takes no "="`);
|
|
2422
|
+
}
|
|
2423
|
+
header(body) {
|
|
2424
|
+
const first = body[0];
|
|
2425
|
+
const isHeader = (s) => s?.kind === "decl" && s.keyword.name === "loadmap";
|
|
2426
|
+
if (!isHeader(first)) {
|
|
2427
|
+
this.report.error(first ?? {
|
|
2428
|
+
from: 0,
|
|
2429
|
+
to: 0
|
|
2430
|
+
}, `a definition starts with "loadmap 1"`, `add "loadmap 1" as the first line`);
|
|
2431
|
+
throw new Bail();
|
|
2432
|
+
}
|
|
2433
|
+
for (const s of body.slice(1)) if (isHeader(s)) this.report.error(s.keyword, "the \"loadmap\" line must be the first and only one");
|
|
2434
|
+
const v = first.args[0];
|
|
2435
|
+
if (v?.t === "num" && v.value !== 1) {
|
|
2436
|
+
this.report.error(v, v.value > 1 ? `this definition is written in language version ${v.value}; this loadmap reads version 1` : `there is no language version ${v.value}; the first is 1`, v.value > 1 ? "update @loadmap/dsl" : void 0);
|
|
2437
|
+
throw new Bail();
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
declare(kind, id, span, b, taken, detail) {
|
|
2441
|
+
if (FORBIDDEN.has(id)) this.report.error(span, `"${id}" cannot be used as a name`);
|
|
2442
|
+
if (taken.has(id)) this.report.error(span, `"${id}" is declared twice`, "rename one of them");
|
|
2443
|
+
else taken.set(id, span);
|
|
2444
|
+
this.symbols.push({
|
|
2445
|
+
kind,
|
|
2446
|
+
id,
|
|
2447
|
+
span,
|
|
2448
|
+
decl: b.decl ?? b.at,
|
|
2449
|
+
detail
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
box(b) {
|
|
2453
|
+
const id = b.args.id.name;
|
|
2454
|
+
const stageBlocks = b.children.filter((c) => STAGES.includes(c.keyword));
|
|
2455
|
+
const stages = stageBlocks.map((s) => ({
|
|
2456
|
+
block: s,
|
|
2457
|
+
id: s.args.id?.name ?? id
|
|
2458
|
+
}));
|
|
2459
|
+
const ownLines = Object.keys(COMPONENT_PROPS).some((k) => b.props.has(k));
|
|
2460
|
+
const nested = b.children.filter((c) => c.keyword === "component");
|
|
2461
|
+
let component;
|
|
2462
|
+
if (ownLines) component = {
|
|
2463
|
+
block: b,
|
|
2464
|
+
id,
|
|
2465
|
+
own: true
|
|
2466
|
+
};
|
|
2467
|
+
for (const n of nested) if (component) this.report.error(n.at, `"${id}" already has a component`, "one component per box; put others at the top");
|
|
2468
|
+
else component = {
|
|
2469
|
+
block: n,
|
|
2470
|
+
id: n.args.id?.name ?? id,
|
|
2471
|
+
own: false
|
|
2472
|
+
};
|
|
2473
|
+
if (b.keyword === "node" && stages.length > 1) this.report.error(stageBlocks[1].at, "a node runs at most one stage; a cluster can hold several queues");
|
|
2474
|
+
if (b.keyword === "cluster" && !stages.length) this.report.error(b.at, "a cluster needs a queue", "queue topic { partitions { orders: 12 } }");
|
|
2475
|
+
if (b.keyword === "pool" && stages.length !== 1) this.report.error(b.at, "a pool needs exactly one workers stage", "workers { pods: 3; perPod: 8 }");
|
|
2476
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2477
|
+
for (const s of stages) {
|
|
2478
|
+
if (ids.has(s.id)) this.report.error(s.block.at, `"${id}" has two stages called "${s.id}"`, "give each an id");
|
|
2479
|
+
ids.add(s.id);
|
|
2480
|
+
}
|
|
2481
|
+
return {
|
|
2482
|
+
block: b,
|
|
2483
|
+
id,
|
|
2484
|
+
stages,
|
|
2485
|
+
component
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
slice(s) {
|
|
2489
|
+
return this.source.slice(s.from, s.to);
|
|
2490
|
+
}
|
|
2491
|
+
prop(b, key) {
|
|
2492
|
+
return b.props.get(key)?.[0];
|
|
2493
|
+
}
|
|
2494
|
+
formula(e, scope, want) {
|
|
2495
|
+
const f = compileFormula(e, this.names, scope, this.report);
|
|
2496
|
+
if (want && want !== "any" && f.type !== want && f.type !== "any") {
|
|
2497
|
+
if (want === "string" && f.type === "number") {
|
|
2498
|
+
const run = f.run;
|
|
2499
|
+
return {
|
|
2500
|
+
...f,
|
|
2501
|
+
type: "string",
|
|
2502
|
+
run: (c, l) => String(run(c, l))
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
if (want === "capacity" && f.type === "number") this.report.error(e, "a capacity says where it came from", `write measured(${this.slice(e)}, "unit", "source") or estimated(…)`);
|
|
2506
|
+
else this.report.error(e, `expected ${typeWord(want)}, this is ${typeWord(f.type)}`);
|
|
2507
|
+
}
|
|
2508
|
+
return f;
|
|
2509
|
+
}
|
|
2510
|
+
/** A value that must not depend on the model. */
|
|
2511
|
+
constant(e, want) {
|
|
2512
|
+
const f = this.formula(e, "const", want);
|
|
2513
|
+
try {
|
|
2514
|
+
return f.run(void 0);
|
|
2515
|
+
} catch (err) {
|
|
2516
|
+
this.report.error(e, err instanceof Error ? err.message : String(err));
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
constProp(b, key) {
|
|
2521
|
+
const p = this.prop(b, key);
|
|
2522
|
+
if (!p) return void 0;
|
|
2523
|
+
const spec = b.spec.props[key];
|
|
2524
|
+
if (spec.value === "enum") return this.enumValue(p, spec);
|
|
2525
|
+
return this.constant(p.value, spec.value);
|
|
2526
|
+
}
|
|
2527
|
+
/**
|
|
2528
|
+
* A plain or computed value: a constant is computed now, so the model sees it as it would from TypeScript. `check`
|
|
2529
|
+
* throws on a value the drawing cannot take (a count too large); at run time its error carries the formula's place.
|
|
2530
|
+
*/
|
|
2531
|
+
dyn(e, want, scope = "ctx", check) {
|
|
2532
|
+
const f = this.formula(e, scope, want);
|
|
2533
|
+
if (f.constant) {
|
|
2534
|
+
const v = this.constant(e, want);
|
|
2535
|
+
try {
|
|
2536
|
+
if (v !== void 0) check?.(v);
|
|
2537
|
+
} catch (err) {
|
|
2538
|
+
this.report.error(e, err.message);
|
|
2539
|
+
}
|
|
2540
|
+
return v;
|
|
2541
|
+
}
|
|
2542
|
+
const run = f.run;
|
|
2543
|
+
if (!check) return placed(e, run);
|
|
2544
|
+
return placed(e, (c, l) => {
|
|
2545
|
+
const v = run(c, l);
|
|
2546
|
+
check(v);
|
|
2547
|
+
return v;
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
dynProp(b, key, want, check) {
|
|
2551
|
+
const p = this.prop(b, key);
|
|
2552
|
+
if (!p) return void 0;
|
|
2553
|
+
if (key === "rate" && b.args.id) this.flowRate.set(b.args.id.name, p.value);
|
|
2554
|
+
return this.dyn(p.value, want, own(b.spec.props, key)?.scope ?? "ctx", check);
|
|
2555
|
+
}
|
|
2556
|
+
/** A label or name argument: a plain text, or a template when the argument may be computed. */
|
|
2557
|
+
text(e) {
|
|
2558
|
+
if (!e) return void 0;
|
|
2559
|
+
const v = this.constant(e, "string");
|
|
2560
|
+
return typeof v === "string" ? v : void 0;
|
|
2561
|
+
}
|
|
2562
|
+
label(e) {
|
|
2563
|
+
if (!e) return void 0;
|
|
2564
|
+
return this.dyn(e, "string");
|
|
2565
|
+
}
|
|
2566
|
+
enumValue(p, spec) {
|
|
2567
|
+
const raw = p.value.t === "str" ? p.value.value : this.slice(p.value).replace(/\s+/g, "");
|
|
2568
|
+
if (spec.values?.includes(raw)) return raw;
|
|
2569
|
+
this.report.error(p.value, `"${raw}" is not one of ${spec.values.join(", ")}`, didYouMean(raw, spec.values ?? []));
|
|
2570
|
+
}
|
|
2571
|
+
color(e) {
|
|
2572
|
+
if (e.t === "id") {
|
|
2573
|
+
if (COLORS.includes(e.name)) return e.name;
|
|
2574
|
+
this.report.error(e, `"${e.name}" is not a theme colour`, didYouMean(e.name, COLORS) ?? "or quote a CSS colour");
|
|
2575
|
+
} else if (e.t === "str") {
|
|
2576
|
+
if (CSS_COLOR.test(e.value.trim())) return e.value.trim();
|
|
2577
|
+
this.report.error(e, `"${e.value}" is not a colour`, "use #rrggbb, rgb(…), hsl(…) or a theme colour");
|
|
2578
|
+
} else this.report.error(e, "a colour is a theme colour or a CSS colour in quotes");
|
|
2579
|
+
return "accent";
|
|
2580
|
+
}
|
|
2581
|
+
count(what) {
|
|
2582
|
+
const max = LIMITS[what];
|
|
2583
|
+
return (v) => {
|
|
2584
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0) throw new Error(`${what} must be a whole number of at least 0, not ${String(v)}`);
|
|
2585
|
+
if (v > max) throw new Error(`${what} is ${v}; at most ${max}`);
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
load(b) {
|
|
2589
|
+
const o = {};
|
|
2590
|
+
const label = this.text(b.args.label);
|
|
2591
|
+
if (label != null) o.label = label;
|
|
2592
|
+
for (const k of [
|
|
2593
|
+
"min",
|
|
2594
|
+
"max",
|
|
2595
|
+
"step",
|
|
2596
|
+
"default"
|
|
2597
|
+
]) {
|
|
2598
|
+
const v = this.constProp(b, k);
|
|
2599
|
+
if (v != null) o[k] = v;
|
|
2600
|
+
}
|
|
2601
|
+
if (typeof o.max !== "number") o.max = 1;
|
|
2602
|
+
return p.load(o);
|
|
2603
|
+
}
|
|
2604
|
+
paramType(b) {
|
|
2605
|
+
const options = this.prop(b, "options");
|
|
2606
|
+
if (options?.value.t === "array") return options.value.items[0]?.t === "str" ? "string" : "number";
|
|
2607
|
+
if (b.props.has("min") || b.props.has("max")) return "number";
|
|
2608
|
+
const d = this.prop(b, "default")?.value;
|
|
2609
|
+
return d?.t === "bool" ? "boolean" : d?.t === "str" ? "string" : d?.t === "num" ? "number" : "any";
|
|
2610
|
+
}
|
|
2611
|
+
param(b) {
|
|
2612
|
+
const o = { label: this.text(b.args.label) ?? b.args.id.name };
|
|
2613
|
+
for (const k of ["hint", "tag"]) {
|
|
2614
|
+
const v = this.constProp(b, k);
|
|
2615
|
+
if (v != null) o[k] = v;
|
|
2616
|
+
}
|
|
2617
|
+
const enabled = this.prop(b, "enabled");
|
|
2618
|
+
if (enabled) o.enabled = placed(enabled.value, this.formula(enabled.value, "values").run);
|
|
2619
|
+
const format = this.prop(b, "format");
|
|
2620
|
+
if (format) {
|
|
2621
|
+
const run = placed(format.value, this.formula(format.value, "value", "string").run);
|
|
2622
|
+
o.format = (v) => run(void 0, v);
|
|
2623
|
+
}
|
|
2624
|
+
const dflt = this.prop(b, "default");
|
|
2625
|
+
if (!dflt) return void 0;
|
|
2626
|
+
const value = this.constant(dflt.value, "any");
|
|
2627
|
+
const options = this.prop(b, "options");
|
|
2628
|
+
if (options) {
|
|
2629
|
+
for (const k of [
|
|
2630
|
+
"min",
|
|
2631
|
+
"max",
|
|
2632
|
+
"step"
|
|
2633
|
+
]) if (b.props.has(k)) this.report.error(this.prop(b, k).key, `"${k}" is for a slider; this param has options`);
|
|
2634
|
+
const list = this.options(options);
|
|
2635
|
+
if (!list) return void 0;
|
|
2636
|
+
if (!list.includes(value)) {
|
|
2637
|
+
this.report.error(dflt.value, `the default ${this.slice(dflt.value)} is not one of the options`, list.join(", "));
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
return p.select({
|
|
2641
|
+
...o,
|
|
2642
|
+
options: list,
|
|
2643
|
+
default: value
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
if (b.props.has("min") || b.props.has("max")) {
|
|
2647
|
+
for (const k of ["min", "max"]) if (!b.props.has(k)) this.report.error(b.at, `a slider needs "${k}: …"`);
|
|
2648
|
+
const r = { ...o };
|
|
2649
|
+
for (const k of [
|
|
2650
|
+
"min",
|
|
2651
|
+
"max",
|
|
2652
|
+
"step"
|
|
2653
|
+
]) {
|
|
2654
|
+
const v = this.constProp(b, k);
|
|
2655
|
+
if (v != null) r[k] = v;
|
|
2656
|
+
}
|
|
2657
|
+
if (typeof value !== "number") this.report.error(dflt.value, "the default of a slider is a number");
|
|
2658
|
+
return p.range({
|
|
2659
|
+
...r,
|
|
2660
|
+
default: value
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
if (typeof value === "boolean") return p.switch({
|
|
2664
|
+
...o,
|
|
2665
|
+
default: value
|
|
2666
|
+
});
|
|
2667
|
+
this.report.error(b.at, `"${b.args.id.name}" is neither a select, a slider nor a switch`, "give it options: [...], min and max, or a default of true or false");
|
|
2668
|
+
}
|
|
2669
|
+
options(prop) {
|
|
2670
|
+
if (prop.value.t !== "array" || !prop.value.items.length) {
|
|
2671
|
+
this.report.error(prop.value, "options are a list: [2, 3, 4] or [\"a\", \"b\"]");
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
const list = prop.value.items.map((e) => this.constant(e, "any"));
|
|
2675
|
+
const kind = typeof list[0];
|
|
2676
|
+
if (!list.every((v) => (typeof v === "number" || typeof v === "string") && typeof v === kind)) {
|
|
2677
|
+
this.report.error(prop.value, "options are all numbers or all texts");
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
return list;
|
|
2681
|
+
}
|
|
2682
|
+
/** A preset's values, or a scenario's: the same, but a scenario leaves the load to the slider. */
|
|
2683
|
+
preset(b, params, kind = "preset") {
|
|
2684
|
+
const out = {};
|
|
2685
|
+
const known = new Map(params.filter((x) => x.args.id).map((x) => [x.args.id.name, x]));
|
|
2686
|
+
for (const [k, props] of b.props) {
|
|
2687
|
+
if (own(b.spec.props, k)) continue;
|
|
2688
|
+
const prop = props[0];
|
|
2689
|
+
const v = this.constant(prop.value, "any");
|
|
2690
|
+
if (k === "load" && kind === "scenario") {
|
|
2691
|
+
this.report.error(prop.key, "a scenario leaves the load to the slider", "use a preset for a load");
|
|
2692
|
+
continue;
|
|
2693
|
+
}
|
|
2694
|
+
if (k !== "load" && !known.has(k)) {
|
|
2695
|
+
const names = [...kind === "preset" ? ["load"] : [], ...known.keys()];
|
|
2696
|
+
this.report.error(prop.key, `unknown parameter "${k}"`, didYouMean(k, names));
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2699
|
+
const type = this.names.params.get(k);
|
|
2700
|
+
if (type && type !== "any" && typeof v !== type) {
|
|
2701
|
+
this.report.error(prop.value, `"${k}" takes ${typeWord(type)}`);
|
|
2702
|
+
continue;
|
|
2703
|
+
}
|
|
2704
|
+
const opts = known.get(k) && this.prop(known.get(k), "options");
|
|
2705
|
+
if (opts?.value.t === "array") {
|
|
2706
|
+
const list = opts.value.items.map((e) => e.t === "num" || e.t === "str" ? e.value : void 0);
|
|
2707
|
+
if (!list.includes(v)) this.report.error(prop.value, `${String(v)} is not an option of "${k}"`, list.join(", "));
|
|
2708
|
+
}
|
|
2709
|
+
out[k] = v;
|
|
2710
|
+
}
|
|
2711
|
+
return out;
|
|
2712
|
+
}
|
|
2713
|
+
/** An `expect` line as the model takes it: names checked here, so a mistake points at its word. */
|
|
2714
|
+
expectation(b, scenarios, components) {
|
|
2715
|
+
const e = {};
|
|
2716
|
+
const label = this.text(b.args.label);
|
|
2717
|
+
if (label != null) e.label = label;
|
|
2718
|
+
const scenario = this.named(b, "scenario", Object.keys(scenarios), "scenario");
|
|
2719
|
+
const component = this.named(b, "component", [...components.keys()], "component");
|
|
2720
|
+
if (scenario === null || component === null) return void 0;
|
|
2721
|
+
if (scenario != null) e.scenario = scenario;
|
|
2722
|
+
if (component != null) e.component = component;
|
|
2723
|
+
for (const k of ["target", "full"]) {
|
|
2724
|
+
const v = this.constProp(b, k);
|
|
2725
|
+
if (v == null) continue;
|
|
2726
|
+
if (!(v > 0 && Number.isFinite(v))) this.report.error(this.prop(b, k).value, `${k} is a load multiple above 0`);
|
|
2727
|
+
else e[k] = v;
|
|
2728
|
+
}
|
|
2729
|
+
if (!b.props.has("target") && !b.props.has("full")) {
|
|
2730
|
+
this.report.error(b.at, "an expect needs a target or a full multiple", "expect { target: 8 }");
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
return e.target != null || e.full != null ? e : void 0;
|
|
2734
|
+
}
|
|
2735
|
+
/** A property naming a scenario or a component: undefined when absent, null when wrong (reported). */
|
|
2736
|
+
named(b, key, known, what) {
|
|
2737
|
+
const p = this.prop(b, key);
|
|
2738
|
+
if (!p) return void 0;
|
|
2739
|
+
const v = p.value.t === "id" ? p.value.name : p.value.t === "str" ? p.value.value : void 0;
|
|
2740
|
+
if (v == null) this.report.error(p.value, `${key} is the name of a ${what}`);
|
|
2741
|
+
else if (!known.includes(v)) this.report.error(p.value, `unknown ${what} "${v}"`, didYouMean(v, known));
|
|
2742
|
+
else return v;
|
|
2743
|
+
return null;
|
|
2744
|
+
}
|
|
2745
|
+
/** A story step as the model takes it. */
|
|
2746
|
+
step(b, scenarios, components, params, load) {
|
|
2747
|
+
const text = this.text(b.args.label);
|
|
2748
|
+
for (const k of Object.keys(b.spec.props)) {
|
|
2749
|
+
const at = b.props.get(k)?.[0];
|
|
2750
|
+
if (at && params.some((x) => x.args.id?.name === k)) this.report.error(at.key, `"${k}" in a step is the step's own ${k}, not the parameter "${k}"`, "rename the parameter to set it in a step");
|
|
2751
|
+
}
|
|
2752
|
+
const scenario = this.named(b, "scenario", Object.keys(scenarios), "scenario");
|
|
2753
|
+
const focus = this.named(b, "focus", [...components.keys()], "component");
|
|
2754
|
+
const seconds = this.constProp(b, "seconds");
|
|
2755
|
+
if (seconds != null && !(seconds > 0 && Number.isFinite(seconds))) this.report.error(this.prop(b, "seconds").value, "seconds is a duration above 0");
|
|
2756
|
+
const set = this.preset(b, params);
|
|
2757
|
+
const l = this.prop(b, "load");
|
|
2758
|
+
if (l && load && typeof set.load === "number" && !(set.load >= load.min && set.load <= load.max)) this.report.error(l.value, `the load goes from ${load.min} to ${load.max}`);
|
|
2759
|
+
if (text == null || scenario === null || focus === null) return void 0;
|
|
2760
|
+
return {
|
|
2761
|
+
text,
|
|
2762
|
+
...scenario != null ? { scenario } : {},
|
|
2763
|
+
...focus != null ? { focus } : {},
|
|
2764
|
+
...seconds != null && seconds > 0 ? { seconds } : {},
|
|
2765
|
+
...Object.keys(set).length ? { set } : {}
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
growth(b) {
|
|
2769
|
+
const rate = this.constProp(b, "rate");
|
|
2770
|
+
const per = this.constProp(b, "per");
|
|
2771
|
+
const asOf = this.constProp(b, "asOf");
|
|
2772
|
+
if (rate != null && !(rate > 0)) this.report.error(this.prop(b, "rate").value, "growth needs a rate above 0");
|
|
2773
|
+
if (asOf != null && !parseAsOf(asOf)) this.report.error(this.prop(b, "asOf").value, `"${asOf}" is not a date`, "a day like \"2026-11-01\"");
|
|
2774
|
+
if (rate == null || per == null || asOf == null) return void 0;
|
|
2775
|
+
return {
|
|
2776
|
+
rate,
|
|
2777
|
+
per,
|
|
2778
|
+
asOf
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
fit(b) {
|
|
2782
|
+
const id = b.args.id;
|
|
2783
|
+
if (!id) return;
|
|
2784
|
+
this.fits.set(id.name, null);
|
|
2785
|
+
const pts = b.children.filter((c) => c.keyword === "points");
|
|
2786
|
+
if (pts.length !== 1) {
|
|
2787
|
+
this.report.error(b.at, pts.length ? "a fit has one points block" : "a fit needs its points", usage("points"));
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
const block = pts[0];
|
|
2791
|
+
const { x, y } = block.args;
|
|
2792
|
+
if (!x || !y) return;
|
|
2793
|
+
if (x.name === y.name) this.report.error(y, "x and y need different names");
|
|
2794
|
+
const points = [];
|
|
2795
|
+
for (const r of block.rows) {
|
|
2796
|
+
if (r.values.length !== 2) {
|
|
2797
|
+
this.report.error(r, `a row is two numbers: ${x.name} and ${y.name}`);
|
|
2798
|
+
continue;
|
|
2799
|
+
}
|
|
2800
|
+
const [a, c] = r.values.map((v) => this.constant(v, "number"));
|
|
2801
|
+
points.push({
|
|
2802
|
+
[x.name]: a,
|
|
2803
|
+
[y.name]: c
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
const source = this.constProp(b, "source");
|
|
2807
|
+
const through = this.constProp(b, "through");
|
|
2808
|
+
try {
|
|
2809
|
+
const o = {
|
|
2810
|
+
x: x.name,
|
|
2811
|
+
y: y.name
|
|
2812
|
+
};
|
|
2813
|
+
if (source != null) o.source = source;
|
|
2814
|
+
if (through) o.through = through;
|
|
2815
|
+
this.fits.set(id.name, fitLinear(points, o));
|
|
2816
|
+
} catch (err) {
|
|
2817
|
+
this.report.error(block.at, err.message.replace(/^loadmap: /, ""));
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
overload(b) {
|
|
2821
|
+
const rules = b.children.filter((c) => c.keyword === "drop" || c.keyword === "merge");
|
|
2822
|
+
if (!rules.length) return void 0;
|
|
2823
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2824
|
+
const parts = [];
|
|
2825
|
+
for (const r of rules) {
|
|
2826
|
+
const key = r.keyword;
|
|
2827
|
+
if (seen.has(key)) this.report.error(r.at, `"${key}" is set twice in this flow`);
|
|
2828
|
+
seen.add(key);
|
|
2829
|
+
const fields = [];
|
|
2830
|
+
const first = key === "drop" ? "olderThan" : "share";
|
|
2831
|
+
for (const k of [first, "reason"]) {
|
|
2832
|
+
const v = this.dynProp(r, k, k === "reason" ? "string" : "number");
|
|
2833
|
+
if (v !== void 0) fields.push([k, v]);
|
|
2834
|
+
}
|
|
2835
|
+
const when = this.prop(r, "when");
|
|
2836
|
+
parts.push({
|
|
2837
|
+
key,
|
|
2838
|
+
fields,
|
|
2839
|
+
when: when && placed(when.value, this.formula(when.value, "ctx").run)
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
const fixed = parts.every((x) => !x.when && x.fields.every(([, v]) => typeof v !== "function"));
|
|
2843
|
+
const at = (ctx) => {
|
|
2844
|
+
const o = {};
|
|
2845
|
+
for (const x of parts) {
|
|
2846
|
+
if (x.when && !x.when(ctx)) continue;
|
|
2847
|
+
const rule = {};
|
|
2848
|
+
for (const [k, v] of x.fields) rule[k] = typeof v === "function" ? v(ctx) : v;
|
|
2849
|
+
o[x.key] = rule;
|
|
2850
|
+
}
|
|
2851
|
+
return o;
|
|
2852
|
+
};
|
|
2853
|
+
return fixed ? at(void 0) : at;
|
|
2854
|
+
}
|
|
2855
|
+
component(b, box) {
|
|
2856
|
+
const o = {};
|
|
2857
|
+
const name = this.constProp(b, "name") ?? this.text(b.args.label) ?? this.text(box?.args.label);
|
|
2858
|
+
if (name == null) {
|
|
2859
|
+
const label = box?.args.label ?? b.args.label;
|
|
2860
|
+
if (label) this.report.error(label, "a computed label cannot name the component", "add name: \"…\"");
|
|
2861
|
+
}
|
|
2862
|
+
o.name = name ?? (b.args.id ?? box?.args.id)?.name;
|
|
2863
|
+
for (const k of ["short", "plural"]) {
|
|
2864
|
+
const v = this.constProp(b, k);
|
|
2865
|
+
if (v != null) o[k] = v;
|
|
2866
|
+
}
|
|
2867
|
+
for (const [k, want] of [
|
|
2868
|
+
["demand", "number"],
|
|
2869
|
+
["capacity", "capacity"],
|
|
2870
|
+
["when", "any"],
|
|
2871
|
+
["over", "string"],
|
|
2872
|
+
["describe", "string"]
|
|
2873
|
+
]) {
|
|
2874
|
+
const prop = this.prop(b, k);
|
|
2875
|
+
if (!prop) continue;
|
|
2876
|
+
const check = k === "capacity" ? (v) => {
|
|
2877
|
+
if (!isCapacity(v)) throw new Error("a capacity comes from measured(…) or estimated(…)");
|
|
2878
|
+
} : void 0;
|
|
2879
|
+
o[k] = this.dyn(prop.value, want, "ctx", check);
|
|
2880
|
+
}
|
|
2881
|
+
const at = box ? box.args.id ?? box.at : b.at;
|
|
2882
|
+
for (const k of [
|
|
2883
|
+
"demand",
|
|
2884
|
+
"capacity",
|
|
2885
|
+
"over"
|
|
2886
|
+
]) if (!(k in o)) this.report.error(at, `the component needs "${k}: …"`, COMPONENT_PROPS[k].doc);
|
|
2887
|
+
return o;
|
|
2888
|
+
}
|
|
2889
|
+
stage(b, box, components, resolve, flows, reached) {
|
|
2890
|
+
const label = this.text(b.args.label);
|
|
2891
|
+
const base = label != null ? { label } : {};
|
|
2892
|
+
let component = box.component?.id;
|
|
2893
|
+
const named = this.prop(b, "component");
|
|
2894
|
+
if (named) {
|
|
2895
|
+
const n = named.value;
|
|
2896
|
+
if (n.t === "id" && components.has(n.name)) component = n.name;
|
|
2897
|
+
else this.report.error(n, `unknown component "${this.slice(n)}"`, didYouMean(this.slice(n), components.keys()));
|
|
2898
|
+
}
|
|
2899
|
+
const needs = () => {
|
|
2900
|
+
this.report.error(b.at, `a ${b.keyword} stage needs a component`, "give the box demand and capacity lines, or name one: component: db");
|
|
2901
|
+
};
|
|
2902
|
+
switch (b.keyword) {
|
|
2903
|
+
case "pass": return component ? pass(component, base) : needs();
|
|
2904
|
+
case "store": return component ? store(component, base) : needs();
|
|
2905
|
+
case "sink": return sink(base);
|
|
2906
|
+
case "workers": {
|
|
2907
|
+
if (!component) return needs();
|
|
2908
|
+
const pods = this.dynProp(b, "pods", "number", this.count("pods"));
|
|
2909
|
+
const perPod = this.dynProp(b, "perPod", "number", this.count("perPod"));
|
|
2910
|
+
const fill = this.constProp(b, "fill");
|
|
2911
|
+
return workers(component, {
|
|
2912
|
+
...base,
|
|
2913
|
+
pods: pods ?? 0,
|
|
2914
|
+
perPod: perPod ?? 0,
|
|
2915
|
+
...fill ? { fill } : {}
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
case "queue": {
|
|
2919
|
+
const block = b.children.find((c) => c.keyword === "partitions");
|
|
2920
|
+
if (!block) {
|
|
2921
|
+
this.report.error(b.at, "a queue needs its partitions", "partitions { orders: 12 }");
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
const entries = [];
|
|
2925
|
+
for (const [f, props] of block.props) {
|
|
2926
|
+
const prop = props[0];
|
|
2927
|
+
if (!flows.has(f)) {
|
|
2928
|
+
this.report.error(prop.key, `unknown flow "${f}"`, didYouMean(f, flows.keys()));
|
|
2929
|
+
continue;
|
|
2930
|
+
}
|
|
2931
|
+
entries.push([f, this.dyn(prop.value, "number", "ctx", this.count("partitions"))]);
|
|
2932
|
+
}
|
|
2933
|
+
const fixed = entries.every(([, v]) => typeof v === "number");
|
|
2934
|
+
const at = (ctx) => Object.fromEntries(entries.map(([k, v]) => [k, typeof v === "function" ? v(ctx) : v]));
|
|
2935
|
+
const partitions = fixed ? at(void 0) : at;
|
|
2936
|
+
return queue({
|
|
2937
|
+
...base,
|
|
2938
|
+
partitions,
|
|
2939
|
+
...component ? { component } : {}
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
case "fanout": {
|
|
2943
|
+
const branches = [];
|
|
2944
|
+
for (const e of b.edges) {
|
|
2945
|
+
if (e.chain.length !== 2) {
|
|
2946
|
+
this.report.error(e, "a branch is one \"flow -> stage: copies\" line");
|
|
2947
|
+
continue;
|
|
2948
|
+
}
|
|
2949
|
+
const [f, s] = e.chain;
|
|
2950
|
+
if (!flows.has(f.name)) this.report.error(f, `unknown flow "${f.name}"`, didYouMean(f.name, flows.keys()));
|
|
2951
|
+
const stage = resolve(s, "stage");
|
|
2952
|
+
if (stage) reached.add(stage);
|
|
2953
|
+
if (!e.value) this.report.error(e, "how many copies per unit?", `${f.name} -> ${s.name}: 1`);
|
|
2954
|
+
if (e.body) this.report.error(e.bodySpan ?? e, "a branch has no block");
|
|
2955
|
+
if (stage && e.value && flows.has(f.name)) branches.push([
|
|
2956
|
+
f.name,
|
|
2957
|
+
stage,
|
|
2958
|
+
this.dyn(e.value, "number")
|
|
2959
|
+
]);
|
|
2960
|
+
}
|
|
2961
|
+
const build = (ctx) => {
|
|
2962
|
+
const to = {};
|
|
2963
|
+
for (const [f, stage, share] of branches) {
|
|
2964
|
+
const list = to[f] ?? [];
|
|
2965
|
+
list.push({
|
|
2966
|
+
stage,
|
|
2967
|
+
share: typeof share === "function" ? share(ctx) : share
|
|
2968
|
+
});
|
|
2969
|
+
to[f] = list;
|
|
2970
|
+
}
|
|
2971
|
+
return to;
|
|
2972
|
+
};
|
|
2973
|
+
const fixed = branches.every(([, , v]) => typeof v === "number");
|
|
2974
|
+
return fanout({
|
|
2975
|
+
...base,
|
|
2976
|
+
to: fixed ? build(void 0) : build,
|
|
2977
|
+
...component ? { component } : {}
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
node(box) {
|
|
2983
|
+
const b = box.block;
|
|
2984
|
+
const label = this.label(b.args.label) ?? box.id;
|
|
2985
|
+
const subProp = this.prop(b, "sub");
|
|
2986
|
+
if (subProp && b.args.sub) this.report.error(subProp.key, "the sub-line is set twice: on the first line and here");
|
|
2987
|
+
const sub = this.label(b.args.sub ?? subProp?.value);
|
|
2988
|
+
const o = { label };
|
|
2989
|
+
if (sub != null) o.sub = sub;
|
|
2990
|
+
const indexLabel = (key) => {
|
|
2991
|
+
const prop = this.prop(b, key);
|
|
2992
|
+
if (!prop) return void 0;
|
|
2993
|
+
const run = placed(prop.value, this.formula(prop.value, "i", "string").run);
|
|
2994
|
+
return (i) => run(void 0, i);
|
|
2995
|
+
};
|
|
2996
|
+
const stageIds = box.stages.map((s) => s.id);
|
|
2997
|
+
if (b.keyword === "cluster") {
|
|
2998
|
+
o.stage = stageIds.length === 1 ? stageIds[0] : stageIds;
|
|
2999
|
+
const members = this.prop(b, "members");
|
|
3000
|
+
o.members = members ? this.dyn(members.value, "number", "ctx", this.count("members")) : 1;
|
|
3001
|
+
const memberLabel = indexLabel("memberLabel");
|
|
3002
|
+
if (memberLabel) o.memberLabel = memberLabel;
|
|
3003
|
+
return {
|
|
3004
|
+
kind: "cluster",
|
|
3005
|
+
...o
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
if (b.keyword === "pool") {
|
|
3009
|
+
o.stage = stageIds[0];
|
|
3010
|
+
const podLabel = indexLabel("podLabel");
|
|
3011
|
+
if (podLabel) o.podLabel = podLabel;
|
|
3012
|
+
return {
|
|
3013
|
+
kind: "pool",
|
|
3014
|
+
...o
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
if (stageIds.length) o.stage = stageIds[0];
|
|
3018
|
+
else if (box.component) o.component = box.component.id;
|
|
3019
|
+
return {
|
|
3020
|
+
kind: "node",
|
|
3021
|
+
shape: this.constProp(b, "shape") ?? "box",
|
|
3022
|
+
...o
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
edges(e, boxes) {
|
|
3026
|
+
for (const n of e.chain) if (!boxes.has(n.name)) this.report.error(n, `unknown node "${n.name}"`, didYouMean(n.name, boxes.keys()));
|
|
3027
|
+
const props = /* @__PURE__ */ new Map();
|
|
3028
|
+
for (const s of e.body ?? []) {
|
|
3029
|
+
if (s.kind !== "prop" || !own(EDGE_PROPS, s.key.name)) {
|
|
3030
|
+
const at = s.kind === "prop" ? s.key : s.kind === "decl" ? s.keyword : s;
|
|
3031
|
+
this.report.error(at, "an edge holds only label and when", didYouMean(this.slice(at), Object.keys(EDGE_PROPS)));
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
if (props.has(s.key.name)) this.report.error(s.key, `"${s.key.name}" is set twice on this edge`);
|
|
3035
|
+
props.set(s.key.name, s);
|
|
3036
|
+
}
|
|
3037
|
+
const labelExpr = e.value ?? props.get("label")?.value;
|
|
3038
|
+
if (e.value && props.has("label")) this.report.error(props.get("label").key, "the label is set twice");
|
|
3039
|
+
if (labelExpr && e.chain.length > 2) this.report.error(labelExpr, "a label goes on a single edge: a -> b: \"…\"");
|
|
3040
|
+
const label = this.label(labelExpr);
|
|
3041
|
+
const when = props.get("when");
|
|
3042
|
+
const out = [];
|
|
3043
|
+
for (let i = 0; i + 1 < e.chain.length; i++) {
|
|
3044
|
+
const d = {
|
|
3045
|
+
from: e.chain[i].name,
|
|
3046
|
+
to: e.chain[i + 1].name
|
|
3047
|
+
};
|
|
3048
|
+
if (label != null) d.label = label;
|
|
3049
|
+
if (when) d.when = placed(when.value, this.formula(when.value, "ctx").run);
|
|
3050
|
+
out.push(d);
|
|
3051
|
+
}
|
|
3052
|
+
return out;
|
|
3053
|
+
}
|
|
3054
|
+
};
|
|
3055
|
+
const FORBIDDEN = /* @__PURE__ */ new Set([...Object.getOwnPropertyNames(Object.prototype), "prototype"]);
|
|
3056
|
+
const CSS_COLOR = /^(#[0-9a-f]{3,8}|(rgb|hsl)a?\([\d\s.,%/+-]+\))$/i;
|
|
3057
|
+
function usage(keyword) {
|
|
3058
|
+
const ex = own(SCHEMA, keyword)?.example;
|
|
3059
|
+
return ex ? `e.g. ${ex.split("\n")[0]}` : void 0;
|
|
3060
|
+
}
|
|
3061
|
+
function where(keyword) {
|
|
3062
|
+
const parents = Object.entries(SCHEMA).filter(([, s]) => s.children.includes(keyword)).map(([k]) => k === "" ? "the top level" : k);
|
|
3063
|
+
return parents.length ? `it goes in ${parents.join(" or ")}` : void 0;
|
|
3064
|
+
}
|
|
3065
|
+
export { formatDiagnostic as C, didYouMean as S, SCHEMA as _, MAX_DEPTH as a, Reporter as b, parseExpression as c, BUILTINS as d, FormulaError as f, EDGE_PROPS as g, COMPONENT_PROPS as h, problems as i, LEXICON as l, COLORS as m, LIMITS as n, MAX_SOURCE as o, BOXES as p, compile as r, parse as s, LANGUAGE_VERSION as t, lex as u, STAGES as v, closest as x, own as y };
|