@loadmap/dsl 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +15 -2
- package/dist/compile-6wNrbYqA.js +3065 -0
- package/dist/compile-C2Kdi-Ul.d.ts +212 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +262 -0
- package/dist/internal.d.ts +94 -0
- package/dist/internal.js +44 -0
- package/package.json +40 -3
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { CheckResult, Model } from "@loadmap/core";
|
|
2
|
+
import { Direction, View } from "@loadmap/layout";
|
|
3
|
+
/** A range of the source, as offsets: `from` inclusive, `to` exclusive. */
|
|
4
|
+
interface Span {
|
|
5
|
+
from: number;
|
|
6
|
+
to: number;
|
|
7
|
+
}
|
|
8
|
+
interface Diagnostic extends Span {
|
|
9
|
+
severity: 'error' | 'warning';
|
|
10
|
+
message: string;
|
|
11
|
+
/** 1-based, like an editor shows them. */
|
|
12
|
+
line: number;
|
|
13
|
+
column: number;
|
|
14
|
+
/** A short suggestion shown under the mark: "did you mean "orders"?". */
|
|
15
|
+
hint?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Collects diagnostics and turns offsets into lines and columns. */
|
|
18
|
+
declare class Reporter {
|
|
19
|
+
readonly source: string;
|
|
20
|
+
readonly list: Diagnostic[];
|
|
21
|
+
private seen;
|
|
22
|
+
private starts;
|
|
23
|
+
constructor(source: string);
|
|
24
|
+
error(at: Span, message: string, hint?: string): void;
|
|
25
|
+
warning(at: Span, message: string, hint?: string): void;
|
|
26
|
+
/** The 1-based line and column of an offset. */
|
|
27
|
+
position(offset: number): {
|
|
28
|
+
line: number;
|
|
29
|
+
column: number;
|
|
30
|
+
};
|
|
31
|
+
get failed(): boolean;
|
|
32
|
+
private add;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One diagnostic as a terminal shows it:
|
|
36
|
+
*
|
|
37
|
+
* checkout.loadmap:24:17 error: unknown flow "order"
|
|
38
|
+
* 24 | demand: flows.order + flows.events
|
|
39
|
+
* | ^^^^^ did you mean "orders"?
|
|
40
|
+
*/
|
|
41
|
+
declare function formatDiagnostic(d: Diagnostic, source: string, file?: string): string;
|
|
42
|
+
/** The closest of `options` to `name`, when it is close enough to be a typo. */
|
|
43
|
+
declare function closest(name: string, options: Iterable<string>): string | undefined;
|
|
44
|
+
declare function didYouMean(name: string, options: Iterable<string>): string | undefined;
|
|
45
|
+
interface Ident extends Span {
|
|
46
|
+
name: string;
|
|
47
|
+
}
|
|
48
|
+
type Expr = Span & ({
|
|
49
|
+
t: 'num';
|
|
50
|
+
value: number;
|
|
51
|
+
} | {
|
|
52
|
+
t: 'str';
|
|
53
|
+
value: string;
|
|
54
|
+
} | {
|
|
55
|
+
t: 'bool';
|
|
56
|
+
value: boolean;
|
|
57
|
+
} | {
|
|
58
|
+
t: 'tpl';
|
|
59
|
+
quasis: string[];
|
|
60
|
+
parts: Expr[];
|
|
61
|
+
} | {
|
|
62
|
+
t: 'id';
|
|
63
|
+
name: string;
|
|
64
|
+
} | {
|
|
65
|
+
t: 'member';
|
|
66
|
+
object: Ident;
|
|
67
|
+
prop: Ident;
|
|
68
|
+
} | {
|
|
69
|
+
t: 'call';
|
|
70
|
+
callee: Ident;
|
|
71
|
+
args: Expr[];
|
|
72
|
+
} | {
|
|
73
|
+
t: 'unary';
|
|
74
|
+
op: '-' | '+' | '!';
|
|
75
|
+
arg: Expr;
|
|
76
|
+
} | {
|
|
77
|
+
t: 'bin';
|
|
78
|
+
op: BinOp;
|
|
79
|
+
left: Expr;
|
|
80
|
+
right: Expr;
|
|
81
|
+
} | {
|
|
82
|
+
t: 'cond';
|
|
83
|
+
test: Expr;
|
|
84
|
+
then: Expr;
|
|
85
|
+
else: Expr;
|
|
86
|
+
} | {
|
|
87
|
+
t: 'array';
|
|
88
|
+
items: Expr[];
|
|
89
|
+
} |
|
|
90
|
+
/** `value ± within`, `value ± within%`: only as the first value of measured(…) or estimated(…). */
|
|
91
|
+
{
|
|
92
|
+
t: 'pm';
|
|
93
|
+
value: Expr;
|
|
94
|
+
within: Expr;
|
|
95
|
+
percent: boolean;
|
|
96
|
+
} | {
|
|
97
|
+
t: 'error';
|
|
98
|
+
});
|
|
99
|
+
type BinOp = '||' | '&&' | '==' | '!=' | '<' | '<=' | '>' | '>=' | '+' | '-' | '*' | '/' | '%';
|
|
100
|
+
/** `key: value`. A value is a formula, or a chain of names (`a -> b -> c`) for a route. */
|
|
101
|
+
interface Property extends Span {
|
|
102
|
+
kind: 'prop';
|
|
103
|
+
key: Ident;
|
|
104
|
+
value: Expr;
|
|
105
|
+
chain?: Ident[];
|
|
106
|
+
}
|
|
107
|
+
/** `keyword args… [= init] [{ body }]`: every statement that declares something. */
|
|
108
|
+
interface Decl extends Span {
|
|
109
|
+
kind: 'decl';
|
|
110
|
+
keyword: Ident;
|
|
111
|
+
/** Names, labels, numbers: what follows the keyword on its line. */
|
|
112
|
+
args: Expr[];
|
|
113
|
+
init?: Expr;
|
|
114
|
+
body?: Stmt[];
|
|
115
|
+
/** The braces, for finding the block a cursor sits in. */
|
|
116
|
+
bodySpan?: Span;
|
|
117
|
+
}
|
|
118
|
+
/** `a -> b -> c [: label] [{ body }]`: edges, and in a fan-out its branches (`flow -> stage: share`). */
|
|
119
|
+
interface Edge extends Span {
|
|
120
|
+
kind: 'edge';
|
|
121
|
+
chain: Ident[];
|
|
122
|
+
value?: Expr;
|
|
123
|
+
body?: Stmt[];
|
|
124
|
+
bodySpan?: Span;
|
|
125
|
+
}
|
|
126
|
+
/** A line of numbers: a data point of a fit. */
|
|
127
|
+
interface Row extends Span {
|
|
128
|
+
kind: 'row';
|
|
129
|
+
values: Expr[];
|
|
130
|
+
}
|
|
131
|
+
type Stmt = Property | Decl | Edge | Row;
|
|
132
|
+
interface Document {
|
|
133
|
+
body: Stmt[];
|
|
134
|
+
}
|
|
135
|
+
type Type = 'number' | 'boolean' | 'string' | 'capacity' | 'any';
|
|
136
|
+
/** A host's own functions: the escape hatch for what formulas cannot say. Each is called with plain values. */
|
|
137
|
+
type HostFunctions = Readonly<Record<string, (...args: any[]) => unknown>>;
|
|
138
|
+
/** A formula failed while the model ran: where, and why. */
|
|
139
|
+
declare class FormulaError extends Error {
|
|
140
|
+
readonly span: Span;
|
|
141
|
+
constructor(span: Span, message: string);
|
|
142
|
+
}
|
|
143
|
+
interface Builtin {
|
|
144
|
+
args: Type[];
|
|
145
|
+
/** The rest of the arguments have the last type. */
|
|
146
|
+
variadic?: boolean;
|
|
147
|
+
min?: number;
|
|
148
|
+
result: Type;
|
|
149
|
+
fn: (...a: any[]) => unknown;
|
|
150
|
+
}
|
|
151
|
+
declare const BUILTINS: Readonly<Record<string, Builtin>>;
|
|
152
|
+
/** The version of the language this package reads: the number on a definition's first line. */
|
|
153
|
+
declare const LANGUAGE_VERSION = 1;
|
|
154
|
+
/** Counts that size the drawing; larger ones would hang the layout, so a shared link cannot ask for them. */
|
|
155
|
+
declare const LIMITS: {
|
|
156
|
+
readonly members: 64;
|
|
157
|
+
readonly pods: 256;
|
|
158
|
+
readonly perPod: 256;
|
|
159
|
+
readonly partitions: 1024;
|
|
160
|
+
};
|
|
161
|
+
interface CompileOptions {
|
|
162
|
+
/** Functions formulas may call beyond the built-in ones: the escape hatch for what formulas cannot say. */
|
|
163
|
+
functions?: HostFunctions;
|
|
164
|
+
}
|
|
165
|
+
/** What the definition sets beyond the model and the view: props of `<Loadmap>`. */
|
|
166
|
+
interface DefinitionOptions {
|
|
167
|
+
speed?: number;
|
|
168
|
+
direction?: Direction;
|
|
169
|
+
assumptions: string[];
|
|
170
|
+
}
|
|
171
|
+
interface Definition {
|
|
172
|
+
model: Model;
|
|
173
|
+
view: View;
|
|
174
|
+
options: DefinitionOptions;
|
|
175
|
+
}
|
|
176
|
+
/** A name the definition declares, where: for autocomplete, and for finding a node's source. */
|
|
177
|
+
interface DslSymbol {
|
|
178
|
+
kind: 'param' | 'let' | 'fit' | 'flow' | 'node' | 'stage' | 'component' | 'scenario';
|
|
179
|
+
id: string;
|
|
180
|
+
/** The id where it is written (or the keyword, for an id taken from its box). */
|
|
181
|
+
span: Span;
|
|
182
|
+
/** The whole statement. */
|
|
183
|
+
decl: Span;
|
|
184
|
+
/** For a stage, its box; for a node, its kind. */
|
|
185
|
+
detail?: string;
|
|
186
|
+
}
|
|
187
|
+
/** An `expect` line: where it is and whether it holds. */
|
|
188
|
+
interface ExpectationResult {
|
|
189
|
+
/** The keyword and the label. */
|
|
190
|
+
at: Span;
|
|
191
|
+
line: number;
|
|
192
|
+
column: number;
|
|
193
|
+
result: CheckResult;
|
|
194
|
+
}
|
|
195
|
+
interface CompileResult {
|
|
196
|
+
ok: boolean;
|
|
197
|
+
diagnostics: Diagnostic[];
|
|
198
|
+
/** Only when there are no errors. */
|
|
199
|
+
definition?: Definition;
|
|
200
|
+
/** Each `expect` line, tested on the pessimistic edge; empty when the definition has errors. */
|
|
201
|
+
expectations: ExpectationResult[];
|
|
202
|
+
symbols: DslSymbol[];
|
|
203
|
+
doc: Document;
|
|
204
|
+
}
|
|
205
|
+
/** Reads a definition: parses, checks, and builds the model and view `<Loadmap>` takes. Never throws. */
|
|
206
|
+
declare function compile(source: string, o?: CompileOptions): CompileResult;
|
|
207
|
+
/**
|
|
208
|
+
* The diagnostics and every failing expectation, in source order: an editor marks a failure as a warning (the
|
|
209
|
+
* diagram still shows), `loadmap check` as an error.
|
|
210
|
+
*/
|
|
211
|
+
declare function problems(r: CompileResult, failed?: Diagnostic['severity']): Diagnostic[];
|
|
212
|
+
export { Reporter as C, formatDiagnostic as D, didYouMean as E, Diagnostic as S, closest as T, Expr as _, DslSymbol as a, Row as b, LIMITS as c, BUILTINS as d, FormulaError as f, Edge as g, Document as h, DefinitionOptions as i, compile as l, Decl as m, CompileResult as n, ExpectationResult as o, HostFunctions as p, Definition as r, LANGUAGE_VERSION as s, CompileOptions as t, problems as u, Ident as v, Span as w, Stmt as x, Property as y };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { D as formatDiagnostic, S as Diagnostic, a as DslSymbol, f as FormulaError, i as DefinitionOptions, l as compile, n as CompileResult, o as ExpectationResult, p as HostFunctions, r as Definition, s as LANGUAGE_VERSION, t as CompileOptions, u as problems, w as Span } from "./compile-C2Kdi-Ul.js";
|
|
2
|
+
import { ParamValue } from "@loadmap/core";
|
|
3
|
+
interface Completion {
|
|
4
|
+
label: string;
|
|
5
|
+
type: 'keyword' | 'property' | 'variable' | 'function' | 'constant' | 'type';
|
|
6
|
+
/** A few words next to the label. */
|
|
7
|
+
detail?: string;
|
|
8
|
+
/** The longer help shown beside the list. */
|
|
9
|
+
info?: string;
|
|
10
|
+
/** What to insert, when it differs from the label: "rate: ". */
|
|
11
|
+
apply?: string;
|
|
12
|
+
}
|
|
13
|
+
interface Completions {
|
|
14
|
+
/** The word being completed: replace source[from, to). */
|
|
15
|
+
from: number;
|
|
16
|
+
to: number;
|
|
17
|
+
options: Completion[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* What may be typed at `offset`: the properties and statements of the block the cursor is in, the ids a route or an
|
|
21
|
+
* edge can name, the names a formula can read. Editor-agnostic; pass the last compile result to save a compile.
|
|
22
|
+
*/
|
|
23
|
+
export declare function completions(source: string, offset: number, compiled?: CompileResult): Completions;
|
|
24
|
+
/** A link is refused when its source would unpack to more than this: a small link must not fill the memory. */
|
|
25
|
+
export declare const MAX_SHARED_BYTES = 1000000;
|
|
26
|
+
export declare function encodeShare(source: string, values?: Readonly<Record<string, ParamValue>>): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* `values` are the texts the link holds: only the model knows whether "20" is a number or the option "20", so the
|
|
29
|
+
* page reads them by each parameter's kind.
|
|
30
|
+
*/
|
|
31
|
+
type Shared = {
|
|
32
|
+
source: string;
|
|
33
|
+
values: Record<string, string>;
|
|
34
|
+
} | {
|
|
35
|
+
error: string;
|
|
36
|
+
};
|
|
37
|
+
/** Reads a link's hash (with or without its "#"). Never throws: a broken link gives `{ error }`. */
|
|
38
|
+
export declare function decodeShare(hash: string): Promise<Shared>;
|
|
39
|
+
export { type CompileOptions, type CompileResult, type Completion, type Completions, type Definition, type DefinitionOptions, type Diagnostic, type DslSymbol, type ExpectationResult, FormulaError, type HostFunctions, LANGUAGE_VERSION, type Shared, type Span, compile, formatDiagnostic, problems };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { C as formatDiagnostic, _ as SCHEMA, d as BUILTINS, f as FormulaError, g as EDGE_PROPS, i as problems, m as COLORS, r as compile, t as LANGUAGE_VERSION, y as own } from "./compile-6wNrbYqA.js";
|
|
2
|
+
// src/complete.ts
|
|
3
|
+
/**
|
|
4
|
+
* What may be typed at `offset`: the properties and statements of the block the cursor is in, the ids a route or an
|
|
5
|
+
* edge can name, the names a formula can read. Editor-agnostic; pass the last compile result to save a compile.
|
|
6
|
+
*/
|
|
7
|
+
function completions(source, offset, compiled) {
|
|
8
|
+
const r = compiled ?? compile(source);
|
|
9
|
+
let from = offset;
|
|
10
|
+
while (from > 0 && /\w/.test(source[from - 1])) from--;
|
|
11
|
+
const out = (options) => ({
|
|
12
|
+
from,
|
|
13
|
+
to: offset,
|
|
14
|
+
options
|
|
15
|
+
});
|
|
16
|
+
const ids = (kind) => [...new Set(r.symbols.filter((s) => s.kind === kind).map((s) => s.id))];
|
|
17
|
+
if (source[from - 1] === ".") {
|
|
18
|
+
let t = from - 1;
|
|
19
|
+
while (t > 0 && /\w/.test(source[t - 1])) t--;
|
|
20
|
+
const table = source.slice(t, from - 1);
|
|
21
|
+
return out((table === "flows" ? ids("flow") : table === "demand" || table === "util" ? ids("component") : table === "derived" ? ids("let") : []).map((label) => ({
|
|
22
|
+
label,
|
|
23
|
+
type: "variable",
|
|
24
|
+
detail: table
|
|
25
|
+
})));
|
|
26
|
+
}
|
|
27
|
+
const { spec, edgeBody } = blockAt(r.doc.body, offset);
|
|
28
|
+
const lineStart = source.lastIndexOf("\n", from - 1) + 1;
|
|
29
|
+
const before = source.slice(lineStart, from).replace(/^.*[{;]/, "").trimStart();
|
|
30
|
+
const stages = () => [...ids("stage").map((label) => ({
|
|
31
|
+
label,
|
|
32
|
+
type: "variable",
|
|
33
|
+
detail: "stage"
|
|
34
|
+
})), ...singleStageBoxes(r.symbols).map((label) => ({
|
|
35
|
+
label,
|
|
36
|
+
type: "variable",
|
|
37
|
+
detail: "box"
|
|
38
|
+
}))];
|
|
39
|
+
const nodes = () => ids("node").map((label) => ({
|
|
40
|
+
label,
|
|
41
|
+
type: "variable",
|
|
42
|
+
detail: "node"
|
|
43
|
+
}));
|
|
44
|
+
if (before === "") {
|
|
45
|
+
if (edgeBody) return out(props(EDGE_PROPS));
|
|
46
|
+
const list = [...props(spec.props), ...statements(spec)];
|
|
47
|
+
if (spec.open?.names === "values" || spec.open?.names === "params") list.push(...[...spec.open.names === "values" ? ["load"] : [], ...ids("param")].map((label) => ({
|
|
48
|
+
label,
|
|
49
|
+
type: "variable",
|
|
50
|
+
apply: `${label}: `
|
|
51
|
+
})));
|
|
52
|
+
if (spec.open?.names === "flows") list.push(...ids("flow").map((label) => ({
|
|
53
|
+
label,
|
|
54
|
+
type: "variable",
|
|
55
|
+
apply: `${label}: `
|
|
56
|
+
})));
|
|
57
|
+
if (spec.edges === "edges") list.push(...nodes());
|
|
58
|
+
if (spec.edges === "branches") list.push(...ids("flow").map((label) => ({
|
|
59
|
+
label,
|
|
60
|
+
type: "variable"
|
|
61
|
+
})));
|
|
62
|
+
return out(list);
|
|
63
|
+
}
|
|
64
|
+
const chain = /^(\w+)(\s*->\s*\w+)*\s*->\s*$/.test(before);
|
|
65
|
+
const prop = /^(\w+)\s*:\s*(.*)$/.exec(before);
|
|
66
|
+
if (chain && !prop) return out(spec.edges === "branches" ? stages() : nodes());
|
|
67
|
+
if (prop) {
|
|
68
|
+
const p = edgeBody ? own(EDGE_PROPS, prop[1]) : own(spec.props, prop[1]);
|
|
69
|
+
const kind = p?.value;
|
|
70
|
+
if (kind === "route") return out(stages());
|
|
71
|
+
if (kind === "node") return out(nodes());
|
|
72
|
+
if (kind === "component") return out(ids("component").map((label) => ({
|
|
73
|
+
label,
|
|
74
|
+
type: "variable"
|
|
75
|
+
})));
|
|
76
|
+
if (kind === "scenario") return out(ids("scenario").map((label) => ({
|
|
77
|
+
label,
|
|
78
|
+
type: "constant",
|
|
79
|
+
apply: /^[A-Za-z_]\w*$/.test(label) ? label : JSON.stringify(label)
|
|
80
|
+
})));
|
|
81
|
+
if (kind === "enum") return out((prop[1] === "color" ? COLORS : p?.values ?? []).map((label) => ({
|
|
82
|
+
label,
|
|
83
|
+
type: "constant"
|
|
84
|
+
})));
|
|
85
|
+
if (kind === "boolean" && p?.scope === "const") return out([{
|
|
86
|
+
label: "true",
|
|
87
|
+
type: "constant"
|
|
88
|
+
}, {
|
|
89
|
+
label: "false",
|
|
90
|
+
type: "constant"
|
|
91
|
+
}]);
|
|
92
|
+
if (p?.scope === "const") return out([]);
|
|
93
|
+
return out(formulaNames(r, p?.scope ?? "ctx"));
|
|
94
|
+
}
|
|
95
|
+
if (/^\w+(\s*->\s*\w+)+\s*:/.test(before) || /^let\s+\w+\s*=/.test(before)) return out(formulaNames(r, "ctx"));
|
|
96
|
+
return out([]);
|
|
97
|
+
}
|
|
98
|
+
function props(p) {
|
|
99
|
+
return Object.entries(p).map(([label, s]) => ({
|
|
100
|
+
label,
|
|
101
|
+
type: "property",
|
|
102
|
+
apply: `${label}: `,
|
|
103
|
+
detail: s.required ? "required" : void 0,
|
|
104
|
+
info: s.doc
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
function statements(spec) {
|
|
108
|
+
return spec.children.filter((k) => k !== "loadmap").map((k) => ({
|
|
109
|
+
label: k,
|
|
110
|
+
type: "keyword",
|
|
111
|
+
info: `${SCHEMA[k].doc}\n\n${SCHEMA[k].example}`
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
function formulaNames(r, scope) {
|
|
115
|
+
if (scope === "i" || scope === "value") return [{
|
|
116
|
+
label: scope,
|
|
117
|
+
type: "variable"
|
|
118
|
+
}];
|
|
119
|
+
const list = [{
|
|
120
|
+
label: "load",
|
|
121
|
+
type: "variable",
|
|
122
|
+
detail: "× today"
|
|
123
|
+
}, ...r.symbols.filter((s) => s.kind === "param").map((s) => s.id).map((label) => ({
|
|
124
|
+
label,
|
|
125
|
+
type: "variable",
|
|
126
|
+
detail: "parameter"
|
|
127
|
+
}))];
|
|
128
|
+
if (scope === "values") return list;
|
|
129
|
+
list.push(...r.symbols.filter((s) => s.kind === "let").map((s) => ({
|
|
130
|
+
label: s.id,
|
|
131
|
+
type: "variable",
|
|
132
|
+
detail: "let"
|
|
133
|
+
})), ...[
|
|
134
|
+
"flows",
|
|
135
|
+
"demand",
|
|
136
|
+
"util",
|
|
137
|
+
"derived"
|
|
138
|
+
].map((label) => ({
|
|
139
|
+
label,
|
|
140
|
+
type: "type",
|
|
141
|
+
apply: `${label}.`
|
|
142
|
+
})), ...r.symbols.filter((s) => s.kind === "fit").map((s) => ({
|
|
143
|
+
label: s.id,
|
|
144
|
+
type: "function",
|
|
145
|
+
detail: "fit"
|
|
146
|
+
})), ...Object.keys(BUILTINS).map((label) => ({
|
|
147
|
+
label,
|
|
148
|
+
type: "function"
|
|
149
|
+
})), {
|
|
150
|
+
label: "true",
|
|
151
|
+
type: "constant"
|
|
152
|
+
}, {
|
|
153
|
+
label: "false",
|
|
154
|
+
type: "constant"
|
|
155
|
+
});
|
|
156
|
+
return list;
|
|
157
|
+
}
|
|
158
|
+
function singleStageBoxes(symbols) {
|
|
159
|
+
const count = /* @__PURE__ */ new Map();
|
|
160
|
+
for (const s of symbols) if (s.kind === "stage" && s.detail) count.set(s.detail, (count.get(s.detail) ?? 0) + 1);
|
|
161
|
+
const stages = new Set(symbols.filter((s) => s.kind === "stage").map((s) => s.id));
|
|
162
|
+
return [...count].filter(([box, n]) => n === 1 && !stages.has(box)).map(([box]) => box);
|
|
163
|
+
}
|
|
164
|
+
/** The innermost block whose braces hold `offset`. */
|
|
165
|
+
function blockAt(body, offset) {
|
|
166
|
+
let spec = SCHEMA[""];
|
|
167
|
+
let list = body;
|
|
168
|
+
for (;;) {
|
|
169
|
+
const inside = list.find((s) => (s.kind === "decl" || s.kind === "edge") && !!s.bodySpan && s.bodySpan.from < offset && offset < s.bodySpan.to);
|
|
170
|
+
if (!inside?.body) return {
|
|
171
|
+
spec,
|
|
172
|
+
edgeBody: false
|
|
173
|
+
};
|
|
174
|
+
if (inside.kind === "edge") return {
|
|
175
|
+
spec,
|
|
176
|
+
edgeBody: true
|
|
177
|
+
};
|
|
178
|
+
const next = own(SCHEMA, inside.keyword.name);
|
|
179
|
+
if (!next) return {
|
|
180
|
+
spec,
|
|
181
|
+
edgeBody: false
|
|
182
|
+
};
|
|
183
|
+
spec = next;
|
|
184
|
+
list = inside.body;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// src/share.ts
|
|
188
|
+
/**
|
|
189
|
+
* A definition in a link: `#v1.` + base64url(deflate-raw(source)), then the values that differ from the defaults
|
|
190
|
+
* (`&load=30&pods=6`). The hash never reaches a server, and the source carries its own language version, so old links
|
|
191
|
+
* keep opening. Compression is the platform's CompressionStream: no dependency.
|
|
192
|
+
*/
|
|
193
|
+
const PREFIX = "v1.";
|
|
194
|
+
/** A link is refused when its source would unpack to more than this: a small link must not fill the memory. */
|
|
195
|
+
const MAX_SHARED_BYTES = 1e6;
|
|
196
|
+
async function encodeShare(source, values = {}) {
|
|
197
|
+
const packed = await pipe(new TextEncoder().encode(source), new CompressionStream("deflate-raw"));
|
|
198
|
+
const rest = new URLSearchParams(Object.entries(values).map(([k, v]) => [k, String(v)])).toString();
|
|
199
|
+
return `${PREFIX}${base64url(packed)}${rest ? `&${rest}` : ""}`;
|
|
200
|
+
}
|
|
201
|
+
/** Reads a link's hash (with or without its "#"). Never throws: a broken link gives `{ error }`. */
|
|
202
|
+
async function decodeShare(hash) {
|
|
203
|
+
const h = hash.replace(/^#/, "");
|
|
204
|
+
const amp = h.indexOf("&");
|
|
205
|
+
const head = amp < 0 ? h : h.slice(0, amp);
|
|
206
|
+
if (!head.startsWith(PREFIX)) return { error: "this link holds no loadmap definition" };
|
|
207
|
+
let bytes;
|
|
208
|
+
try {
|
|
209
|
+
bytes = unbase64url(head.slice(3));
|
|
210
|
+
} catch {
|
|
211
|
+
return { error: "this link is cut or garbled" };
|
|
212
|
+
}
|
|
213
|
+
let source;
|
|
214
|
+
try {
|
|
215
|
+
const raw = await pipe(bytes, new DecompressionStream("deflate-raw"), MAX_SHARED_BYTES);
|
|
216
|
+
source = new TextDecoder("utf-8", { fatal: true }).decode(raw);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
return { error: err instanceof TooLarge ? err.message : "this link is cut or garbled" };
|
|
219
|
+
}
|
|
220
|
+
const values = Object.fromEntries(new URLSearchParams(amp < 0 ? "" : h.slice(amp + 1)));
|
|
221
|
+
return {
|
|
222
|
+
source,
|
|
223
|
+
values
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
var TooLarge = class extends Error {};
|
|
227
|
+
async function pipe(input, through, max = Infinity) {
|
|
228
|
+
const writer = through.writable.getWriter();
|
|
229
|
+
writer.write(input).catch(() => {});
|
|
230
|
+
writer.close().catch(() => {});
|
|
231
|
+
const reader = through.readable.getReader();
|
|
232
|
+
const chunks = [];
|
|
233
|
+
let size = 0;
|
|
234
|
+
for (;;) {
|
|
235
|
+
const { done, value } = await reader.read();
|
|
236
|
+
if (done) break;
|
|
237
|
+
size += value.byteLength;
|
|
238
|
+
if (size > max) {
|
|
239
|
+
await reader.cancel();
|
|
240
|
+
throw new TooLarge(`this link unpacks to more than ${max / 1e6} MB`);
|
|
241
|
+
}
|
|
242
|
+
chunks.push(value);
|
|
243
|
+
}
|
|
244
|
+
const out = new Uint8Array(size);
|
|
245
|
+
let at = 0;
|
|
246
|
+
for (const c of chunks) {
|
|
247
|
+
out.set(c, at);
|
|
248
|
+
at += c.byteLength;
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
function base64url(bytes) {
|
|
253
|
+
let bin = "";
|
|
254
|
+
for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
255
|
+
return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
256
|
+
}
|
|
257
|
+
function unbase64url(s) {
|
|
258
|
+
if (!/^[A-Za-z0-9_-]*$/.test(s)) throw new Error("not base64url");
|
|
259
|
+
const bin = atob(s.replaceAll("-", "+").replaceAll("_", "/"));
|
|
260
|
+
return Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
261
|
+
}
|
|
262
|
+
export { FormulaError, LANGUAGE_VERSION, MAX_SHARED_BYTES, compile, completions, decodeShare, encodeShare, formatDiagnostic, problems };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { C as Reporter, E as didYouMean, T as closest, _ as Expr, b as Row, c as LIMITS, d as BUILTINS, g as Edge, h as Document, m as Decl, v as Ident, w as Span, x as Stmt, y as Property } from "./compile-C2Kdi-Ul.js";
|
|
2
|
+
/** What a formula may read: everything, only the parameters, a local (`i`, `value`) or nothing (a constant). */
|
|
3
|
+
type Scope = 'ctx' | 'values' | 'i' | 'value' | 'const';
|
|
4
|
+
type ValueKind = 'number' | 'boolean' | 'string' | 'capacity' |
|
|
5
|
+
/** A word from `values`, written bare or quoted: `shape: cylinder`. */
|
|
6
|
+
'enum' |
|
|
7
|
+
/** Names joined by "->": `route: gateway -> topic -> workers`. */
|
|
8
|
+
'route' |
|
|
9
|
+
/** The id of a node, a component, a flow. */
|
|
10
|
+
'node' | 'component' |
|
|
11
|
+
/** A scenario's label, bare or quoted: `scenario: Proposed`. */
|
|
12
|
+
'scenario' |
|
|
13
|
+
/** A list of options: `[2, 3, 4]`. */
|
|
14
|
+
'options' |
|
|
15
|
+
/** A number, a switch or an option: a parameter's default. */
|
|
16
|
+
'value';
|
|
17
|
+
interface PropSpec {
|
|
18
|
+
value: ValueKind;
|
|
19
|
+
scope: Scope;
|
|
20
|
+
doc: string;
|
|
21
|
+
required?: boolean;
|
|
22
|
+
values?: readonly string[];
|
|
23
|
+
/** May appear more than once; each adds to a list. */
|
|
24
|
+
repeat?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface ArgSpec {
|
|
27
|
+
name: 'id' | 'label' | 'sub' | 'version' | 'x' | 'y';
|
|
28
|
+
kind: 'id' | 'text' | 'number';
|
|
29
|
+
optional?: boolean;
|
|
30
|
+
/** A text that may be a template computed from the model. */
|
|
31
|
+
dynamic?: boolean;
|
|
32
|
+
}
|
|
33
|
+
interface BlockSpec {
|
|
34
|
+
doc: string;
|
|
35
|
+
/** An example line, shown by autocomplete. */
|
|
36
|
+
example: string;
|
|
37
|
+
args: readonly ArgSpec[];
|
|
38
|
+
/** `let x = …`. */
|
|
39
|
+
init?: boolean;
|
|
40
|
+
props: Readonly<Record<string, PropSpec>>;
|
|
41
|
+
/** Keywords of the statements it may hold. */
|
|
42
|
+
children: readonly string[];
|
|
43
|
+
/** Its properties are named by the model: the load and parameters in a preset, parameters only in a scenario, flows in partitions. */
|
|
44
|
+
open?: {
|
|
45
|
+
names: 'values' | 'params' | 'flows';
|
|
46
|
+
spec: PropSpec;
|
|
47
|
+
};
|
|
48
|
+
/** It holds `a -> b` lines: edges at the top, branches in a fan-out. */
|
|
49
|
+
edges?: 'edges' | 'branches';
|
|
50
|
+
/** It holds rows of numbers. */
|
|
51
|
+
rows?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** A component's properties: on a box, or in a `component` block. */
|
|
54
|
+
export declare const COMPONENT_PROPS: Readonly<Record<string, PropSpec>>;
|
|
55
|
+
export declare const COLORS: readonly ['accent', 'alt', 'ok', 'warn', 'drop', 'muted'];
|
|
56
|
+
export declare const SCHEMA: Readonly<Record<string, BlockSpec>>;
|
|
57
|
+
/** The properties of an edge's block: `a -> b { label: …; when: … }`. */
|
|
58
|
+
export declare const EDGE_PROPS: Readonly<Record<string, PropSpec>>;
|
|
59
|
+
export declare const BOXES: readonly ['node', 'cluster', 'pool'];
|
|
60
|
+
export declare const STAGES: readonly ['pass', 'store', 'sink', 'queue', 'workers', 'fanout'];
|
|
61
|
+
type TokenKind = 'ident' | 'number' | 'string' | 'template' | 'punct' | 'newline' | 'eof';
|
|
62
|
+
interface Token extends Span {
|
|
63
|
+
kind: TokenKind;
|
|
64
|
+
/** The source text: an identifier's name, a punctuation mark. */
|
|
65
|
+
text: string;
|
|
66
|
+
/** A number's value, a string's cooked text. */
|
|
67
|
+
value?: number | string;
|
|
68
|
+
/** A template's cooked text pieces and, between them, the source ranges of its `${…}` parts. */
|
|
69
|
+
quasis?: string[];
|
|
70
|
+
parts?: Span[];
|
|
71
|
+
}
|
|
72
|
+
/** The token kinds and marks an editor needs to highlight the language like the parser reads it. */
|
|
73
|
+
export declare const LEXICON: {
|
|
74
|
+
readonly punct: string[];
|
|
75
|
+
readonly comment: '#';
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Splits `source[from, to)` into tokens. Newlines end statements, except inside parentheses and brackets, so a long
|
|
79
|
+
* formula can wrap there. `#` starts a comment to the end of the line.
|
|
80
|
+
*/
|
|
81
|
+
export declare function lex(source: string, report: Reporter, from?: number, to?: number): Token[];
|
|
82
|
+
/** Deeper formulas are refused: a shared link must not be able to exhaust the stack. */
|
|
83
|
+
export declare const MAX_DEPTH = 64;
|
|
84
|
+
/** Longer sources are refused before parsing. */
|
|
85
|
+
export declare const MAX_SOURCE = 200000;
|
|
86
|
+
/** Parses a definition into statements. Never throws: errors go to the reporter, and parsing goes on at the next line. */
|
|
87
|
+
export declare function parse(source: string, report?: Reporter): {
|
|
88
|
+
doc: Document;
|
|
89
|
+
report: Reporter;
|
|
90
|
+
};
|
|
91
|
+
/** Parses one formula on its own, e.g. a template's `${…}` part. */
|
|
92
|
+
export declare function parseExpression(source: string, from: number, to: number, report: Reporter, depth?: number): Expr;
|
|
93
|
+
export declare function reference(): string;
|
|
94
|
+
export { BUILTINS, type Decl, type Document, type Edge, type Expr, type Ident, LIMITS, type PropSpec, type Property, Reporter, type Row, type Stmt, type Token, closest, didYouMean };
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { S as didYouMean, _ as SCHEMA, a as MAX_DEPTH, b as Reporter, c as parseExpression, d as BUILTINS, g as EDGE_PROPS, h as COMPONENT_PROPS, l as LEXICON, m as COLORS, n as LIMITS, o as MAX_SOURCE, p as BOXES, s as parse, u as lex, v as STAGES, x as closest } from "./compile-6wNrbYqA.js";
|
|
2
|
+
// src/reference.ts
|
|
3
|
+
const SCOPE = {
|
|
4
|
+
ctx: "formula",
|
|
5
|
+
values: "formula of parameters",
|
|
6
|
+
i: "formula of `i`",
|
|
7
|
+
value: "formula of `value`",
|
|
8
|
+
const: "constant"
|
|
9
|
+
};
|
|
10
|
+
/** The statement reference of docs/dsl.md, written from the schema table, so the two cannot drift apart. */
|
|
11
|
+
const OPEN = {
|
|
12
|
+
values: "parameters (and `load`)",
|
|
13
|
+
params: "parameters (not `load`)",
|
|
14
|
+
flows: "flows"
|
|
15
|
+
};
|
|
16
|
+
function reference() {
|
|
17
|
+
const out = [];
|
|
18
|
+
const table = (props) => {
|
|
19
|
+
if (!Object.keys(props).length) return;
|
|
20
|
+
out.push("| Property | Value | |", "| --- | --- | --- |");
|
|
21
|
+
for (const [k, p] of Object.entries(props)) {
|
|
22
|
+
const value = p.values ? p.values.map((v) => `\`${v}\``).join(" \\| ") : `${p.value}, ${SCOPE[p.scope]}`;
|
|
23
|
+
out.push(`| \`${k}\`${p.required ? " (required)" : ""} | ${value} | ${p.doc.replaceAll("|", "\\|")} |`);
|
|
24
|
+
}
|
|
25
|
+
out.push("");
|
|
26
|
+
};
|
|
27
|
+
const block = (name, spec) => {
|
|
28
|
+
out.push(`### ${name || "The top level"}`, "", spec.doc, "");
|
|
29
|
+
if (spec.example) out.push("```", spec.example, "```", "");
|
|
30
|
+
if (spec.args.length) out.push(`After the keyword: ${spec.args.map((a) => a.optional ? `[${a.name}]` : a.name).join(" ")}${spec.init ? " = formula" : ""}.`, "");
|
|
31
|
+
table(spec.props);
|
|
32
|
+
const holds = spec.children.filter((c) => c !== "loadmap");
|
|
33
|
+
if (holds.length) out.push(`Holds: ${holds.map((c) => `\`${c}\``).join(", ")}.`, "");
|
|
34
|
+
if (spec.open) out.push(`Its properties are named by ${OPEN[spec.open.names]}.`, "");
|
|
35
|
+
if (spec.edges === "edges") out.push("Holds edges: `a -> b`, `a -> b: label`, `a -> b { label: …; when: … }`.", "");
|
|
36
|
+
if (spec.edges === "branches") out.push("Holds branches: `flow -> stage: copies per unit`.", "");
|
|
37
|
+
};
|
|
38
|
+
for (const [name, spec] of Object.entries(SCHEMA)) block(name, spec);
|
|
39
|
+
out.push("### Edges", "", "A line between two nodes: `a -> b`, a chain `a -> b -> c`, or with a label `a -> b: \"…\"`.", "");
|
|
40
|
+
table(EDGE_PROPS);
|
|
41
|
+
out.push("### Functions", "", Object.keys(BUILTINS).map((f) => `\`${f}\``).join(", "), "");
|
|
42
|
+
return out.join("\n");
|
|
43
|
+
}
|
|
44
|
+
export { BOXES, BUILTINS, COLORS, COMPONENT_PROPS, EDGE_PROPS, LEXICON, LIMITS, MAX_DEPTH, MAX_SOURCE, Reporter, SCHEMA, STAGES, closest, didYouMean, lex, parse, parseExpression, reference };
|