@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.
@@ -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 };