@tinta-lang/engine 0.0.0-stage → 0.1.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES.txt +13651 -0
- package/dist/analysis.d.ts +8 -0
- package/dist/analysis.js +19 -0
- package/dist/binding.d.ts +15 -0
- package/dist/binding.js +292 -0
- package/dist/chart-presentation.d.ts +11 -0
- package/dist/chart-presentation.js +61 -0
- package/dist/charts.d.ts +3 -0
- package/dist/charts.js +139 -0
- package/dist/codec.d.ts +3 -0
- package/dist/codec.js +52 -0
- package/dist/common.d.ts +24 -0
- package/dist/common.js +128 -0
- package/dist/compiler.d.ts +3 -0
- package/dist/compiler.js +458 -0
- package/dist/expression.d.ts +15 -0
- package/dist/expression.js +236 -0
- package/dist/grid.d.ts +5 -0
- package/dist/grid.js +57 -0
- package/dist/imports.d.ts +4 -0
- package/dist/imports.js +293 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/inspect.d.ts +3 -0
- package/dist/inspect.js +42 -0
- package/dist/program-validator.d.ts +19 -0
- package/dist/program-validator.js +17949 -0
- package/dist/programs.d.ts +3 -0
- package/dist/programs.js +1 -0
- package/dist/registry.d.ts +144 -0
- package/dist/registry.js +200 -0
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.js +32 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +304 -0
- package/package.json +74 -3
- package/README.md +0 -3
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Program } from "@tinta-lang/types";
|
|
2
|
+
import type { EditorType } from "@tinta-lang/types/editor";
|
|
3
|
+
export interface AnalyzedBinding {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly type: EditorType;
|
|
6
|
+
}
|
|
7
|
+
/** Read-only compiler facts; never expose the private binding maps or alter IR. */
|
|
8
|
+
export declare function analyzeProgram(program: Program): readonly AnalyzedBinding[];
|
package/dist/analysis.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { freeze } from "./common.js";
|
|
2
|
+
import { programs } from "./programs.js";
|
|
3
|
+
/** Read-only compiler facts; never expose the private binding maps or alter IR. */
|
|
4
|
+
export function analyzeProgram(program) {
|
|
5
|
+
const bound = programs.get(program);
|
|
6
|
+
if (!bound)
|
|
7
|
+
throw new TypeError("Expected a compiled or decoded program.");
|
|
8
|
+
return freeze(program.declarations.map(({ name }) => {
|
|
9
|
+
const type = bound.types.get(name);
|
|
10
|
+
return {
|
|
11
|
+
name,
|
|
12
|
+
type: typeof type !== "string"
|
|
13
|
+
? { kind: "table", columns: type.map((column) => ({ ...column })) }
|
|
14
|
+
: type.startsWith("list:")
|
|
15
|
+
? { kind: "list", scalar: type.slice(5) }
|
|
16
|
+
: { kind: "scalar", scalar: type },
|
|
17
|
+
};
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ControlDefinition, Declaration, DocNode, Expr, ProgramIR, Span } from "@tinta-lang/types";
|
|
2
|
+
import { Budget } from "./common.js";
|
|
3
|
+
import { type RuntimeValue, type Type } from "./expression.js";
|
|
4
|
+
export interface Bound {
|
|
5
|
+
readonly types: ReadonlyMap<string, Type>;
|
|
6
|
+
readonly compileOperations: number;
|
|
7
|
+
readonly locations: ReadonlyMap<string, Span>;
|
|
8
|
+
readonly order: readonly Declaration[];
|
|
9
|
+
readonly controls: ReadonlyMap<string, ControlDefinition>;
|
|
10
|
+
readonly constants: ReadonlyMap<string, RuntimeValue>;
|
|
11
|
+
readonly limits: Readonly<Record<string, number>>;
|
|
12
|
+
}
|
|
13
|
+
export declare function walkFlow(nodes: readonly DocNode[], visit: (node: DocNode) => void): void;
|
|
14
|
+
export declare function references(expr: Expr): Expr[];
|
|
15
|
+
export declare function bind(ir: ProgramIR, budget: Budget, locations: ReadonlyMap<string, Span>): Bound;
|
package/dist/binding.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { Budget, Failure, fail } from "./common.js";
|
|
2
|
+
import { evaluate, infer } from "./expression.js";
|
|
3
|
+
export function walkFlow(nodes, visit) {
|
|
4
|
+
for (const node of nodes) {
|
|
5
|
+
visit(node);
|
|
6
|
+
if ("children" in node)
|
|
7
|
+
walkFlow(node.children, visit);
|
|
8
|
+
if (node.kind === "list")
|
|
9
|
+
node.items.forEach((item) => walkFlow(item.children, visit));
|
|
10
|
+
if (node.kind === "markdown-table")
|
|
11
|
+
[...node.header, ...node.rows.flat()].forEach((cell) => walkFlow(cell, visit));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function references(expr) {
|
|
15
|
+
if (expr.kind === "name")
|
|
16
|
+
return [expr];
|
|
17
|
+
if (expr.kind === "binary")
|
|
18
|
+
return [...references(expr.left), ...references(expr.right)];
|
|
19
|
+
if (expr.kind === "unary")
|
|
20
|
+
return references(expr.operand);
|
|
21
|
+
if (expr.kind === "call")
|
|
22
|
+
return expr.arguments.flatMap(references);
|
|
23
|
+
if (expr.kind === "list")
|
|
24
|
+
return expr.items.flatMap(references);
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
export function bind(ir, budget, locations) {
|
|
28
|
+
const symbols = new Map();
|
|
29
|
+
const controls = new Map();
|
|
30
|
+
const order = [];
|
|
31
|
+
const types = new Map();
|
|
32
|
+
const diagnostics = [];
|
|
33
|
+
const nameSpan = (d) => {
|
|
34
|
+
const offset = ir.source
|
|
35
|
+
.slice(d.span.start.offset, d.span.end.offset)
|
|
36
|
+
.indexOf(d.name, d.kind.length + 1);
|
|
37
|
+
const start = {
|
|
38
|
+
...d.span.start,
|
|
39
|
+
offset: d.span.start.offset + offset,
|
|
40
|
+
column: d.span.start.column + offset,
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
uri: d.span.uri,
|
|
44
|
+
start,
|
|
45
|
+
end: {
|
|
46
|
+
...start,
|
|
47
|
+
offset: start.offset + d.name.length,
|
|
48
|
+
column: start.column + d.name.length,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
const report = (code, span, related = []) => {
|
|
53
|
+
diagnostics.push({
|
|
54
|
+
code,
|
|
55
|
+
severity: "error",
|
|
56
|
+
phase: "bind",
|
|
57
|
+
message: code === "E_CYCLE"
|
|
58
|
+
? "A dependency cycle prevents evaluation."
|
|
59
|
+
: code === "E_NAME_DUPLICATE"
|
|
60
|
+
? "A name is declared more than once."
|
|
61
|
+
: "A global reference cannot be resolved.",
|
|
62
|
+
span,
|
|
63
|
+
related,
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const finish = () => {
|
|
67
|
+
if (!diagnostics.length)
|
|
68
|
+
return;
|
|
69
|
+
diagnostics.sort((a, b) => a.span.start.offset - b.span.start.offset ||
|
|
70
|
+
a.code.localeCompare(b.code, "en"));
|
|
71
|
+
const maximum = budget.limits.diagnostics;
|
|
72
|
+
if (diagnostics.length > maximum) {
|
|
73
|
+
const observed = diagnostics.length;
|
|
74
|
+
diagnostics.splice(maximum - 1);
|
|
75
|
+
diagnostics.push({
|
|
76
|
+
code: "E_LIMIT",
|
|
77
|
+
severity: "error",
|
|
78
|
+
phase: "bind",
|
|
79
|
+
message: "The diagnostic limit was exceeded.",
|
|
80
|
+
span: budget.span,
|
|
81
|
+
details: { limitName: "diagnostics", observed, maximum },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
throw new Failure(diagnostics);
|
|
85
|
+
};
|
|
86
|
+
budget.limit("declarations", ir.declarations.length);
|
|
87
|
+
for (const d of ir.declarations) {
|
|
88
|
+
const previous = symbols.get(d.name);
|
|
89
|
+
if (previous) {
|
|
90
|
+
report("E_NAME_DUPLICATE", nameSpan(d), [
|
|
91
|
+
{ message: "First declaration.", span: nameSpan(previous) },
|
|
92
|
+
]);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
symbols.set(d.name, d);
|
|
96
|
+
if (d.kind === "slider" || d.kind === "choice" || d.kind === "toggle")
|
|
97
|
+
controls.set(d.name, d);
|
|
98
|
+
}
|
|
99
|
+
budget.limit("controls", controls.size);
|
|
100
|
+
budget.limit("tableCount", ir.declarations.filter((d) => d.kind === "data" || d.kind === "view")
|
|
101
|
+
.length);
|
|
102
|
+
const graph = new Map();
|
|
103
|
+
for (const d of symbols.values()) {
|
|
104
|
+
const refs = [];
|
|
105
|
+
if (d.kind === "view")
|
|
106
|
+
refs.push({ name: d.source, span: d.span });
|
|
107
|
+
const expr = "expression" in d
|
|
108
|
+
? d.expression
|
|
109
|
+
: d.kind === "view"
|
|
110
|
+
? d.where
|
|
111
|
+
: undefined;
|
|
112
|
+
if (expr)
|
|
113
|
+
for (const ref of references(expr))
|
|
114
|
+
if (ref.kind === "name")
|
|
115
|
+
refs.push(ref);
|
|
116
|
+
const edges = [];
|
|
117
|
+
for (const ref of refs) {
|
|
118
|
+
budget.charge(ref.span);
|
|
119
|
+
if (!symbols.has(ref.name))
|
|
120
|
+
report("E_NAME_UNKNOWN", ref.span);
|
|
121
|
+
else
|
|
122
|
+
edges.push(ref.name);
|
|
123
|
+
}
|
|
124
|
+
graph.set(d.name, edges);
|
|
125
|
+
}
|
|
126
|
+
// Tarjan's stack identifies every component, including inactive branches.
|
|
127
|
+
const indices = new Map(), low = new Map();
|
|
128
|
+
const stack = [], onStack = new Set();
|
|
129
|
+
function component(name) {
|
|
130
|
+
const index = indices.size;
|
|
131
|
+
indices.set(name, index);
|
|
132
|
+
low.set(name, index);
|
|
133
|
+
stack.push(name);
|
|
134
|
+
onStack.add(name);
|
|
135
|
+
for (const edge of graph.get(name)) {
|
|
136
|
+
if (!indices.has(edge)) {
|
|
137
|
+
component(edge);
|
|
138
|
+
low.set(name, Math.min(low.get(name), low.get(edge)));
|
|
139
|
+
}
|
|
140
|
+
else if (onStack.has(edge))
|
|
141
|
+
low.set(name, Math.min(low.get(name), indices.get(edge)));
|
|
142
|
+
}
|
|
143
|
+
if (low.get(name) !== index)
|
|
144
|
+
return;
|
|
145
|
+
const members = [];
|
|
146
|
+
let popped;
|
|
147
|
+
do {
|
|
148
|
+
popped = stack.pop();
|
|
149
|
+
onStack.delete(popped);
|
|
150
|
+
members.push(symbols.get(popped));
|
|
151
|
+
} while (popped !== name);
|
|
152
|
+
if (members.length > 1 || graph.get(name).includes(name)) {
|
|
153
|
+
members.sort((a, b) => a.span.start.offset - b.span.start.offset);
|
|
154
|
+
report("E_CYCLE", nameSpan(members[0]), members.map((d) => ({
|
|
155
|
+
message: "Participating declaration.",
|
|
156
|
+
span: nameSpan(d),
|
|
157
|
+
})));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const name of symbols.keys())
|
|
161
|
+
if (!indices.has(name))
|
|
162
|
+
component(name);
|
|
163
|
+
finish();
|
|
164
|
+
function declaration(name, span) {
|
|
165
|
+
budget.charge(span);
|
|
166
|
+
const d = symbols.get(name);
|
|
167
|
+
if (!d)
|
|
168
|
+
fail("E_NAME_UNKNOWN", span, "bind");
|
|
169
|
+
return d;
|
|
170
|
+
}
|
|
171
|
+
function visit(d) {
|
|
172
|
+
if (types.has(d.name))
|
|
173
|
+
return;
|
|
174
|
+
const deps = [];
|
|
175
|
+
if (d.kind === "view")
|
|
176
|
+
deps.push({ name: d.source, span: d.span });
|
|
177
|
+
const expr = "expression" in d
|
|
178
|
+
? d.expression
|
|
179
|
+
: d.kind === "view"
|
|
180
|
+
? d.where
|
|
181
|
+
: undefined;
|
|
182
|
+
if (expr)
|
|
183
|
+
for (const ref of references(expr))
|
|
184
|
+
if (ref.kind === "name")
|
|
185
|
+
deps.push(ref);
|
|
186
|
+
for (const ref of deps) {
|
|
187
|
+
const target = declaration(ref.name, ref.span);
|
|
188
|
+
if (d.kind === "let" && target.kind !== "let")
|
|
189
|
+
fail("E_CONST_DEPENDENCY", ref.span, "bind");
|
|
190
|
+
visit(target);
|
|
191
|
+
}
|
|
192
|
+
const get = (name, expr) => {
|
|
193
|
+
declaration(name, expr.span);
|
|
194
|
+
return types.get(name);
|
|
195
|
+
};
|
|
196
|
+
let type;
|
|
197
|
+
if (d.kind === "slider")
|
|
198
|
+
type = "number";
|
|
199
|
+
else if (d.kind === "toggle")
|
|
200
|
+
type = "boolean";
|
|
201
|
+
else if (d.kind === "choice")
|
|
202
|
+
type = "text";
|
|
203
|
+
else if (d.kind === "data")
|
|
204
|
+
type = d.table.columns;
|
|
205
|
+
else if (d.kind === "view") {
|
|
206
|
+
type = types.get(d.source);
|
|
207
|
+
if (typeof type === "string")
|
|
208
|
+
fail("E_TYPE", d.span);
|
|
209
|
+
if (d.where && infer(d.where, get, budget, type) !== "boolean")
|
|
210
|
+
fail("E_TYPE", d.where.span);
|
|
211
|
+
if (d.sort && !type.some((c) => c.name === d.sort))
|
|
212
|
+
fail("E_COLUMN_UNKNOWN", d.span);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
type = infer(d.expression, get, budget);
|
|
216
|
+
if (typeof type !== "string")
|
|
217
|
+
fail("E_TYPE", d.expression.span);
|
|
218
|
+
}
|
|
219
|
+
types.set(d.name, type);
|
|
220
|
+
order.push(d);
|
|
221
|
+
}
|
|
222
|
+
ir.declarations.forEach(visit);
|
|
223
|
+
const get = (name, expr) => {
|
|
224
|
+
declaration(name, expr.span);
|
|
225
|
+
return types.get(name);
|
|
226
|
+
};
|
|
227
|
+
const schema = (name, span) => {
|
|
228
|
+
declaration(name, span);
|
|
229
|
+
const type = types.get(name);
|
|
230
|
+
if (typeof type === "string")
|
|
231
|
+
fail("E_TYPE", span);
|
|
232
|
+
return type;
|
|
233
|
+
};
|
|
234
|
+
let charts = 0;
|
|
235
|
+
walkFlow(ir.flow, (node) => {
|
|
236
|
+
budget.charge(node.span);
|
|
237
|
+
if (node.kind === "interpolation") {
|
|
238
|
+
const type = infer(node.expression, get, budget);
|
|
239
|
+
if (type !== "number" && type !== "text" && type !== "boolean")
|
|
240
|
+
fail("E_TYPE", node.expression.span);
|
|
241
|
+
}
|
|
242
|
+
if (node.kind === "when" &&
|
|
243
|
+
infer(node.condition, get, budget) !== "boolean")
|
|
244
|
+
fail("E_TYPE", node.condition.span);
|
|
245
|
+
if (node.kind === "data-table")
|
|
246
|
+
schema(node.source, node.span);
|
|
247
|
+
if (node.kind !== "chart")
|
|
248
|
+
return;
|
|
249
|
+
budget.limit("chartCount", ++charts, node.span);
|
|
250
|
+
const c = node.chart, columns = schema(c.source, c.span);
|
|
251
|
+
const x = columns.find((col) => col.name === c.x);
|
|
252
|
+
if (!x)
|
|
253
|
+
fail("E_COLUMN_UNKNOWN", locations.get(`${c.id}:option:x`));
|
|
254
|
+
if (x.type === "boolean" ||
|
|
255
|
+
((c.kind === "scatter" || c.kind === "histogram") && x.type !== "number"))
|
|
256
|
+
fail("E_TYPE", c.span);
|
|
257
|
+
if (c.y?.kind === "columns") {
|
|
258
|
+
budget.limit("chartSeries", c.y.columns.length, c.span);
|
|
259
|
+
for (const name of c.y.columns) {
|
|
260
|
+
const col = columns.find((col) => col.name === name);
|
|
261
|
+
if (!col)
|
|
262
|
+
fail("E_COLUMN_UNKNOWN", locations.get(`${c.id}:option:y`));
|
|
263
|
+
if (col.type !== "number")
|
|
264
|
+
fail("E_TYPE", c.span);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (c.y && infer(c.y.expression, get, budget, columns) !== "number")
|
|
268
|
+
fail("E_TYPE", c.y.expression.span);
|
|
269
|
+
if (c.select) {
|
|
270
|
+
const target = controls.get(c.select);
|
|
271
|
+
if (!target || target.kind !== (x.type === "text" ? "choice" : "slider"))
|
|
272
|
+
fail("E_SELECTION_TARGET", c.span);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
const constants = new Map();
|
|
276
|
+
for (const d of order)
|
|
277
|
+
if (d.kind === "let")
|
|
278
|
+
constants.set(d.name, evaluate(d.expression, {
|
|
279
|
+
get: (name) => constants.get(name),
|
|
280
|
+
metadata: ir.metadata,
|
|
281
|
+
budget,
|
|
282
|
+
}));
|
|
283
|
+
return {
|
|
284
|
+
types,
|
|
285
|
+
order,
|
|
286
|
+
controls,
|
|
287
|
+
constants,
|
|
288
|
+
limits: budget.limits,
|
|
289
|
+
locations,
|
|
290
|
+
compileOperations: budget.used,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PreparedChart } from "@tinta-lang/types";
|
|
2
|
+
/** Viewport-independent presentation derivation; never changes document state. */
|
|
3
|
+
export declare function numericDomain(values: readonly number[], zero?: boolean): [number, number];
|
|
4
|
+
export declare function legendKey(chart: PreparedChart, index: number): string;
|
|
5
|
+
export declare function chartPresentation(chart: PreparedChart, hidden?: ReadonlySet<string>): {
|
|
6
|
+
points: import("@tinta-lang/types").ChartPoint[];
|
|
7
|
+
total: number;
|
|
8
|
+
message: string;
|
|
9
|
+
xDomain: [number, number] | undefined;
|
|
10
|
+
yDomain: [number, number] | undefined;
|
|
11
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Viewport-independent presentation derivation; never changes document state. */
|
|
2
|
+
export function numericDomain(values, zero = false) {
|
|
3
|
+
let lo = Math.min(...values);
|
|
4
|
+
let hi = Math.max(...values);
|
|
5
|
+
if (zero) {
|
|
6
|
+
lo = Math.min(0, lo);
|
|
7
|
+
hi = Math.max(0, hi);
|
|
8
|
+
}
|
|
9
|
+
if (lo === hi) {
|
|
10
|
+
const delta = Math.max(1, Math.abs(lo) * 0.05);
|
|
11
|
+
lo -= delta;
|
|
12
|
+
hi += delta;
|
|
13
|
+
}
|
|
14
|
+
if (!Number.isFinite(lo) ||
|
|
15
|
+
!Number.isFinite(hi) ||
|
|
16
|
+
!Number.isFinite(hi - lo) ||
|
|
17
|
+
hi <= lo) {
|
|
18
|
+
throw new Error("E_CHART_DOMAIN");
|
|
19
|
+
}
|
|
20
|
+
return [lo, hi];
|
|
21
|
+
}
|
|
22
|
+
export function legendKey(chart, index) {
|
|
23
|
+
const point = chart.points[index];
|
|
24
|
+
return chart.type === "pie" || chart.type === "donut"
|
|
25
|
+
? JSON.stringify(point.x)
|
|
26
|
+
: chart.series[point.seriesIndex].id;
|
|
27
|
+
}
|
|
28
|
+
export function chartPresentation(chart, hidden = new Set()) {
|
|
29
|
+
const sector = chart.type === "pie" || chart.type === "donut";
|
|
30
|
+
const points = chart.points.filter((_, i) => !hidden.has(legendKey(chart, i)));
|
|
31
|
+
const total = sector ? points.reduce((sum, point) => sum + point.y, 0) : 0;
|
|
32
|
+
if (!Number.isFinite(total))
|
|
33
|
+
throw new Error("E_CHART_DOMAIN");
|
|
34
|
+
const message = chart.empty
|
|
35
|
+
? "No data matches the current choices."
|
|
36
|
+
: !points.length && chart.type !== "histogram"
|
|
37
|
+
? sector
|
|
38
|
+
? "All categories are hidden."
|
|
39
|
+
: "All series are hidden."
|
|
40
|
+
: sector && total === 0
|
|
41
|
+
? "All visible values are zero."
|
|
42
|
+
: "";
|
|
43
|
+
const numeric = typeof chart.points[0]?.x === "number";
|
|
44
|
+
const xDomain = chart.bins.length
|
|
45
|
+
? [chart.bins[0].lower, chart.bins[chart.bins.length - 1].upper]
|
|
46
|
+
: !sector && numeric && chart.type !== "bar" && chart.points.length
|
|
47
|
+
? numericDomain(chart.points.map((point) => point.x))
|
|
48
|
+
: undefined;
|
|
49
|
+
const yDomain = chart.bins.length
|
|
50
|
+
? numericDomain(chart.bins.map((bin) => bin.count), true)
|
|
51
|
+
: !sector && points.length
|
|
52
|
+
? numericDomain(points.map((point) => point.y), chart.type === "bar" || chart.type === "area")
|
|
53
|
+
: undefined;
|
|
54
|
+
return {
|
|
55
|
+
points,
|
|
56
|
+
total,
|
|
57
|
+
message,
|
|
58
|
+
xDomain,
|
|
59
|
+
yDomain,
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/charts.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ChartSpec, ControlDefinition, PreparedChart, TableValue } from "@tinta-lang/types";
|
|
2
|
+
import { type Environment } from "./expression.js";
|
|
3
|
+
export declare function prepare(chart: ChartSpec, table: TableValue, env: Environment, controls: ReadonlyMap<string, ControlDefinition>): PreparedChart;
|
package/dist/charts.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { fail, label } from "./common.js";
|
|
2
|
+
import { evaluate } from "./expression.js";
|
|
3
|
+
import { inDomain } from "./grid.js";
|
|
4
|
+
import { numericDomain } from "./chart-presentation.js";
|
|
5
|
+
function domain(values, zero, chart) {
|
|
6
|
+
if (!values.length)
|
|
7
|
+
return;
|
|
8
|
+
try {
|
|
9
|
+
numericDomain(values, zero);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
fail("E_CHART_DOMAIN", chart.span, "evaluate");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function prepare(chart, table, env, controls) {
|
|
16
|
+
const budget = env.budget;
|
|
17
|
+
budget.limit("chartRows", table.rows.length, chart.span);
|
|
18
|
+
const xIndex = table.columns.findIndex((c) => c.name === chart.x);
|
|
19
|
+
const numericX = table.columns[xIndex].type === "number";
|
|
20
|
+
const sector = chart.kind === "pie" || chart.kind === "donut";
|
|
21
|
+
if (sector)
|
|
22
|
+
budget.limit("chartSectors", table.rows.length, chart.span);
|
|
23
|
+
if (chart.kind === "bar" ||
|
|
24
|
+
(!numericX && (chart.kind === "line" || chart.kind === "area")))
|
|
25
|
+
budget.limit("chartCategories", table.rows.length, chart.span);
|
|
26
|
+
const rows = [...table.rows];
|
|
27
|
+
if (numericX && (chart.kind === "line" || chart.kind === "area"))
|
|
28
|
+
rows.sort((a, b) => {
|
|
29
|
+
budget.charge(chart.span);
|
|
30
|
+
return a.cells[xIndex] - b.cells[xIndex];
|
|
31
|
+
});
|
|
32
|
+
const xs = rows.map((row) => row.cells[xIndex]);
|
|
33
|
+
if (chart.kind !== "scatter" &&
|
|
34
|
+
chart.kind !== "histogram" &&
|
|
35
|
+
new Set(xs).size !== xs.length)
|
|
36
|
+
fail("E_CHART_DUPLICATE_X", chart.span, "evaluate");
|
|
37
|
+
if (chart.select)
|
|
38
|
+
for (const x of xs)
|
|
39
|
+
if (!inDomain(controls.get(chart.select), x))
|
|
40
|
+
fail("E_SELECTION_DOMAIN", chart.span, "evaluate");
|
|
41
|
+
const series = chart.y?.kind === "columns"
|
|
42
|
+
? chart.y.columns.map((name) => ({ id: name, label: label(name) }))
|
|
43
|
+
: chart.y
|
|
44
|
+
? [{ id: "value", label: chart.yLabel }]
|
|
45
|
+
: [];
|
|
46
|
+
const points = [];
|
|
47
|
+
const bins = [];
|
|
48
|
+
if (chart.kind === "histogram" && rows.length) {
|
|
49
|
+
const values = xs;
|
|
50
|
+
let lo = Math.min(...values), hi = Math.max(...values);
|
|
51
|
+
if (lo === hi) {
|
|
52
|
+
const delta = Math.max(0.5, Math.abs(lo) * 0.05);
|
|
53
|
+
lo -= delta;
|
|
54
|
+
hi += delta;
|
|
55
|
+
}
|
|
56
|
+
if (!Number.isFinite(lo) ||
|
|
57
|
+
!Number.isFinite(hi) ||
|
|
58
|
+
!Number.isFinite(hi - lo) ||
|
|
59
|
+
hi <= lo)
|
|
60
|
+
fail("E_CHART_DOMAIN", chart.span, "evaluate");
|
|
61
|
+
const count = chart.bins;
|
|
62
|
+
const edges = Array.from({ length: count + 1 }, (_, i) => i === 0 ? lo : i === count ? hi : lo + (i * (hi - lo)) / count);
|
|
63
|
+
for (let i = 0; i < count; i++) {
|
|
64
|
+
budget.charge(chart.span);
|
|
65
|
+
if (!Number.isFinite(edges[i + 1]) || edges[i + 1] <= edges[i])
|
|
66
|
+
fail("E_CHART_DOMAIN", chart.span, "evaluate");
|
|
67
|
+
bins.push({
|
|
68
|
+
id: `${chart.id}:bin:${i}`,
|
|
69
|
+
lower: edges[i],
|
|
70
|
+
upper: edges[i + 1],
|
|
71
|
+
upperInclusive: i === count - 1,
|
|
72
|
+
count: 0,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const counts = bins.map(() => 0);
|
|
76
|
+
for (const x of values) {
|
|
77
|
+
budget.charge(chart.span);
|
|
78
|
+
const index = bins.findIndex((bin) => x >= bin.lower &&
|
|
79
|
+
(x < bin.upper || (bin.upperInclusive && x === bin.upper)));
|
|
80
|
+
counts[index]++;
|
|
81
|
+
}
|
|
82
|
+
bins.forEach((bin, i) => {
|
|
83
|
+
bins[i] = { ...bin, count: counts[i] };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else if (chart.y) {
|
|
87
|
+
let total = 0;
|
|
88
|
+
for (const row of rows) {
|
|
89
|
+
budget.charge(chart.span);
|
|
90
|
+
const fields = new Map(table.columns.map((c, i) => [c.name, row.cells[i]]));
|
|
91
|
+
const ys = chart.y.kind === "columns"
|
|
92
|
+
? chart.y.columns.map((name) => row.cells[table.columns.findIndex((c) => c.name === name)])
|
|
93
|
+
: [evaluate(chart.y.expression, { ...env, row: fields })];
|
|
94
|
+
ys.forEach((y, i) => {
|
|
95
|
+
budget.charge(chart.span);
|
|
96
|
+
if (!Number.isFinite(y) || (sector && y < 0))
|
|
97
|
+
fail("E_CHART_VALUE", chart.span, "evaluate");
|
|
98
|
+
if (sector) {
|
|
99
|
+
total += y;
|
|
100
|
+
if (!Number.isFinite(total))
|
|
101
|
+
fail("E_CHART_DOMAIN", chart.span, "evaluate");
|
|
102
|
+
}
|
|
103
|
+
points.push({
|
|
104
|
+
id: `${chart.id}:${row.id}:${i}`,
|
|
105
|
+
rowId: row.id,
|
|
106
|
+
seriesIndex: i,
|
|
107
|
+
x: row.cells[xIndex],
|
|
108
|
+
y,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (!sector) {
|
|
113
|
+
if (numericX && chart.kind !== "bar")
|
|
114
|
+
domain(xs, false, chart);
|
|
115
|
+
domain(points.map((p) => p.y), chart.kind === "bar" || chart.kind === "area", chart);
|
|
116
|
+
// Any series can remain visible alone after a presentation-only toggle.
|
|
117
|
+
for (let index = 0; index < series.length; index++) {
|
|
118
|
+
domain(points
|
|
119
|
+
.filter((point) => point.seriesIndex === index)
|
|
120
|
+
.map((point) => point.y), chart.kind === "bar" || chart.kind === "area", chart);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
budget.limit("chartMarks", points.length + bins.length, chart.span);
|
|
125
|
+
return {
|
|
126
|
+
id: chart.id,
|
|
127
|
+
type: chart.kind,
|
|
128
|
+
title: chart.title,
|
|
129
|
+
xLabel: chart.xLabel,
|
|
130
|
+
yLabel: chart.yLabel,
|
|
131
|
+
source: chart.source,
|
|
132
|
+
...(chart.select ? { select: chart.select } : {}),
|
|
133
|
+
series,
|
|
134
|
+
points,
|
|
135
|
+
bins,
|
|
136
|
+
table: { columns: table.columns, rows },
|
|
137
|
+
empty: rows.length === 0,
|
|
138
|
+
};
|
|
139
|
+
}
|
package/dist/codec.d.ts
ADDED
package/dist/codec.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { attempt, Budget, fail } from "./common.js";
|
|
2
|
+
import { compile, programs } from "./compiler.js";
|
|
3
|
+
import validate from "./program-validator.js";
|
|
4
|
+
import { inspect } from "./inspect.js";
|
|
5
|
+
function same(a, b, budget) {
|
|
6
|
+
budget.charge();
|
|
7
|
+
if (a === b)
|
|
8
|
+
return true;
|
|
9
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object")
|
|
10
|
+
return false;
|
|
11
|
+
const x = a, y = b;
|
|
12
|
+
return (Array.isArray(a) === Array.isArray(b) &&
|
|
13
|
+
Object.keys(x).length === Object.keys(y).length &&
|
|
14
|
+
Object.keys(x).every((key) => Object.hasOwn(y, key) && same(x[key], y[key], budget)));
|
|
15
|
+
}
|
|
16
|
+
export function decodeProgram(payload, options) {
|
|
17
|
+
const span = {
|
|
18
|
+
uri: "tinta:program",
|
|
19
|
+
start: { offset: 0, line: 1, column: 1 },
|
|
20
|
+
end: { offset: 0, line: 1, column: 1 },
|
|
21
|
+
};
|
|
22
|
+
const checked = attempt(() => {
|
|
23
|
+
const budget = new Budget(span, "decode", options);
|
|
24
|
+
inspect(payload, budget);
|
|
25
|
+
if (!validate(payload))
|
|
26
|
+
fail("E_PROTOCOL", span, "decode");
|
|
27
|
+
return { ir: payload, budget };
|
|
28
|
+
});
|
|
29
|
+
if (!checked.ok)
|
|
30
|
+
return checked;
|
|
31
|
+
const { ir, budget } = checked.value;
|
|
32
|
+
const compiled = compile({
|
|
33
|
+
uri: ir.sourceUri,
|
|
34
|
+
profile: ir.profile,
|
|
35
|
+
text: ir.source,
|
|
36
|
+
}, options);
|
|
37
|
+
if (!compiled.ok)
|
|
38
|
+
return compiled;
|
|
39
|
+
return attempt(() => {
|
|
40
|
+
budget.charge(span, programs.get(compiled.value).compileOperations);
|
|
41
|
+
if (ir.sourceSha256 !== compiled.value.sourceSha256)
|
|
42
|
+
fail("E_HASH_MISMATCH", span, "decode");
|
|
43
|
+
if (!same(ir, compiled.value, budget))
|
|
44
|
+
fail("E_PROTOCOL", span, "decode");
|
|
45
|
+
return compiled.value;
|
|
46
|
+
}, compiled.diagnostics);
|
|
47
|
+
}
|
|
48
|
+
export function encodeProgram(program) {
|
|
49
|
+
if (!programs.has(program))
|
|
50
|
+
throw new TypeError("Only validated programs can be encoded.");
|
|
51
|
+
return program;
|
|
52
|
+
}
|
package/dist/common.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CompilerOptions, Diagnostic, Phase, Result, Span } from "@tinta-lang/types";
|
|
2
|
+
import { registry } from "./registry.js";
|
|
3
|
+
export declare class Failure extends Error {
|
|
4
|
+
readonly diagnostic: Diagnostic;
|
|
5
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
6
|
+
constructor(input: Diagnostic | readonly Diagnostic[]);
|
|
7
|
+
}
|
|
8
|
+
export declare function fail(code: keyof typeof registry.errors, span: Span, phase?: Phase): never;
|
|
9
|
+
export declare function orderedDiagnostics(diagnostics: readonly Diagnostic[], maximum?: number): readonly Diagnostic[];
|
|
10
|
+
export declare function attempt<T>(run: () => T, diagnostics?: readonly Diagnostic[], maximumDiagnostics?: number): Result<T>;
|
|
11
|
+
export declare function freeze<T>(value: T): T;
|
|
12
|
+
export declare function record<T>(): Record<string, T>;
|
|
13
|
+
export declare class Budget {
|
|
14
|
+
readonly span: Span;
|
|
15
|
+
readonly phase: Phase;
|
|
16
|
+
readonly limits: Record<string, number>;
|
|
17
|
+
private operations;
|
|
18
|
+
get used(): number;
|
|
19
|
+
constructor(span: Span, phase: Phase, options?: CompilerOptions);
|
|
20
|
+
limit(name: string, observed: number, span?: Span): void;
|
|
21
|
+
charge(span?: Span, amount?: number): void;
|
|
22
|
+
}
|
|
23
|
+
export declare const label: (name: string) => string;
|
|
24
|
+
export declare const scalarType: (value: string | number | boolean) => "text" | "number" | "boolean";
|