@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/dist/common.js ADDED
@@ -0,0 +1,128 @@
1
+ import { registry } from "./registry.js";
2
+ export class Failure extends Error {
3
+ diagnostic;
4
+ diagnostics;
5
+ constructor(input) {
6
+ const diagnostics = Array.isArray(input) ? input : [input];
7
+ super(diagnostics[0].message);
8
+ this.diagnostic = diagnostics[0];
9
+ this.diagnostics = diagnostics;
10
+ }
11
+ }
12
+ export function fail(code, span, phase = "type") {
13
+ throw new Failure({
14
+ code,
15
+ severity: "error",
16
+ phase,
17
+ message: registry.errors[code],
18
+ span,
19
+ });
20
+ }
21
+ export function orderedDiagnostics(diagnostics, maximum = 100) {
22
+ const phases = [
23
+ "decode",
24
+ "parse",
25
+ "bind",
26
+ "type",
27
+ "evaluate",
28
+ "assets",
29
+ "export",
30
+ "host",
31
+ ];
32
+ const sorted = [...diagnostics].sort((a, b) => Number(a.details?.limitName === "diagnostics") -
33
+ Number(b.details?.limitName === "diagnostics") ||
34
+ phases.indexOf(a.phase) - phases.indexOf(b.phase) ||
35
+ a.span.start.offset - b.span.start.offset ||
36
+ (a.code < b.code ? -1 : a.code > b.code ? 1 : 0));
37
+ if (sorted.length > maximum) {
38
+ const last = sorted[maximum - 1];
39
+ return [
40
+ ...sorted.slice(0, maximum - 1),
41
+ {
42
+ code: "E_LIMIT",
43
+ severity: "error",
44
+ phase: last.phase,
45
+ message: "The diagnostic limit was exceeded.",
46
+ span: last.span,
47
+ details: {
48
+ limitName: "diagnostics",
49
+ observed: sorted.length,
50
+ maximum,
51
+ },
52
+ },
53
+ ];
54
+ }
55
+ return sorted;
56
+ }
57
+ export function attempt(run, diagnostics = [], maximumDiagnostics = 100) {
58
+ try {
59
+ const value = run();
60
+ return {
61
+ ok: true,
62
+ value,
63
+ diagnostics: orderedDiagnostics(diagnostics, maximumDiagnostics),
64
+ };
65
+ }
66
+ catch (error) {
67
+ if (error instanceof Failure)
68
+ return {
69
+ ok: false,
70
+ diagnostics: orderedDiagnostics([...diagnostics, ...error.diagnostics], maximumDiagnostics),
71
+ };
72
+ throw error;
73
+ }
74
+ }
75
+ export function freeze(value) {
76
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
77
+ for (const child of Object.values(value))
78
+ freeze(child);
79
+ Object.freeze(value);
80
+ }
81
+ return value;
82
+ }
83
+ export function record() {
84
+ return Object.create(null);
85
+ }
86
+ export class Budget {
87
+ span;
88
+ phase;
89
+ limits = { ...registry.limits };
90
+ operations = 0;
91
+ get used() {
92
+ return this.operations;
93
+ }
94
+ constructor(span, phase, options) {
95
+ this.span = span;
96
+ this.phase = phase;
97
+ for (const [key, value] of Object.entries(options?.limits ?? {})) {
98
+ if (!Object.hasOwn(this.limits, key) ||
99
+ !Number.isSafeInteger(value) ||
100
+ value < 1 ||
101
+ value > this.limits[key])
102
+ fail("E_LIMIT", span, phase);
103
+ this.limits[key] = value;
104
+ }
105
+ }
106
+ limit(name, observed, span = this.span) {
107
+ const maximum = this.limits[name];
108
+ if (observed > maximum)
109
+ throw new Failure({
110
+ code: "E_LIMIT",
111
+ severity: "error",
112
+ phase: this.phase,
113
+ message: "A resource limit was exceeded.",
114
+ span,
115
+ details: { limitName: name, observed, maximum },
116
+ });
117
+ }
118
+ charge(span = this.span, amount = 1) {
119
+ this.operations += amount;
120
+ this.limit(this.phase === "evaluate" ? "evaluationOperations" : "compileOperations", this.operations, span);
121
+ }
122
+ }
123
+ export const label = (name) => name.replaceAll("_", " ");
124
+ export const scalarType = (value) => typeof value === "string"
125
+ ? "text"
126
+ : typeof value === "number"
127
+ ? "number"
128
+ : "boolean";
@@ -0,0 +1,3 @@
1
+ import type { CompilerOptions, Program, Result, SourceInput } from "@tinta-lang/types";
2
+ export { programs } from "./programs.js";
3
+ export declare function compile(input: SourceInput, options?: CompilerOptions): Result<Program>;
@@ -0,0 +1,458 @@
1
+ import { parse, } from "@tinta-lang/syntax";
2
+ import { sha256 } from "@noble/hashes/sha2.js";
3
+ import { bytesToHex } from "@noble/hashes/utils.js";
4
+ import { attempt, Budget, fail, freeze, label } from "./common.js";
5
+ import { grid, inDomain, units } from "./grid.js";
6
+ import { bind } from "./binding.js";
7
+ import { registry } from "./registry.js";
8
+ import { programs } from "./programs.js";
9
+ export { programs } from "./programs.js";
10
+ function literal(expr) {
11
+ if (expr.kind === "literal")
12
+ return expr.value;
13
+ if (expr.kind === "unary" &&
14
+ expr.operator !== "not" &&
15
+ expr.operand.kind === "literal" &&
16
+ typeof expr.operand.value === "number")
17
+ return (expr.operator === "-" ? -1 : 1) * expr.operand.value || 0;
18
+ fail("E_PROPERTY_VALUE", expr.span);
19
+ }
20
+ function option(d, key, fallback, node) {
21
+ const expr = d.options[key];
22
+ const value = expr ? literal(expr) : fallback;
23
+ if (typeof value !== typeof fallback)
24
+ fail("E_PROPERTY_VALUE", expr?.span ?? node.span);
25
+ return value;
26
+ }
27
+ export function compile(input, options) {
28
+ const parsed = parse(input, options);
29
+ if (!parsed.ok)
30
+ return parsed;
31
+ return attempt(() => {
32
+ const { tree, source, bytes } = parsed.value;
33
+ const budget = new Budget(tree.span, "bind", options);
34
+ budget.charge(tree.span, parsed.value.compileOperations);
35
+ const declarations = [];
36
+ const locations = new Map();
37
+ const heading = tree.children.find((n) => n.kind === "heading");
38
+ const plain = (node) => node.kind === "interpolation"
39
+ ? ""
40
+ : (node.value ?? node.children.map(plain).join(""));
41
+ let metadata = {
42
+ version: "1.0",
43
+ title: heading ? plain(heading) : "Untitled document",
44
+ locale: "en-US",
45
+ currency: "USD",
46
+ };
47
+ function convert(node) {
48
+ budget.charge(node.span);
49
+ const base = { id: node.id, span: node.span };
50
+ const children = () => node.children.flatMap(convert);
51
+ const a = node.attributes ?? {};
52
+ if (node.directive) {
53
+ const d = node.directive, name = d.binding ?? "";
54
+ const declaration = { ...base, name };
55
+ const staticLiteral = (expression) => {
56
+ const raw = source.slice(expression.span.start.offset, expression.span.end.offset);
57
+ if (expression.kind === "literal")
58
+ return raw === expression.raw;
59
+ if (expression.kind === "unary")
60
+ return ((expression.operator === "+" || expression.operator === "-") &&
61
+ expression.operand.kind === "literal" &&
62
+ typeof expression.operand.value === "number" &&
63
+ raw === expression.operator + expression.operand.raw);
64
+ if (expression.kind === "list")
65
+ return (raw.startsWith("[") &&
66
+ raw.endsWith("]") &&
67
+ expression.items.every(staticLiteral));
68
+ return false;
69
+ };
70
+ for (const [key, expression] of Object.entries(d.options)) {
71
+ locations.set(`${node.id}:option:${key}`, expression.span);
72
+ if (d.name === "view" && key === "where")
73
+ continue;
74
+ if (d.name === "chart" && key === "select") {
75
+ if (expression.kind !== "name" ||
76
+ source.slice(expression.span.start.offset, expression.span.end.offset) !== expression.name)
77
+ fail("E_SELECTION_TARGET", expression.span);
78
+ continue;
79
+ }
80
+ if (d.name === "chart" &&
81
+ key === "y" &&
82
+ expression.kind !== "list" &&
83
+ !(expression.kind === "literal" &&
84
+ typeof expression.value === "string"))
85
+ continue;
86
+ if (!staticLiteral(expression))
87
+ fail(d.name === "chart" && key === "y"
88
+ ? "E_CHART_SERIES"
89
+ : "E_PROPERTY_VALUE", expression.span);
90
+ }
91
+ d.columnSpans?.forEach((span, i) => locations.set(`${node.id}:column:${i}`, span));
92
+ d.rows?.forEach((row, i) => {
93
+ locations.set(`${node.id}:${i}`, row.span);
94
+ row.cellSpans.forEach((span, j) => locations.set(`${node.id}:${i}:cell:${j}`, span));
95
+ });
96
+ const text = (key, fallback) => {
97
+ const value = option(d, key, fallback, node);
98
+ budget.limit("labelCodePoints", [...value].length, d.options[key]?.span ?? node.span);
99
+ return value;
100
+ };
101
+ switch (d.name) {
102
+ case "meta": {
103
+ const version = option(d, "version", "1.0", node);
104
+ if (version !== "1.0")
105
+ fail("E_VERSION", d.options.version.span);
106
+ const locale = option(d, "locale", "en-US", node), currency = option(d, "currency", "USD", node);
107
+ if (!registry.locales.includes(locale) ||
108
+ !registry.currencies.includes(currency))
109
+ fail("E_META_VALUE", node.span);
110
+ metadata = {
111
+ version,
112
+ title: text("title", metadata.title),
113
+ locale: locale,
114
+ currency: currency,
115
+ };
116
+ return [];
117
+ }
118
+ case "let":
119
+ case "calc":
120
+ declarations.push({
121
+ ...declaration,
122
+ kind: d.name,
123
+ expression: d.expression,
124
+ });
125
+ return [];
126
+ case "data":
127
+ declarations.push({
128
+ ...declaration,
129
+ kind: "data",
130
+ table: {
131
+ columns: d.columns,
132
+ rows: d.rows.map((row, i) => ({
133
+ id: `${node.id}:${i}`,
134
+ cells: row.cells.map((value) => typeof value === "number" && value === 0 ? 0 : value),
135
+ })),
136
+ },
137
+ });
138
+ return [];
139
+ case "view": {
140
+ if (d.options.descending && !d.options.sort)
141
+ fail("E_PROPERTY_DEPENDENCY", d.options.descending.span);
142
+ declarations.push({
143
+ ...declaration,
144
+ kind: "view",
145
+ source: d.source,
146
+ descending: option(d, "descending", false, node),
147
+ ...(d.options.where ? { where: d.options.where } : {}),
148
+ ...(d.options.sort
149
+ ? { sort: option(d, "sort", "", node) }
150
+ : {}),
151
+ });
152
+ return [];
153
+ }
154
+ case "slider":
155
+ case "toggle":
156
+ case "choice": {
157
+ const initial = literal(d.expression);
158
+ const controlLabel = text("label", label(name));
159
+ if (!controlLabel.trim())
160
+ fail("E_PROPERTY_VALUE", d.options.label?.span ?? node.span);
161
+ const controlBase = {
162
+ ...declaration,
163
+ label: controlLabel,
164
+ };
165
+ let control;
166
+ if (d.name === "slider") {
167
+ const min = option(d, "min", 0, node), max = option(d, "max", 100, node), step = option(d, "step", 1, node);
168
+ const raw = (key, value) => {
169
+ const e = d.options[key];
170
+ return e
171
+ ? source.slice(e.span.start.offset, e.span.end.offset)
172
+ : String(value);
173
+ };
174
+ control = {
175
+ ...controlBase,
176
+ kind: "slider",
177
+ initial: initial,
178
+ min,
179
+ max,
180
+ step,
181
+ grid: grid(raw("min", min), raw("max", max), raw("step", step), node.span),
182
+ };
183
+ if (units(source.slice(d.expression.span.start.offset, d.expression.span.end.offset)) === undefined)
184
+ fail("E_STATE_DOMAIN", node.span);
185
+ budget.limit("sliderTicks", control.grid.ticks, node.span);
186
+ budget.limit("sliderMagnitude", Math.max(Math.abs(min), Math.abs(max)), node.span);
187
+ const divisor = 10n ** BigInt(6 - budget.limits.sliderFractionalDigits);
188
+ for (const value of [
189
+ control.grid.minUnits,
190
+ control.grid.stepUnits,
191
+ String(units(raw("max", max))),
192
+ String(units(source.slice(d.expression.span.start.offset, d.expression.span.end.offset))),
193
+ ]) {
194
+ if (BigInt(value) % divisor !== 0n)
195
+ fail("E_STATE_DOMAIN", node.span);
196
+ }
197
+ }
198
+ else if (d.name === "toggle")
199
+ control = {
200
+ ...controlBase,
201
+ kind: "toggle",
202
+ initial: initial,
203
+ };
204
+ else {
205
+ const e = d.options.options;
206
+ if (!e)
207
+ fail("E_PROPERTY_REQUIRED", node.span);
208
+ if (e.kind !== "list" || e.items.length === 0)
209
+ fail("E_PROPERTY_VALUE", e.span);
210
+ budget.limit("choiceOptions", e.items.length, e.span);
211
+ const values = e.items.map((item) => {
212
+ const value = literal(item);
213
+ if (typeof value !== "string" || !value.length)
214
+ fail("E_PROPERTY_VALUE", item.span);
215
+ budget.limit("choiceOptionCodePoints", [...value].length, item.span);
216
+ return value;
217
+ });
218
+ if (new Set(values).size !== values.length)
219
+ fail("E_PROPERTY_VALUE", e.span);
220
+ control = {
221
+ ...controlBase,
222
+ kind: "choice",
223
+ initial: initial,
224
+ options: values,
225
+ };
226
+ }
227
+ if (!inDomain(control, initial))
228
+ fail("E_STATE_DOMAIN", d.expression.span);
229
+ declarations.push(control);
230
+ return [{ ...base, kind: "control", name }];
231
+ }
232
+ case "when":
233
+ return [
234
+ {
235
+ ...base,
236
+ kind: "when",
237
+ condition: d.expression,
238
+ children: children(),
239
+ },
240
+ ];
241
+ case "details":
242
+ if (d.summary !== undefined && !d.summary.trim())
243
+ fail("E_PROPERTY_VALUE", node.span);
244
+ budget.limit("labelCodePoints", [...(d.summary ?? "More information")].length, node.span);
245
+ return [
246
+ {
247
+ ...base,
248
+ kind: "details",
249
+ summary: d.summary ?? "More information",
250
+ children: children(),
251
+ },
252
+ ];
253
+ case "table":
254
+ return [
255
+ {
256
+ ...base,
257
+ kind: "data-table",
258
+ source: d.source,
259
+ title: text("title", label(d.source)),
260
+ },
261
+ ];
262
+ case "chart": {
263
+ const kind = d.chartType;
264
+ if (!d.options.x || (kind !== "histogram" && !d.options.y))
265
+ fail("E_PROPERTY_REQUIRED", node.span);
266
+ if ((kind === "histogram" && (d.options.y || d.options.select)) ||
267
+ (kind !== "histogram" && d.options.bins))
268
+ fail("E_PROPERTY_VALUE", node.span);
269
+ const y = d.options.y;
270
+ let series;
271
+ if (y?.kind === "literal" && typeof y.value === "string")
272
+ series = { kind: "columns", columns: [y.value] };
273
+ else if (y?.kind === "list") {
274
+ const columns = y.items.map((e) => {
275
+ if (e.kind !== "literal" || typeof e.value !== "string")
276
+ fail("E_CHART_SERIES", e.span);
277
+ return e.value;
278
+ });
279
+ if (!columns.length ||
280
+ columns.length > 6 ||
281
+ new Set(columns).size !== columns.length)
282
+ fail("E_CHART_SERIES", y.span);
283
+ series = { kind: "columns", columns };
284
+ }
285
+ else if (y)
286
+ series = { kind: "expression", expression: y };
287
+ if ((kind === "pie" || kind === "donut") &&
288
+ series?.kind === "columns" &&
289
+ series.columns.length !== 1)
290
+ fail("E_CHART_SERIES", y.span);
291
+ const select = d.options.select;
292
+ if (select && select.kind !== "name")
293
+ fail("E_SELECTION_TARGET", select.span);
294
+ const bins = option(d, "bins", 8, node);
295
+ if (!Number.isInteger(bins) || bins < 2 || bins > 30)
296
+ fail("E_PROPERTY_VALUE", node.span);
297
+ if (kind === "histogram")
298
+ budget.limit("histogramBinsMax", bins, node.span);
299
+ const typeLabel = {
300
+ line: "Line chart",
301
+ area: "Area chart",
302
+ bar: "Bar chart",
303
+ scatter: "Scatter plot",
304
+ pie: "Pie chart",
305
+ donut: "Donut chart",
306
+ histogram: "Histogram",
307
+ }[kind];
308
+ const x = option(d, "x", "", node);
309
+ const chart = {
310
+ ...base,
311
+ kind,
312
+ source: d.source,
313
+ x,
314
+ title: text("title", `${typeLabel} — ${label(d.source)}`),
315
+ xLabel: text("xLabel", label(x)),
316
+ yLabel: text("yLabel", kind === "histogram"
317
+ ? "Frequency"
318
+ : series?.kind === "columns" && series.columns.length === 1
319
+ ? label(series.columns[0])
320
+ : "Value"),
321
+ ...(series ? { y: series } : {}),
322
+ ...(select?.kind === "name" ? { select: select.name } : {}),
323
+ ...(kind === "histogram" ? { bins } : {}),
324
+ };
325
+ return [{ ...base, kind: "chart", chart }];
326
+ }
327
+ case "end":
328
+ fail("E_BLOCK_END", node.span);
329
+ }
330
+ }
331
+ switch (node.kind) {
332
+ case "root":
333
+ return children();
334
+ case "definition":
335
+ return [];
336
+ case "text":
337
+ return [{ ...base, kind: "text", value: node.value }];
338
+ case "interpolation":
339
+ return [
340
+ {
341
+ ...base,
342
+ kind: "interpolation",
343
+ expression: node.expression,
344
+ },
345
+ ];
346
+ case "html":
347
+ return /^(?:\s*<!--(?:(?!-->)[\s\S])*-->)+\s*$/.test(node.value)
348
+ ? []
349
+ : [{ ...base, kind: "raw-html", value: node.value }];
350
+ case "paragraph":
351
+ case "emphasis":
352
+ case "strong":
353
+ case "delete":
354
+ case "blockquote":
355
+ return [{ ...base, kind: node.kind, children: children() }];
356
+ case "heading":
357
+ return [
358
+ {
359
+ ...base,
360
+ kind: "heading",
361
+ level: a.level,
362
+ anchor: a.anchor,
363
+ children: children(),
364
+ },
365
+ ];
366
+ case "link":
367
+ case "linkReference":
368
+ return [
369
+ {
370
+ ...base,
371
+ kind: "link",
372
+ destination: a.destination,
373
+ ...(typeof a.title === "string" ? { title: a.title } : {}),
374
+ children: children(),
375
+ },
376
+ ];
377
+ case "image":
378
+ case "imageReference":
379
+ return [
380
+ {
381
+ ...base,
382
+ kind: "image",
383
+ destination: a.destination,
384
+ alt: a.alt,
385
+ ...(typeof a.title === "string" ? { title: a.title } : {}),
386
+ },
387
+ ];
388
+ case "code":
389
+ case "inlineCode":
390
+ return [
391
+ {
392
+ ...base,
393
+ kind: "code",
394
+ block: node.kind === "code",
395
+ value: node.value,
396
+ ...(typeof a.language === "string"
397
+ ? { language: a.language }
398
+ : {}),
399
+ },
400
+ ];
401
+ case "break":
402
+ return [{ ...base, kind: "break", hard: true }];
403
+ case "thematicBreak":
404
+ return [{ ...base, kind: "thematic-break" }];
405
+ case "list":
406
+ return [
407
+ {
408
+ ...base,
409
+ kind: "list",
410
+ ordered: a.ordered,
411
+ start: a.start,
412
+ tight: !a.spread,
413
+ items: node.children.map((item) => ({
414
+ id: item.id,
415
+ span: item.span,
416
+ ...(typeof item.attributes?.checked === "boolean"
417
+ ? { checked: item.attributes.checked }
418
+ : {}),
419
+ children: item.children.flatMap(convert),
420
+ })),
421
+ },
422
+ ];
423
+ case "table":
424
+ return [
425
+ {
426
+ ...base,
427
+ kind: "markdown-table",
428
+ align: a.align.map((v) => v ?? "none"),
429
+ header: node.children[0].children.map((cell) => cell.children.flatMap(convert)),
430
+ rows: node.children
431
+ .slice(1)
432
+ .map((row) => row.children.map((cell) => cell.children.flatMap(convert))),
433
+ },
434
+ ];
435
+ default:
436
+ fail("E_INTERNAL", node.span);
437
+ }
438
+ }
439
+ const flow = tree.children.flatMap(convert);
440
+ budget.limit("labelCodePoints", [...metadata.title].length);
441
+ const ir = {
442
+ abi: "tinta-ir-1",
443
+ sourceVersion: "1.0",
444
+ contractRevision: "1.0-draft.3",
445
+ profile: input.profile,
446
+ source,
447
+ sourceUri: input.uri,
448
+ sourceSha256: bytesToHex(sha256(bytes)),
449
+ metadata,
450
+ declarations,
451
+ flow,
452
+ };
453
+ const bound = bind(ir, budget, locations);
454
+ const program = freeze(ir);
455
+ programs.set(program, bound);
456
+ return program;
457
+ }, parsed.diagnostics, options?.limits?.diagnostics);
458
+ }
@@ -0,0 +1,15 @@
1
+ import type { Column, Expr, Metadata, Scalar, ScalarType, TableValue, Value } from "@tinta-lang/types";
2
+ import { Budget } from "./common.js";
3
+ export type Type = ScalarType | "list:never" | "list:text" | "list:number" | "list:boolean" | readonly Column[];
4
+ export type RuntimeValue = Value | TableValue;
5
+ export interface Environment {
6
+ readonly get: (name: string) => RuntimeValue;
7
+ readonly metadata: Metadata;
8
+ readonly budget: Budget;
9
+ readonly row?: ReadonlyMap<string, Scalar>;
10
+ }
11
+ export declare function tableType(type: Type, expr: Expr): readonly Column[];
12
+ export declare function infer(expr: Expr, get: (name: string, expr: Expr) => Type, budget: Budget, columns?: readonly Column[]): Type;
13
+ export declare function numeric(value: number, expr: Expr): number;
14
+ export declare function format(value: Scalar, metadata: Metadata): string;
15
+ export declare function evaluate(expr: Expr, env: Environment): RuntimeValue;