@cotal-ai/lang 0.23.0 → 0.24.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/README.md +57 -0
- package/dist/effects.d.ts +19 -0
- package/dist/effects.d.ts.map +1 -1
- package/dist/effects.js +23 -0
- package/dist/effects.js.map +1 -1
- package/dist/errors.d.ts +48 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +90 -2
- package/dist/errors.js.map +1 -1
- package/dist/grammar.d.ts.map +1 -1
- package/dist/grammar.js +561 -128
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/interpret.d.ts +81 -3
- package/dist/interpret.d.ts.map +1 -1
- package/dist/interpret.js +1446 -270
- package/dist/interpret.js.map +1 -1
- package/dist/journal.d.ts +218 -9
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +232 -12
- package/dist/journal.js.map +1 -1
- package/dist/keys.d.ts +39 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +61 -0
- package/dist/keys.js.map +1 -1
- package/dist/library.d.ts +65 -0
- package/dist/library.d.ts.map +1 -0
- package/dist/library.js +525 -0
- package/dist/library.js.map +1 -0
- package/dist/notify-fact.d.ts +8 -0
- package/dist/notify-fact.d.ts.map +1 -0
- package/dist/notify-fact.js +69 -0
- package/dist/notify-fact.js.map +1 -0
- package/dist/pins.d.ts +82 -0
- package/dist/pins.d.ts.map +1 -0
- package/dist/pins.js +87 -0
- package/dist/pins.js.map +1 -0
- package/dist/primitives.d.ts +14 -0
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +45 -5
- package/dist/primitives.js.map +1 -1
- package/dist/sim.d.ts +9 -1
- package/dist/sim.d.ts.map +1 -1
- package/dist/sim.js +9 -1
- package/dist/sim.js.map +1 -1
- package/dist/syntax.d.ts +34 -0
- package/dist/syntax.d.ts.map +1 -0
- package/dist/syntax.js +178 -0
- package/dist/syntax.js.map +1 -0
- package/dist/values.d.ts +20 -1
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +0 -0
- package/dist/values.js.map +1 -1
- package/package.json +2 -2
package/dist/grammar.js
CHANGED
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { parse } from "acorn";
|
|
14
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";
|
|
15
|
+
import { BUILTINS, FORBIDDEN_GLOBALS, HOST_GLOBAL_HINTS, NOTIFY_BOUND, PRIMITIVES, PROMISE_NAMES, RESERVED_NAMES, STEP_NAME_RE, primitiveDoc, } from "./primitives.js";
|
|
16
|
+
import { KEY_RESERVED_RE } from "./keys.js";
|
|
17
|
+
import { ADMITTED_NODES, FORBIDDEN_NODES, STRUCTURAL_NODES } from "./syntax.js";
|
|
18
|
+
import { MUTATING_METHODS } from "./library.js";
|
|
16
19
|
const ACORN_OPTIONS = {
|
|
17
20
|
ecmaVersion: 2023,
|
|
18
21
|
sourceType: "module",
|
|
@@ -27,6 +30,24 @@ class Validator {
|
|
|
27
30
|
file;
|
|
28
31
|
errors = [];
|
|
29
32
|
warnings = [];
|
|
33
|
+
/**
|
|
34
|
+
* Named functions, by name, for L2032's reach.
|
|
35
|
+
*
|
|
36
|
+
* A branch does not have to be written at the combinator call — `parallel({ a, b })` names two
|
|
37
|
+
* functions declared elsewhere, and an inline branch can call a helper that writes the outer
|
|
38
|
+
* binding on its behalf. Both are the same defect and neither is visible from the call site
|
|
39
|
+
* alone, so the names are resolved here. A name bound to two different functions maps to `null`:
|
|
40
|
+
* it cannot be resolved, and guessing which one a branch meant is worse than saying so.
|
|
41
|
+
*/
|
|
42
|
+
functions = new Map();
|
|
43
|
+
/**
|
|
44
|
+
* The parent of every call, recorded by the shape walk for the resolution walk.
|
|
45
|
+
*
|
|
46
|
+
* L2013 is a rule about POSITION (awaited, returned, or a combinator's thunk) and, for a user
|
|
47
|
+
* function, about the callee's declaration (async or not). The shape walk sees the position and
|
|
48
|
+
* the resolution walk sees the declaration, so the position is carried across.
|
|
49
|
+
*/
|
|
50
|
+
parents = new WeakMap();
|
|
30
51
|
constructor(source, file) {
|
|
31
52
|
this.source = source;
|
|
32
53
|
this.file = file;
|
|
@@ -53,88 +74,16 @@ class Validator {
|
|
|
53
74
|
}
|
|
54
75
|
}
|
|
55
76
|
// ---- walk 1: shape ------------------------------------------------------------------------
|
|
56
|
-
/**
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
},
|
|
77
|
+
/** The two conditional refusals the table cannot carry: a LABELLED jump and an `await` outside async. */
|
|
78
|
+
const LABELLED_JUMP = Object.freeze({
|
|
79
|
+
code: "L1017",
|
|
80
|
+
cause: "A labelled break or continue is an arbitrary jump.",
|
|
81
|
+
fix: "Restructure with a helper function or a boolean flag.",
|
|
82
|
+
});
|
|
83
|
+
const AWAIT_OUTSIDE_ASYNC = Object.freeze({
|
|
84
|
+
code: "L1023",
|
|
85
|
+
cause: "This `await` sits inside a function that is not `async`. Every effect is awaited, so a function that performs one is async.",
|
|
86
|
+
fix: "Mark the enclosing function `async`: `async function name(...) { ... }`.",
|
|
138
87
|
});
|
|
139
88
|
/**
|
|
140
89
|
* Automatic semicolon insertion is ALLOWED, against Jessie, and this is a declared deviation.
|
|
@@ -196,6 +145,13 @@ const PARSE_ERROR_MAP = [
|
|
|
196
145
|
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
146
|
fix: "Publish the result onto the run record, or use `log(...)` if you only wanted it in the trace.",
|
|
198
147
|
},
|
|
148
|
+
{
|
|
149
|
+
// A program is a module, so it is strict, and acorn refuses `with` before any walk sees it.
|
|
150
|
+
test: /'with' in strict mode/i,
|
|
151
|
+
code: "L1013",
|
|
152
|
+
cause: "`with` makes name resolution dynamic, and every name here resolves at parse time.",
|
|
153
|
+
fix: "Reference the record's fields directly.",
|
|
154
|
+
},
|
|
199
155
|
{
|
|
200
156
|
test: /keyword 'await' outside an async function|await is only valid in async/i,
|
|
201
157
|
code: "L1023",
|
|
@@ -225,34 +181,83 @@ function children(node) {
|
|
|
225
181
|
function isNode(v) {
|
|
226
182
|
return v !== null && typeof v === "object" && typeof v.type === "string";
|
|
227
183
|
}
|
|
228
|
-
|
|
184
|
+
/** True when a statement always leaves its `switch` case: a terminator, or a block/if made of them. */
|
|
185
|
+
function terminates(stmt) {
|
|
186
|
+
switch (stmt.type) {
|
|
187
|
+
case "ReturnStatement":
|
|
188
|
+
case "BreakStatement":
|
|
189
|
+
case "ContinueStatement":
|
|
190
|
+
case "ThrowStatement":
|
|
191
|
+
return true;
|
|
192
|
+
case "BlockStatement": {
|
|
193
|
+
const body = stmt.body ?? [];
|
|
194
|
+
const last = body[body.length - 1];
|
|
195
|
+
return last !== undefined && terminates(last);
|
|
196
|
+
}
|
|
197
|
+
case "IfStatement":
|
|
198
|
+
return (isNode(stmt.consequent) &&
|
|
199
|
+
terminates(stmt.consequent) &&
|
|
200
|
+
isNode(stmt.alternate) &&
|
|
201
|
+
terminates(stmt.alternate));
|
|
202
|
+
default:
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** The static name of a member access: `x.a` and `x["a"]` both name `a`; `x[k]` names nothing. */
|
|
207
|
+
function memberName(member) {
|
|
208
|
+
const property = member.property;
|
|
209
|
+
if (property === undefined)
|
|
210
|
+
return null;
|
|
211
|
+
if (member.computed !== true)
|
|
212
|
+
return property.type === "Identifier" ? property.name : null;
|
|
213
|
+
return property.type === "Literal" && typeof property.value === "string" ? property.value : null;
|
|
214
|
+
}
|
|
215
|
+
/** The name of a non-computed property key, or null when it is computed. */
|
|
216
|
+
function propertyKeyName(node) {
|
|
217
|
+
if (node.computed === true)
|
|
218
|
+
return null;
|
|
219
|
+
const key = node.key;
|
|
220
|
+
if (key === undefined)
|
|
221
|
+
return null;
|
|
222
|
+
if (key.type === "Identifier")
|
|
223
|
+
return key.name;
|
|
224
|
+
if (key.type === "Literal")
|
|
225
|
+
return String(key.value);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
function walkShape(node, v, inAsync, parent = null,
|
|
229
|
+
/** An ancestor already carried a forbidden row, so this node's own presence needs no second error. */
|
|
230
|
+
underForbidden = false) {
|
|
229
231
|
const type = node.type;
|
|
230
232
|
// Labels and labelled jumps: a bare break/continue is fine, a labelled one is not.
|
|
231
|
-
if (type === "BreakStatement" || type === "ContinueStatement") {
|
|
232
|
-
|
|
233
|
-
const r = FORBIDDEN_NODES[type];
|
|
234
|
-
if (r !== undefined)
|
|
235
|
-
v.fail(r.code, node, r.cause, r.fix);
|
|
236
|
-
}
|
|
233
|
+
if ((type === "BreakStatement" || type === "ContinueStatement") && node.label !== null && node.label !== undefined) {
|
|
234
|
+
v.fail(LABELLED_JUMP.code, node, LABELLED_JUMP.cause, LABELLED_JUMP.fix);
|
|
237
235
|
return;
|
|
238
236
|
}
|
|
239
237
|
// `await` is legal only inside an async function.
|
|
240
238
|
if (type === "AwaitExpression" && !inAsync) {
|
|
241
|
-
|
|
242
|
-
if (r !== undefined)
|
|
243
|
-
v.fail(r.code, node, r.cause, r.fix);
|
|
239
|
+
v.fail(AWAIT_OUTSIDE_ASYNC.code, node, AWAIT_OUTSIDE_ASYNC.cause, AWAIT_OUTSIDE_ASYNC.fix);
|
|
244
240
|
}
|
|
245
|
-
|
|
241
|
+
// THE TABLE. A node is admitted, structural, on a forbidden row, or outside the language.
|
|
242
|
+
const rule = FORBIDDEN_NODES[type];
|
|
243
|
+
let forbidden = underForbidden;
|
|
246
244
|
if (rule !== undefined) {
|
|
247
245
|
v.fail(rule.code, node, rule.cause, rule.fix);
|
|
246
|
+
forbidden = true;
|
|
247
|
+
}
|
|
248
|
+
else if (!ADMITTED_NODES.has(type) && !STRUCTURAL_NODES.has(type) && !underForbidden) {
|
|
249
|
+
v.fail("L1029", node, `\`${type}\` is valid JavaScript but is not in this language, which is a fixed subset of it.`, "Rewrite with the constructs the language has: functions, records, arrays, loops, conditionals, try/catch, and the effect primitives.");
|
|
250
|
+
forbidden = true;
|
|
248
251
|
}
|
|
249
252
|
if (type === "Program" || type === "BlockStatement")
|
|
250
253
|
checkAsiHazards(node, v);
|
|
251
254
|
if (type === "CallExpression" || (type === "MemberExpression" && node.computed === true)) {
|
|
252
255
|
checkContinuationHazard(node, v);
|
|
253
256
|
}
|
|
254
|
-
if (type === "CallExpression")
|
|
257
|
+
if (type === "CallExpression") {
|
|
258
|
+
v.parents.set(node, parent);
|
|
255
259
|
checkAsyncCallPosition(node, parent, v);
|
|
260
|
+
}
|
|
256
261
|
switch (type) {
|
|
257
262
|
case "VariableDeclaration":
|
|
258
263
|
if (node.kind === "var") {
|
|
@@ -270,6 +275,9 @@ function walkShape(node, v, inAsync, parent = null) {
|
|
|
270
275
|
if (node.regex !== undefined) {
|
|
271
276
|
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
277
|
}
|
|
278
|
+
if (node.bigint !== undefined || typeof node.value === "bigint") {
|
|
279
|
+
v.fail("L1030", node, "Numbers here are IEEE doubles with a canonical JSON form; a bigint has neither, so it could not be journalled or cross an effect boundary.", "Use a number, or a string for an identifier that exceeds 2^53.");
|
|
280
|
+
}
|
|
273
281
|
break;
|
|
274
282
|
case "IfStatement": {
|
|
275
283
|
for (const branch of ["consequent", "alternate"]) {
|
|
@@ -293,8 +301,10 @@ function walkShape(node, v, inAsync, parent = null) {
|
|
|
293
301
|
const consequent = node.consequent;
|
|
294
302
|
if (Array.isArray(consequent) && consequent.length > 0) {
|
|
295
303
|
const last = consequent[consequent.length - 1];
|
|
296
|
-
|
|
297
|
-
|
|
304
|
+
// The check looks THROUGH a braced case body: the language asks for blocks everywhere
|
|
305
|
+
// else, so `case 1: { ...; break; }` is the shape it invites, and refusing it read as a
|
|
306
|
+
// rule against the block rather than against the fall-through.
|
|
307
|
+
if (isNode(last) && !terminates(last)) {
|
|
298
308
|
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
309
|
}
|
|
300
310
|
}
|
|
@@ -302,7 +312,10 @@ function walkShape(node, v, inAsync, parent = null) {
|
|
|
302
312
|
}
|
|
303
313
|
case "Property":
|
|
304
314
|
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`.");
|
|
315
|
+
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`, or write it as `record[key] = value`.");
|
|
316
|
+
}
|
|
317
|
+
if (parent?.type === "ObjectExpression" && propertyKeyName(node) === "__proto__") {
|
|
318
|
+
v.fail("L1028", node, "`__proto__` names an object's prototype, and there are no prototypes here: a record has exactly the fields written on it.", "Choose another field name.");
|
|
306
319
|
}
|
|
307
320
|
if (node.kind === "get" || node.kind === "set") {
|
|
308
321
|
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.");
|
|
@@ -314,6 +327,9 @@ function walkShape(node, v, inAsync, parent = null) {
|
|
|
314
327
|
}
|
|
315
328
|
break;
|
|
316
329
|
case "BinaryExpression":
|
|
330
|
+
if (node.operator === "==" || node.operator === "!=") {
|
|
331
|
+
v.fail("L1025", node, `\`${node.operator}\` coerces its operands before comparing them, so \`0 == ""\` and \`null == undefined\` are true and a comparison's answer depends on rules nobody wrote down here.`, `Use \`${node.operator === "==" ? "===" : "!=="}\`, and \`?? \` or \`=== null\` when the question is about a missing value.`);
|
|
332
|
+
}
|
|
317
333
|
if (node.operator === "instanceof") {
|
|
318
334
|
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
335
|
}
|
|
@@ -322,6 +338,9 @@ function walkShape(node, v, inAsync, parent = null) {
|
|
|
322
338
|
}
|
|
323
339
|
break;
|
|
324
340
|
case "UnaryExpression":
|
|
341
|
+
if (node.operator === "void") {
|
|
342
|
+
v.fail("L1027", node, "`void` evaluates an expression and discards it, which is only ever used to hide a value or to spell `undefined` obscurely.", "Write `undefined` when you mean it, or drop the expression.");
|
|
343
|
+
}
|
|
325
344
|
if (node.operator === "delete") {
|
|
326
345
|
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
346
|
}
|
|
@@ -333,17 +352,51 @@ function walkShape(node, v, inAsync, parent = null) {
|
|
|
333
352
|
? node.async === true
|
|
334
353
|
: inAsync;
|
|
335
354
|
for (const child of children(node))
|
|
336
|
-
walkShape(child, v, nowAsync, node);
|
|
355
|
+
walkShape(child, v, nowAsync, node, forbidden);
|
|
337
356
|
}
|
|
338
357
|
// ---- walk 2: resolution and effect call shape --------------------------------------------
|
|
339
358
|
class Scope {
|
|
340
359
|
parent;
|
|
341
360
|
names = new Map();
|
|
361
|
+
/** The function node a name is bound to HERE, when it is bound to one at all. */
|
|
362
|
+
fns = new Map();
|
|
363
|
+
/**
|
|
364
|
+
* Names declared LATER in this scope than the point the walk has reached. A `let`/`const` binds
|
|
365
|
+
* the WHOLE block it is in, so a reference above the declaration resolves to it — and, executed,
|
|
366
|
+
* would find a binding that does not hold a value yet (JavaScript's temporal dead zone, a
|
|
367
|
+
* guaranteed runtime ReferenceError in straight-line code). This language refuses it when the
|
|
368
|
+
* program is read (L2004), the way it already refuses an unknown name (L2001) that JavaScript
|
|
369
|
+
* would also only catch at runtime. A reference from inside a NESTED FUNCTION is exempt: the
|
|
370
|
+
* function runs later, when the binding may well be initialized — that is the mutual-recursion
|
|
371
|
+
* shape — so the walk only refuses what straight-line execution is certain to hit.
|
|
372
|
+
*/
|
|
373
|
+
pending = new Set();
|
|
374
|
+
/** True where this scope is a function body: references from inside it to a pending outer name are deferred, not certain. */
|
|
375
|
+
fnBoundary = false;
|
|
342
376
|
constructor(parent) {
|
|
343
377
|
this.parent = parent;
|
|
344
378
|
}
|
|
345
|
-
declare(name, kind) {
|
|
379
|
+
declare(name, kind, fn) {
|
|
346
380
|
this.names.set(name, kind);
|
|
381
|
+
if (fn !== undefined)
|
|
382
|
+
this.fns.set(name, fn);
|
|
383
|
+
else
|
|
384
|
+
this.fns.delete(name);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Is a reference to `name` FROM this scope certain to land in a dead zone? True only when the
|
|
388
|
+
* scope that owns the binding still has it pending and no function boundary lies between the
|
|
389
|
+
* reference and the owner.
|
|
390
|
+
*/
|
|
391
|
+
refersToPending(name) {
|
|
392
|
+
let crossedFn = false;
|
|
393
|
+
for (let s = this; s !== null; s = s.parent) {
|
|
394
|
+
if (s.names.has(name))
|
|
395
|
+
return s.pending.has(name) && !crossedFn;
|
|
396
|
+
if (s.fnBoundary)
|
|
397
|
+
crossedFn = true;
|
|
398
|
+
}
|
|
399
|
+
return false;
|
|
347
400
|
}
|
|
348
401
|
lookup(name) {
|
|
349
402
|
for (let s = this; s !== null; s = s.parent) {
|
|
@@ -353,6 +406,23 @@ class Scope {
|
|
|
353
406
|
}
|
|
354
407
|
return undefined;
|
|
355
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* The function this name is bound to AT THIS POINT IN THE PROGRAM, or nothing.
|
|
411
|
+
*
|
|
412
|
+
* The nearest binding decides, and a binding that is not a function answers `undefined` rather
|
|
413
|
+
* than deferring outward — that is the difference between resolving a name and resolving a
|
|
414
|
+
* BINDING. A program-wide name map cannot tell `parallel({ branch })` inside
|
|
415
|
+
* `function use(branch)` from the top-level `function branch()` of the same name, and blaming a
|
|
416
|
+
* clean program for what an unrelated declaration elsewhere happens to write is worse than
|
|
417
|
+
* leaving one branch unproven: an unproven branch is still refused at runtime by the depth check.
|
|
418
|
+
*/
|
|
419
|
+
lookupFn(name) {
|
|
420
|
+
for (let s = this; s !== null; s = s.parent) {
|
|
421
|
+
if (s.names.has(name))
|
|
422
|
+
return s.fns.get(name);
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
356
426
|
}
|
|
357
427
|
/** Collect the identifiers a binding pattern introduces. */
|
|
358
428
|
function patternNames(node, out) {
|
|
@@ -514,31 +584,44 @@ function checkAsyncCallPosition(node, parent, v) {
|
|
|
514
584
|
if (!isNode(callee) || callee.type !== "Identifier")
|
|
515
585
|
return;
|
|
516
586
|
const name = callee.name;
|
|
517
|
-
// Primitives are always effects
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
if (!isEffect)
|
|
587
|
+
// Primitives are always effects and are checked here, where the position is known. A user
|
|
588
|
+
// function is an effect only if it was declared async, which only the resolution walk can see:
|
|
589
|
+
// {@link checkCall} applies the same rule to those, reading the position back from `v.parents`.
|
|
590
|
+
if (PRIMITIVES[name] === undefined)
|
|
522
591
|
return;
|
|
523
|
-
if (parent === null)
|
|
592
|
+
if (parent === null || asyncCallPositionOk(parent))
|
|
524
593
|
return;
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
594
|
+
v.fail("L2013", node, unawaitedCause(name), unawaitedFix(name), name);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Only two positions are legal for a call that starts an effect: awaited, or the concise body of
|
|
598
|
+
* an arrow that a combinator owns as a thunk (a `return` is the braced spelling of the same thing).
|
|
599
|
+
* Everything else, including a bare statement, starts work nothing waits for.
|
|
600
|
+
*/
|
|
601
|
+
function asyncCallPositionOk(parent) {
|
|
602
|
+
return (parent.type === "AwaitExpression" ||
|
|
528
603
|
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);
|
|
604
|
+
parent.type === "ReturnStatement");
|
|
533
605
|
}
|
|
534
|
-
|
|
606
|
+
const unawaitedCause = (name) => `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.`;
|
|
607
|
+
const unawaitedFix = (name) => `Await it (\`await ${name}(...)\`), return it, or make it a branch of \`parallel\`, \`race\` or \`fanOut\`.`;
|
|
608
|
+
function checkCall(node, v, scope) {
|
|
535
609
|
const callee = node.callee;
|
|
536
610
|
if (!isNode(callee) || callee.type !== "Identifier")
|
|
537
611
|
return;
|
|
538
612
|
const name = callee.name;
|
|
539
613
|
const spec = PRIMITIVES[name];
|
|
540
|
-
if (spec === undefined)
|
|
614
|
+
if (spec === undefined) {
|
|
615
|
+
// L2013's other half: a USER function declared `async` (or a const bound to an async function
|
|
616
|
+
// expression) is an effect the moment it is called, and holding its call in a binding is the
|
|
617
|
+
// rule's own motivating example: `const pa = work(a); const pb = work(b);`.
|
|
618
|
+
const fn = scope.lookupFn(name);
|
|
619
|
+
const parent = v.parents.get(node) ?? null;
|
|
620
|
+
if (fn !== undefined && fn.async === true && parent !== null && !asyncCallPositionOk(parent)) {
|
|
621
|
+
v.fail("L2013", node, unawaitedCause(name), unawaitedFix(name));
|
|
622
|
+
}
|
|
541
623
|
return;
|
|
624
|
+
}
|
|
542
625
|
const args = node.arguments ?? [];
|
|
543
626
|
// `checkpoint` takes its name positionally; every other primitive takes it in the option bag.
|
|
544
627
|
if (name === "checkpoint") {
|
|
@@ -573,16 +656,35 @@ function checkCall(node, v) {
|
|
|
573
656
|
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
657
|
}
|
|
575
658
|
}
|
|
576
|
-
//
|
|
577
|
-
|
|
659
|
+
// Step names: REQUIRED-ness and VALIDITY are separate questions and must not share one gate.
|
|
660
|
+
//
|
|
661
|
+
// Required-ness is a statement about the caller's obligation; validity is a statement about the
|
|
662
|
+
// sink. `nameRequired` is false for 7 of the 13 primitives, so behind one gate a name supplied on
|
|
663
|
+
// any of those reaches `stepKeyString` unchecked: the optional path, which is the one nobody
|
|
664
|
+
// writes tests for, feeds the same durable journal key as the required one. A name containing the
|
|
665
|
+
// characters the key grammar reserves forges structure: two different programs then print one
|
|
666
|
+
// identical key, and with matching inputs the collision is entirely silent.
|
|
667
|
+
if (name !== "checkpoint") {
|
|
578
668
|
const prop = given.get("name");
|
|
579
669
|
if (prop === undefined) {
|
|
580
|
-
|
|
670
|
+
if (spec.nameRequired) {
|
|
671
|
+
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);
|
|
672
|
+
}
|
|
581
673
|
}
|
|
582
674
|
else {
|
|
583
675
|
const value = prop.value;
|
|
584
|
-
|
|
585
|
-
|
|
676
|
+
const isLiteral = value.type === "Literal" && typeof value.value === "string";
|
|
677
|
+
// L3013 stays gated on `nameRequired`. Widening it to every present name refused
|
|
678
|
+
// `fanOut([...], async (lens) => sleep("1m", { name: lens }))` — naming a branch's step after
|
|
679
|
+
// its item, which is idiomatic and is how a fan-out gets distinct keys at all. That is the
|
|
680
|
+
// cost this restriction would have had, and it is too high for what it buys: the SHAPE check
|
|
681
|
+
// below is what closes the forgery, and it does not need the name to be static.
|
|
682
|
+
if (!isLiteral) {
|
|
683
|
+
if (spec.nameRequired) {
|
|
684
|
+
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);
|
|
685
|
+
}
|
|
686
|
+
// A COMPUTED name cannot be checked here at all, and it reaches the same journal key. The
|
|
687
|
+
// refusal for that one lives at key construction, where the value exists — see keys.ts.
|
|
586
688
|
}
|
|
587
689
|
else if (!STEP_NAME_RE.test(value.value)) {
|
|
588
690
|
v.fail("L3014", value, `"${value.value}" is not a well-formed step name.`, 'Use kebab-case, 1 to 64 characters: { name: "build" }', name);
|
|
@@ -610,11 +712,287 @@ function checkCall(node, v) {
|
|
|
610
712
|
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
713
|
}
|
|
612
714
|
}
|
|
715
|
+
// A record-form branch KEY becomes a path segment verbatim: `frameString` builds
|
|
716
|
+
// `/kind:name#occ/b:${branch}` by concatenation and escapes nothing. So a key containing one of
|
|
717
|
+
// the three characters the key grammar reserves forges structure — a single branch named
|
|
718
|
+
// `a/parallel:inner#0/b:b` prints the identical key to a genuinely nested `a` → `inner` → `b`,
|
|
719
|
+
// and the journal cannot tell the two locations apart. With different inputs that surfaces as a
|
|
720
|
+
// spurious L5001 divergence for a program that never diverged; with matching inputs it is
|
|
721
|
+
// SILENT, one durable row serving both, and a replay hands the second location the first's
|
|
722
|
+
// recorded effect.
|
|
723
|
+
//
|
|
724
|
+
// Only the reserved characters are rejected, NOT the full step-name pattern: branch keys are
|
|
725
|
+
// ordinary object keys and `{ runTests: ... }` is legitimate, so requiring kebab-case here would
|
|
726
|
+
// refuse correct programs to fix a forgery that needs `/`, `#` or `:`. The restriction is the
|
|
727
|
+
// narrowest one that closes it.
|
|
728
|
+
if (name === "parallel" || name === "race" || name === "fanOut") {
|
|
729
|
+
const branches = args[0];
|
|
730
|
+
if (branches !== undefined && branches.type === "ObjectExpression") {
|
|
731
|
+
for (const prop of branches.properties ?? []) {
|
|
732
|
+
const key = prop.key;
|
|
733
|
+
if (key === undefined)
|
|
734
|
+
continue;
|
|
735
|
+
const text = key.type === "Identifier" ? key.name
|
|
736
|
+
: key.type === "Literal" && typeof key.value === "string" ? key.value
|
|
737
|
+
: undefined;
|
|
738
|
+
if (text === undefined || !KEY_RESERVED_RE.test(text))
|
|
739
|
+
continue;
|
|
740
|
+
v.fail("L3025", key, `The branch key "${text}" contains a character the step-key grammar reserves (\`/\`, \`#\` or \`:\`). Branch keys are written into the journal key verbatim, so this one can spell a path that a genuinely nested scope also produces — and two different locations that share a key share a durable row, silently when their inputs match.`, "Use a branch key without `/`, `#` or `:`.", name);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// L2032: a branch that WRITES a binding declared outside it.
|
|
745
|
+
//
|
|
746
|
+
// Freeze-on-share does not see this one, because nothing crosses an effect boundary: the branches
|
|
747
|
+
// set a scalar in the enclosing scope. And the damage is silent. Live, the branches write in
|
|
748
|
+
// COMPLETION order; on resume the journalled effects return instantly, so they write in LAUNCH
|
|
749
|
+
// order, the binding ends up holding a different value, and the resumed run takes a path it never
|
|
750
|
+
// recorded — with no divergence raised, because no effect's inputs changed.
|
|
751
|
+
//
|
|
752
|
+
// `conclave` is deliberately not here. Its body is a single thunk with nothing to race, so a
|
|
753
|
+
// write from inside it is as ordered as a write anywhere else in the program.
|
|
754
|
+
if (name === "parallel" || name === "race" || name === "fanOut") {
|
|
755
|
+
// One `seen` set per combinator call: two branches calling the same helper is one defect in
|
|
756
|
+
// that helper, not two, and reporting it twice tells an author to fix one line twice.
|
|
757
|
+
const seen = new Set();
|
|
758
|
+
const thunks = name === "fanOut" ? branchThunks(args[1], v, scope) : branchThunks(args[0], v, scope);
|
|
759
|
+
for (const thunk of thunks)
|
|
760
|
+
checkCapturedWrites(thunk, name, v, seen);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function isFunctionNode(node) {
|
|
764
|
+
return (node.type === "ArrowFunctionExpression" ||
|
|
765
|
+
node.type === "FunctionExpression" ||
|
|
766
|
+
node.type === "FunctionDeclaration");
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Index every function the program names, so L2032 can follow a branch that is not written at the
|
|
770
|
+
* combinator call. Ambiguous names (two functions, one name) index as `null` and resolve to
|
|
771
|
+
* nothing — an unproven branch is left to the interpreter's runtime check rather than guessed at.
|
|
772
|
+
*/
|
|
773
|
+
function indexFunctions(node, v) {
|
|
774
|
+
const put = (name, fn) => {
|
|
775
|
+
const prior = v.functions.get(name);
|
|
776
|
+
v.functions.set(name, prior === undefined || prior === fn ? fn : null);
|
|
777
|
+
};
|
|
778
|
+
if (node.type === "FunctionDeclaration" && isNode(node.id)) {
|
|
779
|
+
put(node.id.name, node);
|
|
780
|
+
}
|
|
781
|
+
if (node.type === "VariableDeclarator" && isNode(node.id) && node.id.type === "Identifier") {
|
|
782
|
+
const init = node.init;
|
|
783
|
+
if (isNode(init) && isFunctionNode(init))
|
|
784
|
+
put(node.id.name, init);
|
|
785
|
+
}
|
|
786
|
+
for (const c of children(node))
|
|
787
|
+
indexFunctions(c, v);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Resolve a branch to the function it names, or to nothing.
|
|
791
|
+
*
|
|
792
|
+
* A NAMED branch resolves through its lexical binding at the combinator call, never through the
|
|
793
|
+
* program-wide name map: `async function use(branch) { await parallel({ branch }) }` passes its own
|
|
794
|
+
* parameter, and resolving that name to a same-named top-level declaration made a clean program
|
|
795
|
+
* unwriteable — L2032 blamed a write in a function the program never even called. A name bound to
|
|
796
|
+
* something that is not a function is UNPROVEN, and unproven is the interpreter's depth check to
|
|
797
|
+
* refuse at runtime, not the validator's to guess at.
|
|
798
|
+
*/
|
|
799
|
+
function resolveFunction(node, v, scope) {
|
|
800
|
+
if (isFunctionNode(node))
|
|
801
|
+
return node;
|
|
802
|
+
if (node.type !== "Identifier")
|
|
803
|
+
return undefined;
|
|
804
|
+
const name = node.name;
|
|
805
|
+
// Bound here: the binding answers, whatever it is bound to. Unbound: walk 2 has already raised
|
|
806
|
+
// L2001 for it, so the module index is the fallback.
|
|
807
|
+
if (scope.lookup(name) !== undefined)
|
|
808
|
+
return scope.lookupFn(name);
|
|
809
|
+
return v.functions.get(name) ?? undefined;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* The thunks a combinator owns, in every form the language accepts.
|
|
813
|
+
*
|
|
814
|
+
* `parallel({ a: () => …, b })` mixes an inline branch and a NAMED one, and a first version of this
|
|
815
|
+
* dropped the named half — so a captured-write program written with two named
|
|
816
|
+
* `async function`s instead of two arrows, was accepted. A branch is a branch however it is
|
|
817
|
+
* spelled.
|
|
818
|
+
*/
|
|
819
|
+
function branchThunks(node, v, scope) {
|
|
820
|
+
if (node === undefined)
|
|
821
|
+
return [];
|
|
822
|
+
const one = resolveFunction(node, v, scope);
|
|
823
|
+
if (one !== undefined)
|
|
824
|
+
return [one];
|
|
825
|
+
const out = [];
|
|
826
|
+
if (node.type === "ObjectExpression") {
|
|
827
|
+
for (const p of node.properties ?? []) {
|
|
828
|
+
if (p.type !== "Property" || !isNode(p.value))
|
|
829
|
+
continue;
|
|
830
|
+
const fn = resolveFunction(p.value, v, scope);
|
|
831
|
+
if (fn !== undefined)
|
|
832
|
+
out.push(fn);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (node.type === "ArrayExpression") {
|
|
836
|
+
for (const el of node.elements ?? []) {
|
|
837
|
+
if (el === null || !isNode(el))
|
|
838
|
+
continue;
|
|
839
|
+
const fn = resolveFunction(el, v, scope);
|
|
840
|
+
if (fn !== undefined)
|
|
841
|
+
out.push(fn);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return out;
|
|
845
|
+
}
|
|
846
|
+
/** The identifier a write lands on: `x` for `x`, `x.a`, `x[i].b` and `x?.a`. */
|
|
847
|
+
function rootIdentifier(node) {
|
|
848
|
+
if (node === undefined || !isNode(node))
|
|
849
|
+
return undefined;
|
|
850
|
+
if (node.type === "Identifier")
|
|
851
|
+
return node;
|
|
852
|
+
if (node.type === "MemberExpression")
|
|
853
|
+
return rootIdentifier(node.object);
|
|
854
|
+
if (node.type === "ChainExpression")
|
|
855
|
+
return rootIdentifier(node.expression);
|
|
856
|
+
return undefined;
|
|
857
|
+
}
|
|
858
|
+
function capturedWrite(at, name, combinator, v) {
|
|
859
|
+
v.fail("L2032", at, `\`${name}\` is declared outside this branch and written inside it. Two branches racing to write one place is nondeterministic, and freezing does not cover it because nothing crosses an effect boundary. It is also silent: live, the branches write in completion order, but on resume the recorded effects return instantly and they write in launch order, so \`${name}\` holds a different value and the run takes a path it never recorded — with no divergence raised, because no effect's inputs changed.`, `Return the value from the branch and read it out of \`${combinator}\`'s result, or use \`race\`, which yields its winner.`, combinator);
|
|
860
|
+
}
|
|
861
|
+
/** Walk one branch thunk with its OWN scope chain: anything it did not declare, it captured. */
|
|
862
|
+
function checkCapturedWrites(fn, combinator, v, seen) {
|
|
863
|
+
if (seen.has(fn))
|
|
864
|
+
return;
|
|
865
|
+
seen.add(fn);
|
|
866
|
+
const local = new Scope(null);
|
|
867
|
+
for (const p of fn.params ?? []) {
|
|
868
|
+
const names = [];
|
|
869
|
+
patternNames(p, names);
|
|
870
|
+
for (const n of names)
|
|
871
|
+
local.declare(n, "param");
|
|
872
|
+
}
|
|
873
|
+
if (isNode(fn.body))
|
|
874
|
+
walkCaptured(fn.body, local, combinator, v, seen);
|
|
875
|
+
}
|
|
876
|
+
function walkCaptured(node, scope, combinator, v, seen) {
|
|
877
|
+
switch (node.type) {
|
|
878
|
+
case "VariableDeclaration": {
|
|
879
|
+
const kind = node.kind === "const" ? "const" : "let";
|
|
880
|
+
for (const d of node.declarations ?? []) {
|
|
881
|
+
if (isNode(d.init))
|
|
882
|
+
walkCaptured(d.init, scope, combinator, v, seen);
|
|
883
|
+
const names = [];
|
|
884
|
+
patternNames(d.id, names);
|
|
885
|
+
for (const n of names)
|
|
886
|
+
scope.declare(n, kind);
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
case "AssignmentExpression":
|
|
891
|
+
case "UpdateExpression": {
|
|
892
|
+
// `x = 1`, `x += 1`, `x++`, `x.a = 1` and `x[i] += 1` are the same defect: something declared
|
|
893
|
+
// outside the branch is written inside it. A write through a member expression reaches the
|
|
894
|
+
// VALUE the outer binding holds, which is no more ordered across branches than the binding
|
|
895
|
+
// itself; the runtime half (values carry the depth they were born at) covers what a value
|
|
896
|
+
// reached through an alias hides from this walk.
|
|
897
|
+
const target = (node.type === "AssignmentExpression" ? node.left : node.argument);
|
|
898
|
+
const root = rootIdentifier(target);
|
|
899
|
+
if (root !== undefined && scope.lookup(root.name) === undefined) {
|
|
900
|
+
capturedWrite(root, root.name, combinator, v);
|
|
901
|
+
}
|
|
902
|
+
for (const c of children(node))
|
|
903
|
+
walkCaptured(c, scope, combinator, v, seen);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
case "CallExpression": {
|
|
907
|
+
// FOLLOW THE CALL. A branch that writes nothing itself and calls a helper that writes the
|
|
908
|
+
// outer binding is the same defect one level down, and it is invisible at the combinator call.
|
|
909
|
+
// The helper is checked as a branch in its own right, with its own scope, so the blame lands
|
|
910
|
+
// on the line that actually writes — and a helper that only touches its own locals is clean,
|
|
911
|
+
// which is what keeps ordinary shared procedures usable.
|
|
912
|
+
const callee = node.callee;
|
|
913
|
+
if (isNode(callee) && callee.type === "Identifier") {
|
|
914
|
+
const nm = callee.name;
|
|
915
|
+
// A locally-declared name is a local binding, not the program-level function of that name.
|
|
916
|
+
if (scope.lookup(nm) === undefined) {
|
|
917
|
+
const target = v.functions.get(nm);
|
|
918
|
+
if (target !== undefined && target !== null)
|
|
919
|
+
checkCapturedWrites(target, combinator, v, seen);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
// `outer.push(x)` from a branch is `outer[len(outer)] = x` spelled as a method.
|
|
923
|
+
if (isNode(callee) && callee.type === "MemberExpression") {
|
|
924
|
+
const member = callee;
|
|
925
|
+
const method = memberName(member);
|
|
926
|
+
const root = rootIdentifier(member.object);
|
|
927
|
+
if (method !== null &&
|
|
928
|
+
MUTATING_METHODS.has(method) &&
|
|
929
|
+
root !== undefined &&
|
|
930
|
+
scope.lookup(root.name) === undefined) {
|
|
931
|
+
capturedWrite(root, root.name, combinator, v);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
for (const c of children(node))
|
|
935
|
+
walkCaptured(c, scope, combinator, v, seen);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
case "FunctionDeclaration":
|
|
939
|
+
case "FunctionExpression":
|
|
940
|
+
case "ArrowFunctionExpression": {
|
|
941
|
+
if (node.type === "FunctionDeclaration" && isNode(node.id)) {
|
|
942
|
+
scope.declare(node.id.name, "const");
|
|
943
|
+
}
|
|
944
|
+
const inner = new Scope(scope);
|
|
945
|
+
for (const p of node.params ?? []) {
|
|
946
|
+
const names = [];
|
|
947
|
+
patternNames(p, names);
|
|
948
|
+
for (const n of names)
|
|
949
|
+
inner.declare(n, "param");
|
|
950
|
+
}
|
|
951
|
+
if (isNode(node.body))
|
|
952
|
+
walkCaptured(node.body, inner, combinator, v, seen);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
case "BlockStatement": {
|
|
956
|
+
const inner = new Scope(scope);
|
|
957
|
+
hoistFunctions(node, inner);
|
|
958
|
+
for (const s of node.body ?? [])
|
|
959
|
+
walkCaptured(s, inner, combinator, v, seen);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
case "CatchClause": {
|
|
963
|
+
const inner = new Scope(scope);
|
|
964
|
+
if (isNode(node.param)) {
|
|
965
|
+
const names = [];
|
|
966
|
+
patternNames(node.param, names);
|
|
967
|
+
for (const n of names)
|
|
968
|
+
inner.declare(n, "const");
|
|
969
|
+
}
|
|
970
|
+
if (isNode(node.body))
|
|
971
|
+
walkCaptured(node.body, inner, combinator, v, seen);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
case "ForStatement":
|
|
975
|
+
case "ForOfStatement": {
|
|
976
|
+
const inner = new Scope(scope);
|
|
977
|
+
for (const c of children(node))
|
|
978
|
+
walkCaptured(c, inner, combinator, v, seen);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
default: {
|
|
982
|
+
for (const c of children(node))
|
|
983
|
+
walkCaptured(c, scope, combinator, v, seen);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
613
987
|
}
|
|
614
988
|
function walkResolve(node, v, scope) {
|
|
615
989
|
switch (node.type) {
|
|
616
990
|
case "Identifier": {
|
|
617
991
|
const name = node.name;
|
|
992
|
+
if (scope.refersToPending(name)) {
|
|
993
|
+
v.fail("L2004", node, `\`${name}\` is declared later in this block, and a \`let\` or \`const\` binds the whole block: executed, this line would find a binding that holds no value yet (JavaScript's temporal dead zone, a guaranteed runtime error in straight-line code).`, "Move the declaration above its first use, or rename one of the two.");
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
618
996
|
if (scope.lookup(name) !== undefined)
|
|
619
997
|
return;
|
|
620
998
|
if (RESERVED_NAMES.has(name))
|
|
@@ -624,7 +1002,8 @@ function walkResolve(node, v, scope) {
|
|
|
624
1002
|
return;
|
|
625
1003
|
}
|
|
626
1004
|
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.`,
|
|
1005
|
+
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.`, HOST_GLOBAL_HINTS[name] ??
|
|
1006
|
+
"Use `now()` for time, `random()` for randomness, `sleep()` to wait, and `log()` for output.");
|
|
628
1007
|
return;
|
|
629
1008
|
}
|
|
630
1009
|
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(", ")}.`);
|
|
@@ -638,11 +1017,18 @@ function walkResolve(node, v, scope) {
|
|
|
638
1017
|
walkResolve(init, v, scope);
|
|
639
1018
|
const names = [];
|
|
640
1019
|
patternNames(d.id, names);
|
|
1020
|
+
// `const b = async () => {...}` binds a FUNCTION, and a branch named `b` must resolve to
|
|
1021
|
+
// it. Only the plain `id = function` form does: a destructured name holds whatever the
|
|
1022
|
+
// pattern pulled out, which is not statically a function.
|
|
1023
|
+
const bound = isNode(d.id) && d.id.type === "Identifier" && isNode(init) && isFunctionNode(init)
|
|
1024
|
+
? init
|
|
1025
|
+
: undefined;
|
|
641
1026
|
for (const n of names) {
|
|
642
1027
|
if (RESERVED_NAMES.has(n)) {
|
|
643
1028
|
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
1029
|
}
|
|
645
|
-
scope.declare(n, kind);
|
|
1030
|
+
scope.declare(n, kind, bound);
|
|
1031
|
+
scope.pending.delete(n);
|
|
646
1032
|
}
|
|
647
1033
|
}
|
|
648
1034
|
return;
|
|
@@ -681,21 +1067,46 @@ function walkResolve(node, v, scope) {
|
|
|
681
1067
|
if (RESERVED_NAMES.has(fname)) {
|
|
682
1068
|
v.fail("L2002", node.id, `\`${fname}\` is a builtin, so a function of that name would shadow it.`, "Rename the function.");
|
|
683
1069
|
}
|
|
684
|
-
scope.declare(fname, "const");
|
|
1070
|
+
scope.declare(fname, "const", node);
|
|
685
1071
|
}
|
|
686
1072
|
const inner = new Scope(scope);
|
|
687
|
-
|
|
1073
|
+
inner.fnBoundary = true;
|
|
1074
|
+
// A NAMED function expression binds its own name inside itself, which is how a function
|
|
1075
|
+
// assigned to a `const` recurses: `const f = function walk(n) { ... walk(n - 1) ... }`.
|
|
1076
|
+
if (node.type === "FunctionExpression" && isNode(node.id)) {
|
|
1077
|
+
const fname = node.id.name;
|
|
1078
|
+
if (RESERVED_NAMES.has(fname)) {
|
|
1079
|
+
v.fail("L2002", node.id, `\`${fname}\` is a builtin, so a function expression of that name would shadow it inside itself.`, "Rename the function.");
|
|
1080
|
+
}
|
|
1081
|
+
inner.declare(fname, "const", node);
|
|
1082
|
+
}
|
|
1083
|
+
// Parameters bind LEFT TO RIGHT, and a default sees only the parameters before it: every
|
|
1084
|
+
// name is pre-declared pending, and each parameter leaves the dead zone only after its own
|
|
1085
|
+
// default was walked, so `(a = b, b = 1)` and `(a = a)` are L2004 the way straight-line
|
|
1086
|
+
// block references are — JavaScript would throw the same at every call that evaluates the
|
|
1087
|
+
// default, and a default that cannot ever evaluate is a landmine, not a feature.
|
|
1088
|
+
const params = node.params ?? [];
|
|
1089
|
+
const paramNames = params.map((p) => {
|
|
688
1090
|
const names = [];
|
|
689
1091
|
patternNames(p, names);
|
|
690
|
-
|
|
1092
|
+
return names;
|
|
1093
|
+
});
|
|
1094
|
+
for (let i = 0; i < params.length; i += 1) {
|
|
1095
|
+
const p = params[i];
|
|
1096
|
+
for (const n of paramNames[i]) {
|
|
691
1097
|
if (RESERVED_NAMES.has(n)) {
|
|
692
1098
|
v.fail("L2002", p, `\`${n}\` is a builtin, so a parameter of that name would shadow it inside this function.`, "Rename the parameter.");
|
|
693
1099
|
}
|
|
694
1100
|
inner.declare(n, "param");
|
|
1101
|
+
inner.pending.add(n);
|
|
695
1102
|
}
|
|
696
|
-
|
|
1103
|
+
}
|
|
1104
|
+
for (let i = 0; i < params.length; i += 1) {
|
|
1105
|
+
const p = params[i];
|
|
697
1106
|
if (p.type === "AssignmentPattern" && isNode(p.right))
|
|
698
1107
|
walkResolve(p.right, v, inner);
|
|
1108
|
+
for (const n of paramNames[i])
|
|
1109
|
+
inner.pending.delete(n);
|
|
699
1110
|
}
|
|
700
1111
|
if (isNode(node.body))
|
|
701
1112
|
walkResolve(node.body, v, inner);
|
|
@@ -704,6 +1115,7 @@ function walkResolve(node, v, scope) {
|
|
|
704
1115
|
case "BlockStatement": {
|
|
705
1116
|
const inner = new Scope(scope);
|
|
706
1117
|
hoistFunctions(node, inner);
|
|
1118
|
+
hoistBindings(node, inner);
|
|
707
1119
|
for (const s of node.body ?? [])
|
|
708
1120
|
walkResolve(s, v, inner);
|
|
709
1121
|
return;
|
|
@@ -728,7 +1140,7 @@ function walkResolve(node, v, scope) {
|
|
|
728
1140
|
return;
|
|
729
1141
|
}
|
|
730
1142
|
case "CallExpression": {
|
|
731
|
-
checkCall(node, v);
|
|
1143
|
+
checkCall(node, v, scope);
|
|
732
1144
|
for (const c of children(node))
|
|
733
1145
|
walkResolve(c, v, scope);
|
|
734
1146
|
return;
|
|
@@ -744,7 +1156,26 @@ function walkResolve(node, v, scope) {
|
|
|
744
1156
|
function hoistFunctions(block, scope) {
|
|
745
1157
|
for (const s of block.body ?? []) {
|
|
746
1158
|
if (s.type === "FunctionDeclaration" && isNode(s.id)) {
|
|
747
|
-
scope.declare(s.id.name, "const");
|
|
1159
|
+
scope.declare(s.id.name, "const", s);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* `let`/`const` bind the whole block too — as PENDING, so a straight-line reference above the
|
|
1165
|
+
* declaration is L2004 rather than a resolution to whatever outer binding shares the name.
|
|
1166
|
+
*/
|
|
1167
|
+
function hoistBindings(block, scope) {
|
|
1168
|
+
for (const s of block.body ?? []) {
|
|
1169
|
+
if (s.type !== "VariableDeclaration")
|
|
1170
|
+
continue;
|
|
1171
|
+
const kind = s.kind === "const" ? "const" : "let";
|
|
1172
|
+
for (const d of s.declarations ?? []) {
|
|
1173
|
+
const names = [];
|
|
1174
|
+
patternNames(d.id, names);
|
|
1175
|
+
for (const n of names) {
|
|
1176
|
+
scope.declare(n, kind);
|
|
1177
|
+
scope.pending.add(n);
|
|
1178
|
+
}
|
|
748
1179
|
}
|
|
749
1180
|
}
|
|
750
1181
|
}
|
|
@@ -782,8 +1213,10 @@ export function validate(source, file = "program.cotal.js") {
|
|
|
782
1213
|
}
|
|
783
1214
|
// The module body is an async context: a program's top level is where the workflow lives.
|
|
784
1215
|
walkShape(ast, v, true);
|
|
1216
|
+
indexFunctions(ast, v);
|
|
785
1217
|
const top = new Scope(null);
|
|
786
1218
|
hoistFunctions(ast, top);
|
|
1219
|
+
hoistBindings(ast, top);
|
|
787
1220
|
for (const s of ast.body ?? [])
|
|
788
1221
|
walkResolve(s, v, top);
|
|
789
1222
|
if (v.errors.length > 0)
|