@cotal-ai/lang 0.0.0 → 0.15.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 +202 -0
- package/dist/dryrun.d.ts +106 -0
- package/dist/dryrun.d.ts.map +1 -0
- package/dist/dryrun.js +172 -0
- package/dist/dryrun.js.map +1 -0
- package/dist/duration.d.ts +12 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +34 -0
- package/dist/duration.js.map +1 -0
- package/dist/effects.d.ts +231 -0
- package/dist/effects.d.ts.map +1 -0
- package/dist/effects.js +64 -0
- package/dist/effects.js.map +1 -0
- package/dist/errors.d.ts +141 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +163 -0
- package/dist/errors.js.map +1 -0
- package/dist/grammar.d.ts +28 -0
- package/dist/grammar.d.ts.map +1 -0
- package/dist/grammar.js +793 -0
- package/dist/grammar.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/interpret.d.ts +89 -0
- package/dist/interpret.d.ts.map +1 -0
- package/dist/interpret.js +1117 -0
- package/dist/interpret.js.map +1 -0
- package/dist/journal.d.ts +177 -0
- package/dist/journal.d.ts.map +1 -0
- package/dist/journal.js +198 -0
- package/dist/journal.js.map +1 -0
- package/dist/keys.d.ts +87 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +105 -0
- package/dist/keys.js.map +1 -0
- package/dist/primitives.d.ts +84 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +265 -0
- package/dist/primitives.js.map +1 -0
- package/dist/sim.d.ts +101 -0
- package/dist/sim.d.ts.map +1 -0
- package/dist/sim.js +192 -0
- package/dist/sim.js.map +1 -0
- package/dist/values.d.ts +35 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +0 -0
- package/dist/values.js.map +1 -0
- package/package.json +27 -7
- package/README.md +0 -10
package/dist/grammar.js
ADDED
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The validator: one acorn parse followed by two AST walks, before anything executes.
|
|
3
|
+
*
|
|
4
|
+
* This file is where the design's central claim becomes mechanical rather than aspirational. A
|
|
5
|
+
* program that could reach ambient IO, an ambient clock, host identity, or hidden concurrency
|
|
6
|
+
* does not parse, so "determinism by convention" is not an option an author can take. Every rule
|
|
7
|
+
* in section 3 of the design doc maps to a check here and to a stable error code.
|
|
8
|
+
*
|
|
9
|
+
* Walk 1 is SHAPE: reject forbidden node types. Walk 2 is RESOLUTION: build the scope tree, bind
|
|
10
|
+
* every identifier, and check effect call shape. Both collect every error before reporting, so an
|
|
11
|
+
* author sees the whole repair list at once instead of one item per round trip.
|
|
12
|
+
*/
|
|
13
|
+
import { parse } from "acorn";
|
|
14
|
+
import { LangError, LangErrors } from "./errors.js";
|
|
15
|
+
import { BUILTINS, FORBIDDEN_GLOBALS, NOTIFY_BOUND, PRIMITIVES, PROMISE_NAMES, RESERVED_NAMES, STEP_NAME_RE, primitiveDoc, } from "./primitives.js";
|
|
16
|
+
const ACORN_OPTIONS = {
|
|
17
|
+
ecmaVersion: 2023,
|
|
18
|
+
sourceType: "module",
|
|
19
|
+
locations: true,
|
|
20
|
+
// A program's module body IS the workflow, so top-level `await` is the normal way to write
|
|
21
|
+
// one. Only an await inside a non-async nested function is an error, and walk 1 catches that.
|
|
22
|
+
allowAwaitOutsideFunction: true,
|
|
23
|
+
allowReturnOutsideFunction: false,
|
|
24
|
+
};
|
|
25
|
+
class Validator {
|
|
26
|
+
source;
|
|
27
|
+
file;
|
|
28
|
+
errors = [];
|
|
29
|
+
warnings = [];
|
|
30
|
+
constructor(source, file) {
|
|
31
|
+
this.source = source;
|
|
32
|
+
this.file = file;
|
|
33
|
+
}
|
|
34
|
+
span(node) {
|
|
35
|
+
const loc = node.loc;
|
|
36
|
+
return {
|
|
37
|
+
file: this.file,
|
|
38
|
+
line: loc?.start.line ?? 1,
|
|
39
|
+
column: (loc?.start.column ?? 0) + 1,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
fail(code, node, cause, fix, calleeName) {
|
|
43
|
+
const callee = calleeName === undefined ? undefined : primitiveDoc(calleeName);
|
|
44
|
+
this.errors.push(new LangError(callee === undefined
|
|
45
|
+
? { code, span: this.span(node), cause, fix }
|
|
46
|
+
: { code, span: this.span(node), cause, fix, callee }));
|
|
47
|
+
}
|
|
48
|
+
warn(code, node, cause, fix, calleeName) {
|
|
49
|
+
const callee = calleeName === undefined ? undefined : primitiveDoc(calleeName);
|
|
50
|
+
this.warnings.push(new LangError(callee === undefined
|
|
51
|
+
? { code, span: this.span(node), cause, fix }
|
|
52
|
+
: { code, span: this.span(node), cause, fix, callee }));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// ---- walk 1: shape ------------------------------------------------------------------------
|
|
56
|
+
/** Node types rejected outright, with the code and the repair to suggest. */
|
|
57
|
+
const FORBIDDEN_NODES = Object.freeze({
|
|
58
|
+
ClassDeclaration: {
|
|
59
|
+
code: "L1001",
|
|
60
|
+
cause: "There are no classes in this language. State lives in records and behaviour lives in functions.",
|
|
61
|
+
fix: "Replace the class with a function that returns a record.",
|
|
62
|
+
},
|
|
63
|
+
ClassExpression: {
|
|
64
|
+
code: "L1001",
|
|
65
|
+
cause: "There are no classes in this language. State lives in records and behaviour lives in functions.",
|
|
66
|
+
fix: "Replace the class with a function that returns a record.",
|
|
67
|
+
},
|
|
68
|
+
ThisExpression: {
|
|
69
|
+
code: "L1002",
|
|
70
|
+
cause: "`this` does not exist, so nothing can capture a calling context by accident.",
|
|
71
|
+
fix: "Pass what the function needs as an argument.",
|
|
72
|
+
},
|
|
73
|
+
ForInStatement: {
|
|
74
|
+
code: "L1004",
|
|
75
|
+
cause: "`for...in` walks an unspecified order and reaches inherited names, so it cannot be deterministic.",
|
|
76
|
+
fix: "Iterate explicitly: `for (const k of keys(record)) { ... }`.",
|
|
77
|
+
},
|
|
78
|
+
WithStatement: {
|
|
79
|
+
code: "L1013",
|
|
80
|
+
cause: "`with` makes name resolution dynamic, and every name here resolves at parse time.",
|
|
81
|
+
fix: "Reference the record's fields directly.",
|
|
82
|
+
},
|
|
83
|
+
TaggedTemplateExpression: {
|
|
84
|
+
code: "L1018",
|
|
85
|
+
cause: "A tagged template runs user code during evaluation of a literal, which hides an effect inside what looks like data.",
|
|
86
|
+
fix: "Use a plain template literal, or call the function explicitly.",
|
|
87
|
+
},
|
|
88
|
+
NewExpression: {
|
|
89
|
+
code: "L1019",
|
|
90
|
+
cause: "There are no constructors, so `new` has nothing to construct.",
|
|
91
|
+
fix: "Build a record literal, or call a function that returns one.",
|
|
92
|
+
},
|
|
93
|
+
ImportDeclaration: {
|
|
94
|
+
code: "L1020",
|
|
95
|
+
cause: "A program is exactly one module, because a run pins to the content hash of its source.",
|
|
96
|
+
fix: "Define the function in this file. Shared procedures are ordinary functions.",
|
|
97
|
+
},
|
|
98
|
+
ExportNamedDeclaration: {
|
|
99
|
+
code: "L1020",
|
|
100
|
+
cause: "A program is exactly one module and has nothing to export to.",
|
|
101
|
+
fix: "Remove the `export`.",
|
|
102
|
+
},
|
|
103
|
+
ExportDefaultDeclaration: {
|
|
104
|
+
code: "L1020",
|
|
105
|
+
cause: "A program is exactly one module and has nothing to export to.",
|
|
106
|
+
fix: "Remove the `export`.",
|
|
107
|
+
},
|
|
108
|
+
ExportAllDeclaration: {
|
|
109
|
+
code: "L1020",
|
|
110
|
+
cause: "A program is exactly one module and has nothing to export to.",
|
|
111
|
+
fix: "Remove the `export`.",
|
|
112
|
+
},
|
|
113
|
+
DoWhileStatement: {
|
|
114
|
+
code: "L1022",
|
|
115
|
+
cause: "`do...while` is not in the language.",
|
|
116
|
+
fix: "Use `while` with the condition checked first, or a `for` loop.",
|
|
117
|
+
},
|
|
118
|
+
LabeledStatement: {
|
|
119
|
+
code: "L1017",
|
|
120
|
+
cause: "Labels turn the derived flowchart's back-edges into arbitrary jumps.",
|
|
121
|
+
fix: "Restructure with a helper function or a boolean flag.",
|
|
122
|
+
},
|
|
123
|
+
BreakStatement: {
|
|
124
|
+
code: "L1017",
|
|
125
|
+
cause: "A labelled break is an arbitrary jump.",
|
|
126
|
+
fix: "Restructure with a helper function or a boolean flag.",
|
|
127
|
+
},
|
|
128
|
+
ContinueStatement: {
|
|
129
|
+
code: "L1017",
|
|
130
|
+
cause: "A labelled continue is an arbitrary jump.",
|
|
131
|
+
fix: "Restructure with a helper function or a boolean flag.",
|
|
132
|
+
},
|
|
133
|
+
AwaitExpression: {
|
|
134
|
+
code: "L1023",
|
|
135
|
+
cause: "This `await` sits inside a function that is not `async`. Every effect is awaited, so a function that performs one is async.",
|
|
136
|
+
fix: "Mark the enclosing function `async`: `async function name(...) { ... }`.",
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
/**
|
|
140
|
+
* Automatic semicolon insertion is ALLOWED, against Jessie, and this is a declared deviation.
|
|
141
|
+
*
|
|
142
|
+
* Jessie bans ASI reliance because a newline hazard can silently change what a program means. Two
|
|
143
|
+
* things break that argument here: the author is a language model writing the JavaScript it would
|
|
144
|
+
* write anyway, which is frequently semicolon-free, and ASI is parse-deterministic, so
|
|
145
|
+
* determinism by construction is untouched either way. Banning it also rejected constructs nobody
|
|
146
|
+
* intended, including every `for` loop and the design's own examples.
|
|
147
|
+
*
|
|
148
|
+
* What survives is the part with a live rationale: the two constructs where a newline genuinely
|
|
149
|
+
* changes meaning stay errors, so the hazard is caught without taxing the ordinary program.
|
|
150
|
+
*/
|
|
151
|
+
function checkAsiHazards(block, v) {
|
|
152
|
+
const body = block.body ?? [];
|
|
153
|
+
for (let i = 0; i < body.length - 1; i += 1) {
|
|
154
|
+
const here = body[i];
|
|
155
|
+
const next = body[i + 1];
|
|
156
|
+
if (here.type !== "ReturnStatement")
|
|
157
|
+
continue;
|
|
158
|
+
if (here.argument !== null && here.argument !== undefined)
|
|
159
|
+
continue;
|
|
160
|
+
if (next.type !== "ExpressionStatement")
|
|
161
|
+
continue;
|
|
162
|
+
v.fail("L1008", next, "This value follows a bare `return`, so the statement already ended on the line above and this expression is unreachable. The newline decided that, not the code.", "Put the value on the same line as `return`, or terminate the return with `;` if it was meant to return nothing.");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The continuation hazard, checked where it actually lives.
|
|
167
|
+
*
|
|
168
|
+
* A line beginning with `(` or `[` continues the statement above it rather than starting a new
|
|
169
|
+
* one, and by the time there are two statements to compare the parser has already made that
|
|
170
|
+
* choice: it produced ONE. So the check is on the call itself, looking for a newline between a
|
|
171
|
+
* callee and the `(` that a semicolon would have separated.
|
|
172
|
+
*/
|
|
173
|
+
function checkContinuationHazard(node, v) {
|
|
174
|
+
const inner = node.type === "CallExpression" ? node.callee : node.object;
|
|
175
|
+
if (!isNode(inner))
|
|
176
|
+
return;
|
|
177
|
+
const gap = v.source.slice(inner.end, node.end);
|
|
178
|
+
const openAt = gap.indexOf(node.type === "CallExpression" ? "(" : "[");
|
|
179
|
+
if (openAt < 0)
|
|
180
|
+
return;
|
|
181
|
+
if (!gap.slice(0, openAt).includes("\n"))
|
|
182
|
+
return;
|
|
183
|
+
v.fail("L1008", node, node.type === "CallExpression"
|
|
184
|
+
? "The `(` on this line continues the expression above it, so this is one call rather than two statements. A semicolon is what decides that, and there is not one."
|
|
185
|
+
: "The `[` on this line indexes the expression above it, so this is one expression rather than two statements. A semicolon is what decides that, and there is not one.", "Terminate the previous statement with `;`.");
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Acorn's own parse errors, re-coded. The default is "this is not valid JavaScript", which is
|
|
189
|
+
* true but useless; these are the mistakes worth naming precisely, because every effect in this
|
|
190
|
+
* language is awaited and so this is the mistake an author will actually make.
|
|
191
|
+
*/
|
|
192
|
+
const PARSE_ERROR_MAP = [
|
|
193
|
+
{
|
|
194
|
+
test: /'return' outside of function/i,
|
|
195
|
+
code: "L1024",
|
|
196
|
+
cause: "A program has no return value. Its outcome is what it did: the journal of its effects, and whatever it published onto the run record. There is nobody for a top-level `return` to return to.",
|
|
197
|
+
fix: "Publish the result onto the run record, or use `log(...)` if you only wanted it in the trace.",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
test: /keyword 'await' outside an async function|await is only valid in async/i,
|
|
201
|
+
code: "L1023",
|
|
202
|
+
cause: "This `await` sits inside a function that is not `async`. Every effect is awaited, so a function that performs one is async. A program's top level is already async and needs no marking.",
|
|
203
|
+
fix: "Mark the enclosing function `async`: `async function name(...) { ... }`.",
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
/** Child keys to descend into, per node, without pulling in a walker dependency. */
|
|
207
|
+
function children(node) {
|
|
208
|
+
const out = [];
|
|
209
|
+
for (const key of Object.keys(node)) {
|
|
210
|
+
if (key === "loc" || key === "range" || key === "start" || key === "end" || key === "type") {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const value = node[key];
|
|
214
|
+
if (Array.isArray(value)) {
|
|
215
|
+
for (const v of value)
|
|
216
|
+
if (isNode(v))
|
|
217
|
+
out.push(v);
|
|
218
|
+
}
|
|
219
|
+
else if (isNode(value)) {
|
|
220
|
+
out.push(value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
function isNode(v) {
|
|
226
|
+
return v !== null && typeof v === "object" && typeof v.type === "string";
|
|
227
|
+
}
|
|
228
|
+
function walkShape(node, v, inAsync, parent = null) {
|
|
229
|
+
const type = node.type;
|
|
230
|
+
// Labels and labelled jumps: a bare break/continue is fine, a labelled one is not.
|
|
231
|
+
if (type === "BreakStatement" || type === "ContinueStatement") {
|
|
232
|
+
if (node.label !== null && node.label !== undefined) {
|
|
233
|
+
const r = FORBIDDEN_NODES[type];
|
|
234
|
+
if (r !== undefined)
|
|
235
|
+
v.fail(r.code, node, r.cause, r.fix);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// `await` is legal only inside an async function.
|
|
240
|
+
if (type === "AwaitExpression" && !inAsync) {
|
|
241
|
+
const r = FORBIDDEN_NODES.AwaitExpression;
|
|
242
|
+
if (r !== undefined)
|
|
243
|
+
v.fail(r.code, node, r.cause, r.fix);
|
|
244
|
+
}
|
|
245
|
+
const rule = type === "AwaitExpression" ? undefined : FORBIDDEN_NODES[type];
|
|
246
|
+
if (rule !== undefined) {
|
|
247
|
+
v.fail(rule.code, node, rule.cause, rule.fix);
|
|
248
|
+
}
|
|
249
|
+
if (type === "Program" || type === "BlockStatement")
|
|
250
|
+
checkAsiHazards(node, v);
|
|
251
|
+
if (type === "CallExpression" || (type === "MemberExpression" && node.computed === true)) {
|
|
252
|
+
checkContinuationHazard(node, v);
|
|
253
|
+
}
|
|
254
|
+
if (type === "CallExpression")
|
|
255
|
+
checkAsyncCallPosition(node, parent, v);
|
|
256
|
+
switch (type) {
|
|
257
|
+
case "VariableDeclaration":
|
|
258
|
+
if (node.kind === "var") {
|
|
259
|
+
v.fail("L1003", node, "`var` is function-scoped and hoists, so a name can be read before the line that gives it a value.", "Use `const`, or `let` when the binding is reassigned.");
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
case "FunctionDeclaration":
|
|
263
|
+
case "FunctionExpression":
|
|
264
|
+
case "ArrowFunctionExpression":
|
|
265
|
+
if (node.generator === true) {
|
|
266
|
+
v.fail("L1005", node, "Generators suspend and resume outside the effect journal, so a resumed run could not reproduce them.", "Use a loop, and `await` the effects inside it.");
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
case "Literal":
|
|
270
|
+
if (node.regex !== undefined) {
|
|
271
|
+
v.fail("L1007", node, "There are no regular expressions, so a program cannot spend unbounded time in a match.", "Use `contains`, `startsWith`, `endsWith`, or `split`.");
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
case "IfStatement": {
|
|
275
|
+
for (const branch of ["consequent", "alternate"]) {
|
|
276
|
+
const b = node[branch];
|
|
277
|
+
if (isNode(b) && b.type !== "BlockStatement" && b.type !== "IfStatement") {
|
|
278
|
+
v.fail("L1009", b, "Every branch body is a block, so inserting a second statement can never silently fall outside the branch.", "Wrap the body in braces.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "ForStatement":
|
|
284
|
+
case "ForOfStatement":
|
|
285
|
+
case "WhileStatement": {
|
|
286
|
+
const body = node.body;
|
|
287
|
+
if (isNode(body) && body.type !== "BlockStatement") {
|
|
288
|
+
v.fail("L1009", body, "Every loop body is a block.", "Wrap the body in braces.");
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case "SwitchCase": {
|
|
293
|
+
const consequent = node.consequent;
|
|
294
|
+
if (Array.isArray(consequent) && consequent.length > 0) {
|
|
295
|
+
const last = consequent[consequent.length - 1];
|
|
296
|
+
const terminators = ["ReturnStatement", "BreakStatement", "ContinueStatement", "ThrowStatement"];
|
|
297
|
+
if (isNode(last) && !terminators.includes(last.type)) {
|
|
298
|
+
v.fail("L1010", node, "A case that falls through to the next one is nearly always a missing `break`.", "End the case with `return`, `break`, `continue`, or `throw`.");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case "Property":
|
|
304
|
+
if (node.computed === true) {
|
|
305
|
+
v.fail("L1011", node, "A computed key means the record's shape is not visible in the source, so neither the validator nor the flowchart can read it.", "Use a literal key, or build the record with `merge`.");
|
|
306
|
+
}
|
|
307
|
+
if (node.kind === "get" || node.kind === "set") {
|
|
308
|
+
v.fail("L1015", node, "An accessor runs code when a property is read, which hides an effect behind what looks like data.", "Store the value, or call a function explicitly.");
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
case "ArrayExpression":
|
|
312
|
+
if (Array.isArray(node.elements) && node.elements.some((e) => e === null)) {
|
|
313
|
+
v.fail("L1012", node, "An elided slot is neither absent nor a value, and it does not survive canonicalization.", "Write the value explicitly, or use `null`.");
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
case "BinaryExpression":
|
|
317
|
+
if (node.operator === "instanceof") {
|
|
318
|
+
v.fail("L1016", node, "There are no classes or prototypes, so `instanceof` can only probe host objects.", "Compare a field instead, for example `value.status === \"done\"`.");
|
|
319
|
+
}
|
|
320
|
+
if (node.operator === "in") {
|
|
321
|
+
v.fail("L1004", node, "The `in` operator reaches inherited names.", "Use `has(record, key)`.");
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
case "UnaryExpression":
|
|
325
|
+
if (node.operator === "delete") {
|
|
326
|
+
v.fail("L1021", node, "Records that cross an effect boundary are frozen, and deleting from a live one makes its shape depend on control flow.", "Build a new record with the fields you want.");
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
default:
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
const nowAsync = type === "FunctionDeclaration" || type === "FunctionExpression" || type === "ArrowFunctionExpression"
|
|
333
|
+
? node.async === true
|
|
334
|
+
: inAsync;
|
|
335
|
+
for (const child of children(node))
|
|
336
|
+
walkShape(child, v, nowAsync, node);
|
|
337
|
+
}
|
|
338
|
+
// ---- walk 2: resolution and effect call shape --------------------------------------------
|
|
339
|
+
class Scope {
|
|
340
|
+
parent;
|
|
341
|
+
names = new Map();
|
|
342
|
+
constructor(parent) {
|
|
343
|
+
this.parent = parent;
|
|
344
|
+
}
|
|
345
|
+
declare(name, kind) {
|
|
346
|
+
this.names.set(name, kind);
|
|
347
|
+
}
|
|
348
|
+
lookup(name) {
|
|
349
|
+
for (let s = this; s !== null; s = s.parent) {
|
|
350
|
+
const k = s.names.get(name);
|
|
351
|
+
if (k !== undefined)
|
|
352
|
+
return k;
|
|
353
|
+
}
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/** Collect the identifiers a binding pattern introduces. */
|
|
358
|
+
function patternNames(node, out) {
|
|
359
|
+
switch (node.type) {
|
|
360
|
+
case "Identifier":
|
|
361
|
+
out.push(node.name);
|
|
362
|
+
break;
|
|
363
|
+
case "ObjectPattern":
|
|
364
|
+
for (const p of node.properties) {
|
|
365
|
+
if (p.type === "RestElement")
|
|
366
|
+
patternNames(p.argument, out);
|
|
367
|
+
else
|
|
368
|
+
patternNames(p.value, out);
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
case "ArrayPattern":
|
|
372
|
+
for (const el of node.elements)
|
|
373
|
+
if (el !== null)
|
|
374
|
+
patternNames(el, out);
|
|
375
|
+
break;
|
|
376
|
+
case "AssignmentPattern":
|
|
377
|
+
patternNames(node.left, out);
|
|
378
|
+
break;
|
|
379
|
+
case "RestElement":
|
|
380
|
+
patternNames(node.argument, out);
|
|
381
|
+
break;
|
|
382
|
+
default:
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* `notify`'s fact is a bounded decision record, not a message.
|
|
388
|
+
*
|
|
389
|
+
* This is the one place a program can push its own bytes toward another agent's context, so the
|
|
390
|
+
* bound is what keeps "conversation is the data plane, the program is the control plane" true at
|
|
391
|
+
* the one boundary where it is easiest to break. A literal fact is checked exactly; a computed
|
|
392
|
+
* one is checked at the effect boundary by the same rules.
|
|
393
|
+
*/
|
|
394
|
+
/**
|
|
395
|
+
* `to` addresses the escalated mint and nothing else, so accepting it elsewhere records an input
|
|
396
|
+
* that decides nothing, which the hash table then has to classify for no reason.
|
|
397
|
+
*/
|
|
398
|
+
function checkEscalateTo(bag, v) {
|
|
399
|
+
if (bag === undefined || bag.type !== "ObjectExpression")
|
|
400
|
+
return;
|
|
401
|
+
const prop = (want) => (bag.properties ?? []).find((p) => {
|
|
402
|
+
const k = p.key;
|
|
403
|
+
return k !== undefined && (k.type === "Identifier" ? k.name === want : k.value === want);
|
|
404
|
+
});
|
|
405
|
+
const to = prop("to");
|
|
406
|
+
if (to === undefined)
|
|
407
|
+
return;
|
|
408
|
+
const onExpiry = prop("onExpiry");
|
|
409
|
+
const value = onExpiry?.value;
|
|
410
|
+
if (value?.type === "Literal" && value.value === "escalate")
|
|
411
|
+
return;
|
|
412
|
+
v.fail("L3044", to, "`to` only addresses an escalated checkpoint, and this one does not escalate.", 'Set `onExpiry: "escalate"`, or drop `to`.', "checkpoint");
|
|
413
|
+
}
|
|
414
|
+
function checkNotifyFact(fact, v) {
|
|
415
|
+
if (fact === undefined || fact.type !== "ObjectExpression")
|
|
416
|
+
return; // computed: checked at run time
|
|
417
|
+
const seen = new Map();
|
|
418
|
+
for (const p of fact.properties ?? []) {
|
|
419
|
+
if (p.type !== "Property")
|
|
420
|
+
continue;
|
|
421
|
+
const key = p.key;
|
|
422
|
+
const keyName = key.type === "Identifier"
|
|
423
|
+
? key.name
|
|
424
|
+
: key.type === "Literal" && typeof key.value === "string"
|
|
425
|
+
? key.value
|
|
426
|
+
: null;
|
|
427
|
+
if (keyName !== null)
|
|
428
|
+
seen.set(keyName, p);
|
|
429
|
+
}
|
|
430
|
+
for (const [key, prop] of seen) {
|
|
431
|
+
if (key !== "decision" && key !== "outcome" && key !== "detail") {
|
|
432
|
+
v.fail("L3043", prop, `A notify fact carries \`decision\`, \`outcome\`, and an optional \`detail\`, and nothing else. \`${key}\` would be an unbounded channel from the program into another agent's context.`, "Move the information into `detail` as a short scalar, or leave it for the agent to read from the channel.", "notify");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const field of ["decision", "outcome"]) {
|
|
436
|
+
const prop = seen.get(field);
|
|
437
|
+
if (prop === undefined) {
|
|
438
|
+
v.fail("L3043", fact, `A notify fact needs a \`${field}\`.`, 'Name the decision and its outcome as tokens: { decision: "build", outcome: "blocked" }', "notify");
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const value = prop.value;
|
|
442
|
+
if (value.type !== "Literal" || typeof value.value !== "string")
|
|
443
|
+
continue; // computed
|
|
444
|
+
if (!NOTIFY_BOUND.tokenRe.test(value.value)) {
|
|
445
|
+
v.fail("L3043", value, `\`${field}\` names a decision, so it is a token rather than prose. "${value.value}" is not one.`, 'Use kebab-case, 1 to 64 characters: { decision: "approve-plan", outcome: "auto-proceeded" }', "notify");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const detail = seen.get("detail");
|
|
449
|
+
if (detail === undefined)
|
|
450
|
+
return;
|
|
451
|
+
const value = detail.value;
|
|
452
|
+
if (value.type !== "ObjectExpression") {
|
|
453
|
+
v.fail("L3043", value, "`detail` is a record of short scalars.", "Use `{ attempts: 3 }` rather than a value of another shape.", "notify");
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const props = value.properties ?? [];
|
|
457
|
+
if (props.length > NOTIFY_BOUND.maxDetailKeys) {
|
|
458
|
+
v.fail("L3043", value, `\`detail\` carries at most ${NOTIFY_BOUND.maxDetailKeys} keys; this one has ${props.length}. The cap is what keeps a notice a decision rather than a message.`, "Keep the fields that name the decision and drop the rest.", "notify");
|
|
459
|
+
}
|
|
460
|
+
for (const p of props) {
|
|
461
|
+
if (p.type !== "Property")
|
|
462
|
+
continue;
|
|
463
|
+
const key = p.key;
|
|
464
|
+
const keyName = key.type === "Identifier"
|
|
465
|
+
? key.name
|
|
466
|
+
: key.type === "Literal" && typeof key.value === "string"
|
|
467
|
+
? key.value
|
|
468
|
+
: null;
|
|
469
|
+
if (keyName !== null && !NOTIFY_BOUND.detailKeyRe.test(keyName)) {
|
|
470
|
+
v.fail("L3043", key, `\`${keyName}\` is not a detail key; they are kebab-case tokens of at most 32 characters.`, "Rename it, for example `attempt-count`.", "notify");
|
|
471
|
+
}
|
|
472
|
+
const dv = p.value;
|
|
473
|
+
if (dv.type === "ObjectExpression" || dv.type === "ArrayExpression") {
|
|
474
|
+
v.fail("L3043", dv, "Detail values are scalars: a short string, a number, or a boolean. A nested structure is an unbounded pipe into another agent's context.", "Flatten it, or leave it for the agent to read from the channel.", "notify");
|
|
475
|
+
}
|
|
476
|
+
else if (dv.type === "Literal" &&
|
|
477
|
+
typeof dv.value === "string" &&
|
|
478
|
+
dv.value.length > NOTIFY_BOUND.maxDetailStringLength) {
|
|
479
|
+
v.fail("L3043", dv, `A detail string is at most ${NOTIFY_BOUND.maxDetailStringLength} characters; this one is ${dv.value.length}. Longer than that is prose, and prose belongs in the channel where the agent can answer it.`, "Shorten it to a label, or put the content on the run record.", "notify");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/** True when every element of an array literal is a record literal with a string `id`. */
|
|
484
|
+
function arrayItemsCarryId(items) {
|
|
485
|
+
const els = items.elements ?? [];
|
|
486
|
+
if (els.length === 0)
|
|
487
|
+
return false;
|
|
488
|
+
return els.every((el) => {
|
|
489
|
+
if (el === null || el === undefined || el.type !== "ObjectExpression")
|
|
490
|
+
return false;
|
|
491
|
+
return (el.properties ?? []).some((p) => {
|
|
492
|
+
if (p.type !== "Property")
|
|
493
|
+
return false;
|
|
494
|
+
const key = p.key;
|
|
495
|
+
const name = key.type === "Identifier" ? key.name : key.value;
|
|
496
|
+
const value = p.value;
|
|
497
|
+
return name === "id" && value.type === "Literal" && typeof value.value === "string";
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* L2013: an async call must be immediately awaited, immediately returned, or be the thunk a
|
|
503
|
+
* combinator owns.
|
|
504
|
+
*
|
|
505
|
+
* Banning `Promise` is not enough, because calling an async function is itself a way to start
|
|
506
|
+
* work. `const pa = work(a); const pb = work(b);` reads as two concurrent chains and never
|
|
507
|
+
* mentions a combinator. The defect is not the race a reviewer predicted: executed, those calls
|
|
508
|
+
* run strictly sequentially because the walker awaits every call site. It is the mirror image,
|
|
509
|
+
* and for an author who is a language model it is worse. The program says "concurrently" and the
|
|
510
|
+
* runtime silently runs them one after the other, and nothing says so.
|
|
511
|
+
*/
|
|
512
|
+
function checkAsyncCallPosition(node, parent, v) {
|
|
513
|
+
const callee = node.callee;
|
|
514
|
+
if (!isNode(callee) || callee.type !== "Identifier")
|
|
515
|
+
return;
|
|
516
|
+
const name = callee.name;
|
|
517
|
+
// Primitives are always effects; a user function is only interesting if it was declared async,
|
|
518
|
+
// which the resolution walk cannot know here, so both are treated the same way: the POSITION
|
|
519
|
+
// is what is checked, not the callee's nature.
|
|
520
|
+
const isEffect = PRIMITIVES[name] !== undefined;
|
|
521
|
+
if (!isEffect)
|
|
522
|
+
return;
|
|
523
|
+
if (parent === null)
|
|
524
|
+
return;
|
|
525
|
+
// Only two positions are legal: awaited, or the concise body of an arrow that a combinator
|
|
526
|
+
// owns as a thunk. Everything else, including a bare statement, starts work nothing waits for.
|
|
527
|
+
const ok = parent.type === "AwaitExpression" ||
|
|
528
|
+
parent.type === "ArrowFunctionExpression" ||
|
|
529
|
+
parent.type === "ReturnStatement";
|
|
530
|
+
if (ok)
|
|
531
|
+
return;
|
|
532
|
+
v.fail("L2013", node, `This \`${name}\` is not awaited, so it starts work whose result nothing waits for. Read literally the program says one thing and the runtime does another: calls outside a combinator run in sequence, not concurrently.`, `Await it (\`await ${name}(...)\`), return it, or make it a branch of \`parallel\`, \`race\` or \`fanOut\`.`, name);
|
|
533
|
+
}
|
|
534
|
+
function checkCall(node, v) {
|
|
535
|
+
const callee = node.callee;
|
|
536
|
+
if (!isNode(callee) || callee.type !== "Identifier")
|
|
537
|
+
return;
|
|
538
|
+
const name = callee.name;
|
|
539
|
+
const spec = PRIMITIVES[name];
|
|
540
|
+
if (spec === undefined)
|
|
541
|
+
return;
|
|
542
|
+
const args = node.arguments ?? [];
|
|
543
|
+
// `checkpoint` takes its name positionally; every other primitive takes it in the option bag.
|
|
544
|
+
if (name === "checkpoint") {
|
|
545
|
+
const first = args[0];
|
|
546
|
+
if (first === undefined || first.type !== "Literal" || typeof first.value !== "string") {
|
|
547
|
+
v.fail("L3013", first ?? node, "A checkpoint's name must be a string literal, because the flowchart, the linter, and the migration report all read it without running the program.", 'Pass a literal: checkpoint("approve-plan", "Approve the plan?", { timeout: "10m" })', name);
|
|
548
|
+
}
|
|
549
|
+
else if (!STEP_NAME_RE.test(first.value)) {
|
|
550
|
+
v.fail("L3014", first, `"${first.value}" is not a well-formed step name.`, "Use kebab-case, 1 to 64 characters: \"approve-plan\".", name);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// The option bag sits at a FIXED index per primitive. It is deliberately not "the last record
|
|
554
|
+
// argument": `notify(agents, fact, opts)` and `checkpoint(name, prompt, opts)` both take a
|
|
555
|
+
// record in an earlier position, and reading that as options would reject correct data.
|
|
556
|
+
const bag = args[spec.optionsAt];
|
|
557
|
+
const bagIsRecord = bag !== undefined && bag.type === "ObjectExpression";
|
|
558
|
+
const given = new Map();
|
|
559
|
+
if (bagIsRecord) {
|
|
560
|
+
for (const p of bag.properties ?? []) {
|
|
561
|
+
if (p.type !== "Property")
|
|
562
|
+
continue;
|
|
563
|
+
const key = p.key;
|
|
564
|
+
if (key.type === "Identifier")
|
|
565
|
+
given.set(key.name, p);
|
|
566
|
+
else if (key.type === "Literal" && typeof key.value === "string")
|
|
567
|
+
given.set(key.value, p);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// Closed bags: an unknown key answers with the full signature rather than being ignored.
|
|
571
|
+
for (const [key, prop] of given) {
|
|
572
|
+
if (!spec.options.includes(key)) {
|
|
573
|
+
v.fail("L3011", prop, `\`${name}\` has no option named \`${key}\`, and option bags are closed so a typo cannot be silently dropped.`, `Accepted keys: ${spec.options.join(", ")}.`, name);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// Required step names.
|
|
577
|
+
if (spec.nameRequired && name !== "checkpoint") {
|
|
578
|
+
const prop = given.get("name");
|
|
579
|
+
if (prop === undefined) {
|
|
580
|
+
v.fail("L3012", node, `Every \`${name}\` needs a name, because its journal entry is keyed by that name rather than by its position. Without one, a resumed run cannot tell this step from any other.`, `Add a kebab-case name literal: ${name}(..., { name: "..." })`, name);
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
const value = prop.value;
|
|
584
|
+
if (value.type !== "Literal" || typeof value.value !== "string") {
|
|
585
|
+
v.fail("L3013", value, "A step name must be a string literal, because the flowchart, the linter, and the migration report all read it without running the program.", 'Use a literal: { name: "build" }', name);
|
|
586
|
+
}
|
|
587
|
+
else if (!STEP_NAME_RE.test(value.value)) {
|
|
588
|
+
v.fail("L3014", value, `"${value.value}" is not a well-formed step name.`, 'Use kebab-case, 1 to 64 characters: { name: "build" }', name);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (name === "notify")
|
|
593
|
+
checkNotifyFact(args[1], v);
|
|
594
|
+
if (name === "checkpoint")
|
|
595
|
+
checkEscalateTo(bag, v);
|
|
596
|
+
// fanOut needs a stable branch key, or items that carry one. Warn only when the source SHOWS
|
|
597
|
+
// there are no ids: items carrying a string `id` supply the key by design, so warning on those
|
|
598
|
+
// would flag correct code, and a computed list cannot be judged from here at all. The runtime
|
|
599
|
+
// refuses the genuinely unkeyable case, which is where an unknown list gets decided.
|
|
600
|
+
if (name === "fanOut" && !given.has("key")) {
|
|
601
|
+
const items = args[0];
|
|
602
|
+
if (items !== undefined && items.type === "ArrayExpression" && !arrayItemsCarryId(items)) {
|
|
603
|
+
v.warn("L3021", node, "These items carry no `id`, so this fan-out has no stable branch key, and a reordered or filtered list would silently reshuffle every journal key underneath it.", 'Pass a key function: fanOut(items, fn, { name: "reviews", key: (i) => i.id })', name);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// Array-form concurrency branches are keyed by index (design doc 7.2). Legal, linted.
|
|
607
|
+
if ((name === "parallel" || name === "race") && args.length > 0) {
|
|
608
|
+
const branches = args[0];
|
|
609
|
+
if (branches !== undefined && branches.type === "ArrayExpression") {
|
|
610
|
+
v.warn("L3023", branches, "Array branches are keyed by index, so inserting a branch shifts every later branch's journal namespace and re-runs its steps.", `Use the record form: ${name}({ lint: () => ..., tests: () => ... }, { name: "checks" })`, name);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function walkResolve(node, v, scope) {
|
|
615
|
+
switch (node.type) {
|
|
616
|
+
case "Identifier": {
|
|
617
|
+
const name = node.name;
|
|
618
|
+
if (scope.lookup(name) !== undefined)
|
|
619
|
+
return;
|
|
620
|
+
if (RESERVED_NAMES.has(name))
|
|
621
|
+
return;
|
|
622
|
+
if (PROMISE_NAMES.has(name)) {
|
|
623
|
+
v.fail("L2011", node, "Promises are not in the language, so concurrency is always visible in the source and therefore in the journal and the flowchart.", "Use `parallel`, `race`, or `fanOut`.");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (FORBIDDEN_GLOBALS.has(name)) {
|
|
627
|
+
v.fail("L2012", node, `\`${name}\` is a host global. There is no ambient IO, clock, or randomness here: the interpreter has nothing nondeterministic to offer.`, "Use `now()` for time, `random()` for randomness, `sleep()` to wait, and `log()` for output.");
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
v.fail("L2001", node, `\`${name}\` is not defined anywhere in this program. Every name resolves when the program is read, so this is never a runtime surprise.`, `Define it, or check the spelling. The builtins are: ${BUILTINS.join(", ")}.`);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
case "VariableDeclaration": {
|
|
634
|
+
const kind = node.kind === "const" ? "const" : "let";
|
|
635
|
+
for (const d of node.declarations ?? []) {
|
|
636
|
+
const init = d.init;
|
|
637
|
+
if (isNode(init))
|
|
638
|
+
walkResolve(init, v, scope);
|
|
639
|
+
const names = [];
|
|
640
|
+
patternNames(d.id, names);
|
|
641
|
+
for (const n of names) {
|
|
642
|
+
if (RESERVED_NAMES.has(n)) {
|
|
643
|
+
v.fail("L2002", d.id, `\`${n}\` is a builtin, so shadowing it would make a call to \`${n}\` mean two different things in one program.`, `Rename the binding, for example \`${n}Result\`.`);
|
|
644
|
+
}
|
|
645
|
+
scope.declare(n, kind);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
case "AssignmentExpression": {
|
|
651
|
+
const left = node.left;
|
|
652
|
+
if (left.type === "Identifier" && scope.lookup(left.name) === "const") {
|
|
653
|
+
v.fail("L2003", left, `\`${left.name}\` is declared \`const\`.`, "Declare it with `let` if it is meant to be reassigned.");
|
|
654
|
+
}
|
|
655
|
+
for (const c of children(node))
|
|
656
|
+
walkResolve(c, v, scope);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
case "MemberExpression": {
|
|
660
|
+
// Only the object side is a name; a non-computed property is a field, not a binding.
|
|
661
|
+
const object = node.object;
|
|
662
|
+
if (isNode(object))
|
|
663
|
+
walkResolve(object, v, scope);
|
|
664
|
+
if (node.computed === true && isNode(node.property))
|
|
665
|
+
walkResolve(node.property, v, scope);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
case "Property": {
|
|
669
|
+
// Shorthand `{ x }` reads `x`; a non-computed key is a field name, not a binding.
|
|
670
|
+
if (node.computed === true && isNode(node.key))
|
|
671
|
+
walkResolve(node.key, v, scope);
|
|
672
|
+
if (isNode(node.value))
|
|
673
|
+
walkResolve(node.value, v, scope);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
case "FunctionDeclaration":
|
|
677
|
+
case "FunctionExpression":
|
|
678
|
+
case "ArrowFunctionExpression": {
|
|
679
|
+
if (node.type === "FunctionDeclaration" && isNode(node.id)) {
|
|
680
|
+
const fname = node.id.name;
|
|
681
|
+
if (RESERVED_NAMES.has(fname)) {
|
|
682
|
+
v.fail("L2002", node.id, `\`${fname}\` is a builtin, so a function of that name would shadow it.`, "Rename the function.");
|
|
683
|
+
}
|
|
684
|
+
scope.declare(fname, "const");
|
|
685
|
+
}
|
|
686
|
+
const inner = new Scope(scope);
|
|
687
|
+
for (const p of node.params ?? []) {
|
|
688
|
+
const names = [];
|
|
689
|
+
patternNames(p, names);
|
|
690
|
+
for (const n of names) {
|
|
691
|
+
if (RESERVED_NAMES.has(n)) {
|
|
692
|
+
v.fail("L2002", p, `\`${n}\` is a builtin, so a parameter of that name would shadow it inside this function.`, "Rename the parameter.");
|
|
693
|
+
}
|
|
694
|
+
inner.declare(n, "param");
|
|
695
|
+
}
|
|
696
|
+
// Default values are evaluated in the inner scope.
|
|
697
|
+
if (p.type === "AssignmentPattern" && isNode(p.right))
|
|
698
|
+
walkResolve(p.right, v, inner);
|
|
699
|
+
}
|
|
700
|
+
if (isNode(node.body))
|
|
701
|
+
walkResolve(node.body, v, inner);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
case "BlockStatement": {
|
|
705
|
+
const inner = new Scope(scope);
|
|
706
|
+
hoistFunctions(node, inner);
|
|
707
|
+
for (const s of node.body ?? [])
|
|
708
|
+
walkResolve(s, v, inner);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
case "CatchClause": {
|
|
712
|
+
const inner = new Scope(scope);
|
|
713
|
+
if (isNode(node.param)) {
|
|
714
|
+
const names = [];
|
|
715
|
+
patternNames(node.param, names);
|
|
716
|
+
for (const n of names)
|
|
717
|
+
inner.declare(n, "const");
|
|
718
|
+
}
|
|
719
|
+
if (isNode(node.body))
|
|
720
|
+
walkResolve(node.body, v, inner);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
case "ForStatement":
|
|
724
|
+
case "ForOfStatement": {
|
|
725
|
+
const inner = new Scope(scope);
|
|
726
|
+
for (const c of children(node))
|
|
727
|
+
walkResolve(c, v, inner);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
case "CallExpression": {
|
|
731
|
+
checkCall(node, v);
|
|
732
|
+
for (const c of children(node))
|
|
733
|
+
walkResolve(c, v, scope);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
default: {
|
|
737
|
+
for (const c of children(node))
|
|
738
|
+
walkResolve(c, v, scope);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/** Function declarations are visible to the whole block, so bind them before walking it. */
|
|
744
|
+
function hoistFunctions(block, scope) {
|
|
745
|
+
for (const s of block.body ?? []) {
|
|
746
|
+
if (s.type === "FunctionDeclaration" && isNode(s.id)) {
|
|
747
|
+
scope.declare(s.id.name, "const");
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// ---- entry point ---------------------------------------------------------------------------
|
|
752
|
+
/**
|
|
753
|
+
* Parse and validate a program. Throws {@link LangErrors} carrying every problem found, so an
|
|
754
|
+
* author repairs the whole list in one pass. Returns the AST plus the lints that did not fail
|
|
755
|
+
* the program.
|
|
756
|
+
*/
|
|
757
|
+
export function validate(source, file = "program.cotal.js") {
|
|
758
|
+
const v = new Validator(source, file);
|
|
759
|
+
let ast;
|
|
760
|
+
try {
|
|
761
|
+
ast = parse(source, ACORN_OPTIONS);
|
|
762
|
+
}
|
|
763
|
+
catch (e) {
|
|
764
|
+
const err = e;
|
|
765
|
+
const span = {
|
|
766
|
+
file,
|
|
767
|
+
line: err.loc?.line ?? 1,
|
|
768
|
+
column: (err.loc?.column ?? 0) + 1,
|
|
769
|
+
};
|
|
770
|
+
const message = err.message ?? "could not be parsed";
|
|
771
|
+
const mapped = PARSE_ERROR_MAP.find((m) => m.test.test(message));
|
|
772
|
+
throw new LangErrors([
|
|
773
|
+
new LangError(mapped !== undefined
|
|
774
|
+
? { code: mapped.code, span, cause: mapped.cause, fix: mapped.fix }
|
|
775
|
+
: {
|
|
776
|
+
code: "L1008",
|
|
777
|
+
span,
|
|
778
|
+
cause: `This program is not valid JavaScript, so none of the language rules could be checked: ${message}.`,
|
|
779
|
+
fix: "Fix the syntax at the marked position. Statements terminate explicitly here; there is no automatic semicolon insertion.",
|
|
780
|
+
}),
|
|
781
|
+
], source);
|
|
782
|
+
}
|
|
783
|
+
// The module body is an async context: a program's top level is where the workflow lives.
|
|
784
|
+
walkShape(ast, v, true);
|
|
785
|
+
const top = new Scope(null);
|
|
786
|
+
hoistFunctions(ast, top);
|
|
787
|
+
for (const s of ast.body ?? [])
|
|
788
|
+
walkResolve(s, v, top);
|
|
789
|
+
if (v.errors.length > 0)
|
|
790
|
+
throw new LangErrors(v.errors, source);
|
|
791
|
+
return { ast, warnings: v.warnings };
|
|
792
|
+
}
|
|
793
|
+
//# sourceMappingURL=grammar.js.map
|