@invinite-org/chartlang-compiler 1.2.1 → 1.4.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +310 -0
  2. package/dist/analysis/extractDependencyGraph.d.ts.map +1 -1
  3. package/dist/analysis/extractDependencyGraph.js +9 -1
  4. package/dist/analysis/extractDependencyGraph.js.map +1 -1
  5. package/dist/analysis/extractInputs.d.ts.map +1 -1
  6. package/dist/analysis/extractInputs.js +2 -0
  7. package/dist/analysis/extractInputs.js.map +1 -1
  8. package/dist/analysis/extractMaxLookback.d.ts +2 -1
  9. package/dist/analysis/extractMaxLookback.d.ts.map +1 -1
  10. package/dist/analysis/extractMaxLookback.js +90 -6
  11. package/dist/analysis/extractMaxLookback.js.map +1 -1
  12. package/dist/analysis/extractRequestedIntervals.d.ts +63 -1
  13. package/dist/analysis/extractRequestedIntervals.d.ts.map +1 -1
  14. package/dist/analysis/extractRequestedIntervals.js +245 -29
  15. package/dist/analysis/extractRequestedIntervals.js.map +1 -1
  16. package/dist/analysis/forbiddenConstructs.d.ts.map +1 -1
  17. package/dist/analysis/forbiddenConstructs.js +2 -41
  18. package/dist/analysis/forbiddenConstructs.js.map +1 -1
  19. package/dist/analysis/index.d.ts +4 -1
  20. package/dist/analysis/index.d.ts.map +1 -1
  21. package/dist/analysis/index.js +3 -1
  22. package/dist/analysis/index.js.map +1 -1
  23. package/dist/analysis/loopBounds.d.ts +91 -0
  24. package/dist/analysis/loopBounds.d.ts.map +1 -0
  25. package/dist/analysis/loopBounds.js +132 -0
  26. package/dist/analysis/loopBounds.js.map +1 -0
  27. package/dist/analysis/resolveIndexBound.d.ts +73 -0
  28. package/dist/analysis/resolveIndexBound.d.ts.map +1 -0
  29. package/dist/analysis/resolveIndexBound.js +336 -0
  30. package/dist/analysis/resolveIndexBound.js.map +1 -0
  31. package/dist/analysis/stateArrayCapacity.d.ts +58 -0
  32. package/dist/analysis/stateArrayCapacity.d.ts.map +1 -0
  33. package/dist/analysis/stateArrayCapacity.js +108 -0
  34. package/dist/analysis/stateArrayCapacity.js.map +1 -0
  35. package/dist/analysis/validateSecurityExpr.d.ts +25 -0
  36. package/dist/analysis/validateSecurityExpr.d.ts.map +1 -0
  37. package/dist/analysis/validateSecurityExpr.js +154 -0
  38. package/dist/analysis/validateSecurityExpr.js.map +1 -0
  39. package/dist/api.d.ts.map +1 -1
  40. package/dist/api.js +22 -3
  41. package/dist/api.js.map +1 -1
  42. package/dist/diagnostics.d.ts +8 -2
  43. package/dist/diagnostics.d.ts.map +1 -1
  44. package/dist/diagnostics.js.map +1 -1
  45. package/dist/manifest.d.ts +3 -1
  46. package/dist/manifest.d.ts.map +1 -1
  47. package/dist/manifest.js +11 -0
  48. package/dist/manifest.js.map +1 -1
  49. package/dist/program.d.ts.map +1 -1
  50. package/dist/program.js +148 -15
  51. package/dist/program.js.map +1 -1
  52. package/dist/transformers/callsiteIdInjection.d.ts +21 -0
  53. package/dist/transformers/callsiteIdInjection.d.ts.map +1 -1
  54. package/dist/transformers/callsiteIdInjection.js +34 -4
  55. package/dist/transformers/callsiteIdInjection.js.map +1 -1
  56. package/dist/transformers/plotKindFromCallsite.d.ts +3 -0
  57. package/dist/transformers/plotKindFromCallsite.d.ts.map +1 -1
  58. package/dist/transformers/plotKindFromCallsite.js +7 -0
  59. package/dist/transformers/plotKindFromCallsite.js.map +1 -1
  60. package/dist/transformers/resolveCallee.d.ts +21 -0
  61. package/dist/transformers/resolveCallee.d.ts.map +1 -1
  62. package/dist/transformers/resolveCallee.js +14 -1
  63. package/dist/transformers/resolveCallee.js.map +1 -1
  64. package/package.json +2 -2
@@ -0,0 +1,336 @@
1
+ // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
+ // See the LICENSE file in the repo root for full license text.
3
+ import ts from "typescript";
4
+ import { boundedLoopVarId, parseBoundedForLoop, unwrapParens, } from "./loopBounds.js";
5
+ /**
6
+ * The provable maximum non-negative integer a series-index expression
7
+ * can reach at runtime, or `null` when no sound upper bound exists.
8
+ * Over-approximates: a result is always `>=` the true max index, so the
9
+ * runtime buffer (sized `maxLookback + 1`) never under-sizes. `null`
10
+ * signals the caller to fall back to the 5000-slot dynamic buffer.
11
+ *
12
+ * Resolves any expression built from numeric literals, `const`
13
+ * numeric-literal bindings (`ctx.constEnv`), bounded-loop induction
14
+ * variables (resolved to their full range), the binary operators `+`,
15
+ * `−`, `*`, unary `±`, and parentheses, by computing its integer
16
+ * interval and returning the **upper** endpoint. Any other node (another
17
+ * identifier, a call, `/`, `%`, `**`, a bitwise op, a non-numeric
18
+ * literal) collapses the containing interval — and thus the whole
19
+ * index — to `null`.
20
+ *
21
+ * @since 0.1
22
+ * @stable
23
+ * @example
24
+ * // for (let i = 0; i < 5; i++) { series[i + 1]; }
25
+ * // resolveIndexUpperBound(<the `i + 1` arg>, <access node>, ctx) → 5
26
+ * const fn: typeof resolveIndexUpperBound = resolveIndexUpperBound;
27
+ * void fn;
28
+ */
29
+ export function resolveIndexUpperBound(argument, node, ctx) {
30
+ const interval = evalInterval(argument, node, ctx);
31
+ return interval === null ? null : interval.hi;
32
+ }
33
+ /**
34
+ * The integer interval an index sub-expression spans, or `null` when any
35
+ * leaf or operator cannot be soundly bounded. The single evaluator that
36
+ * subsumes the leaf cases (literal / bounded-loop var / `const` number)
37
+ * and their affine combinations (`+`, `−`, `*`, unary `±`, parens).
38
+ */
39
+ function evalInterval(expr, node, ctx) {
40
+ const inner = unwrapParens(expr);
41
+ if (ts.isNumericLiteral(inner)) {
42
+ const value = Number(inner.text);
43
+ return finiteInterval(value, value);
44
+ }
45
+ if (ts.isIdentifier(inner)) {
46
+ const loopInterval = resolveLoopVarInterval(inner, node, ctx.checker);
47
+ if (loopInterval !== null)
48
+ return loopInterval;
49
+ const constValue = ctx.constEnv.get(inner.text);
50
+ return constValue === undefined ? null : finiteInterval(constValue, constValue);
51
+ }
52
+ if (ts.isPrefixUnaryExpression(inner)) {
53
+ if (inner.operator === ts.SyntaxKind.PlusToken) {
54
+ return evalInterval(inner.operand, node, ctx);
55
+ }
56
+ if (inner.operator === ts.SyntaxKind.MinusToken) {
57
+ const operand = evalInterval(inner.operand, node, ctx);
58
+ return operand === null ? null : finiteInterval(-operand.hi, -operand.lo);
59
+ }
60
+ return null;
61
+ }
62
+ if (ts.isBinaryExpression(inner)) {
63
+ return evalBinaryInterval(inner, node, ctx);
64
+ }
65
+ return null;
66
+ }
67
+ /**
68
+ * The interval of a `+`/`−`/`*` over two sub-intervals, or `null` when
69
+ * either operand is unbounded or the operator is unsupported (`/`, `%`,
70
+ * `**`, bitwise, …). Multiplication takes the min/max of the four
71
+ * endpoint products so the bound is correct for any sign combination.
72
+ */
73
+ function evalBinaryInterval(expr, node, ctx) {
74
+ const left = evalInterval(expr.left, node, ctx);
75
+ if (left === null)
76
+ return null;
77
+ const right = evalInterval(expr.right, node, ctx);
78
+ if (right === null)
79
+ return null;
80
+ switch (expr.operatorToken.kind) {
81
+ case ts.SyntaxKind.PlusToken:
82
+ return finiteInterval(left.lo + right.lo, left.hi + right.hi);
83
+ case ts.SyntaxKind.MinusToken:
84
+ return finiteInterval(left.lo - right.hi, left.hi - right.lo);
85
+ case ts.SyntaxKind.AsteriskToken: {
86
+ const products = [
87
+ left.lo * right.lo,
88
+ left.lo * right.hi,
89
+ left.hi * right.lo,
90
+ left.hi * right.hi,
91
+ ];
92
+ return finiteInterval(Math.min(...products), Math.max(...products));
93
+ }
94
+ default:
95
+ return null;
96
+ }
97
+ }
98
+ /**
99
+ * An interval with finite endpoints, or `null` when either endpoint is
100
+ * non-finite. A defensive guard against a pathological literal
101
+ * (`1e400` → `Infinity`) or an overflow product feeding a non-finite
102
+ * `hi` into `maxLookback`; integer-ness needs no check (see `Interval`).
103
+ */
104
+ function finiteInterval(lo, hi) {
105
+ if (!Number.isFinite(lo) || !Number.isFinite(hi))
106
+ return null;
107
+ // Normalise `-0` (e.g. `-2 * 0`) to `0` so a resolved bound is never the
108
+ // negative zero a downstream `Object.is`/strict consumer would distinguish.
109
+ return { lo: lo + 0, hi: hi + 0 };
110
+ }
111
+ /**
112
+ * The index range a bare bounded-loop induction variable can span, or
113
+ * `null` when the identifier is not the induction variable of an
114
+ * enclosing legal `for`, is reassigned in the body, runs a
115
+ * non-terminating `>`/`>=` loop, or is a shadowed name that does not
116
+ * resolve to the loop's own declaration. Affine forms need the full
117
+ * range (`K - i` is largest when `i` is smallest); a bare loop-var read
118
+ * still takes `interval.hi`, identical to the leaf max.
119
+ */
120
+ function resolveLoopVarInterval(id, node, checker) {
121
+ const idSymbol = checker.getSymbolAtLocation(id);
122
+ let current = node;
123
+ while (current) {
124
+ if (ts.isForStatement(current)) {
125
+ const loopVarId = boundedLoopVarId(current);
126
+ if (loopVarId && loopVarId.text === id.text) {
127
+ // Confirm the use refers to THIS loop's induction variable
128
+ // and not a nested binding that shadows the same text.
129
+ const loopSymbol = checker.getSymbolAtLocation(loopVarId);
130
+ if (!idSymbol || !loopSymbol || idSymbol !== loopSymbol)
131
+ return null;
132
+ const loop = parseBoundedForLoop(current);
133
+ if (loop === null)
134
+ return null;
135
+ if (isLoopVarReassigned(current, loop.varName))
136
+ return null;
137
+ return loopVarInterval(loop);
138
+ }
139
+ }
140
+ current = current.parent;
141
+ }
142
+ return null;
143
+ }
144
+ /**
145
+ * The full index range a terminating `for` reaches (`[start, max]`), or
146
+ * `null` for a non-terminating (`>`/`>=` with `i++`) header the resolver
147
+ * cannot bound. `<` reaches `limit - 1`; `<=` reaches `limit`.
148
+ */
149
+ function loopVarInterval(loop) {
150
+ if (loop.op === ts.SyntaxKind.LessThanToken) {
151
+ return finiteInterval(loop.start, loop.limit - 1);
152
+ }
153
+ if (loop.op === ts.SyntaxKind.LessThanEqualsToken) {
154
+ return finiteInterval(loop.start, loop.limit);
155
+ }
156
+ return null;
157
+ }
158
+ /**
159
+ * Whether the loop body reassigns `varName` beyond the header `i++`
160
+ * incrementor (a plain `=`, a compound assignment, or an extra `++`/`--`
161
+ * whose target is the induction variable). Such a body breaks the
162
+ * `limit`-based bound, so the resolver refuses to size the read.
163
+ */
164
+ function isLoopVarReassigned(loop, varName) {
165
+ const body = loop.statement;
166
+ let reassigned = false;
167
+ const visit = (node) => {
168
+ if (reassigned)
169
+ return;
170
+ if (ts.isBinaryExpression(node) &&
171
+ isAssignmentOperator(node.operatorToken.kind) &&
172
+ ts.isIdentifier(node.left) &&
173
+ node.left.text === varName) {
174
+ reassigned = true;
175
+ return;
176
+ }
177
+ if ((ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node)) &&
178
+ (node.operator === ts.SyntaxKind.PlusPlusToken ||
179
+ node.operator === ts.SyntaxKind.MinusMinusToken) &&
180
+ ts.isIdentifier(node.operand) &&
181
+ node.operand.text === varName) {
182
+ reassigned = true;
183
+ return;
184
+ }
185
+ ts.forEachChild(node, visit);
186
+ };
187
+ visit(body);
188
+ return reassigned;
189
+ }
190
+ /** Whether a binary operator token writes to its left operand. */
191
+ function isAssignmentOperator(kind) {
192
+ return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
193
+ }
194
+ /**
195
+ * The `const <id> = <numeric literal>` bindings lexically visible at a
196
+ * specific series-index expression. Only `const` initialised with a
197
+ * numeric literal — or a unary `+`/`-` on one — is included (mirroring
198
+ * `extractInputs.readLiteral`'s numeric handling); a binary initialiser
199
+ * is left for Task 2's interval evaluator and is simply omitted. The walk
200
+ * runs from `useSite` outward through its lexical containers up to
201
+ * `scopeRoot`, collecting only declarations that occur before
202
+ * `useSite.pos` within each container, so it never sees a declaration
203
+ * after the read, inside a sibling block, or in a nested function/class
204
+ * that does not contain `useSite`. The innermost visible binding for a
205
+ * name wins (normal shadowing) — including binders that are not
206
+ * `var`/`let`/`const` statements: a `for`-init induction variable and a
207
+ * function parameter shadow an outer numeric `const` of the same name
208
+ * (`markContainerBinders`), so a reassigned `for (let i …)` index or a
209
+ * `request.security((k) => series[k])` callback parameter can never leak
210
+ * an unrelated outer `const k`'s value into the bound (which would
211
+ * under-size the buffer).
212
+ *
213
+ * @since 0.1
214
+ * @stable
215
+ * @example
216
+ * // const k = 3; series[k];
217
+ * // collectConstNumberEnv(<the `k` arg>, scope).get("k") → 3
218
+ * const fn: typeof collectConstNumberEnv = collectConstNumberEnv;
219
+ * void fn;
220
+ */
221
+ export function collectConstNumberEnv(useSite, scopeRoot) {
222
+ const env = new Map();
223
+ // Every `var`/`let`/`const` name bound at or inside a nearer container,
224
+ // numeric or not. A nearer binding shadows an outer one even when it is
225
+ // a `let` or a non-numeric `const`, so an outer `const k = 5` must not
226
+ // leak through it — once a name is `seen`, no outer container can set it.
227
+ const seen = new Set();
228
+ let container = useSite.parent;
229
+ while (container) {
230
+ // A `for`-init variable / function parameter introduced by this
231
+ // container shadows any same-named outer `const`, even though it is
232
+ // not a `var`/`let`/`const` statement `variableDeclarationsIn` scans.
233
+ markContainerBinders(container, seen);
234
+ for (const declaration of variableDeclarationsIn(container)) {
235
+ if (declaration.pos >= useSite.pos)
236
+ continue;
237
+ if (!ts.isIdentifier(declaration.name))
238
+ continue;
239
+ const name = declaration.name.text;
240
+ if (seen.has(name))
241
+ continue;
242
+ seen.add(name);
243
+ const list = declaration.parent;
244
+ if (ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0) {
245
+ const value = readNumericLiteralInit(declaration);
246
+ if (value !== null)
247
+ env.set(name, value);
248
+ }
249
+ }
250
+ if (container === scopeRoot)
251
+ break;
252
+ container = container.parent;
253
+ }
254
+ return env;
255
+ }
256
+ /**
257
+ * Mark every binding name a container introduces at its OWN level — a
258
+ * `for` initializer's induction variable and a function-like's parameters
259
+ * — as `seen`, so an outer numeric `const` of the same name cannot leak
260
+ * past it. These binders are never numeric `const`s the resolver trusts,
261
+ * so marking them only blocks unsound shadow leaks: a reassigned
262
+ * `for (let i …)` index or a callback parameter (`(k) => series[k]`) that
263
+ * collides with an outer `const i`/`const k`. Only identifier binders
264
+ * matter here (a numeric series index is an identifier); destructured
265
+ * parameter/loop patterns bind no numeric index and are skipped.
266
+ */
267
+ function markContainerBinders(container, seen) {
268
+ if (ts.isForStatement(container)) {
269
+ const init = container.initializer;
270
+ if (init && ts.isVariableDeclarationList(init)) {
271
+ for (const declaration of init.declarations) {
272
+ if (ts.isIdentifier(declaration.name))
273
+ seen.add(declaration.name.text);
274
+ }
275
+ }
276
+ return;
277
+ }
278
+ if (ts.isFunctionLike(container)) {
279
+ for (const parameter of container.parameters) {
280
+ if (ts.isIdentifier(parameter.name))
281
+ seen.add(parameter.name.text);
282
+ }
283
+ }
284
+ }
285
+ /**
286
+ * The direct `var`/`let`/`const` `VariableDeclaration`s of a container —
287
+ * of every declaration kind, so the caller can detect a nearer binding
288
+ * that shadows an outer numeric `const` — without descending into nested
289
+ * functions, classes, or blocks (those are handled by their own
290
+ * enclosing-container pass when they actually contain the use site).
291
+ * `Block`, `SourceFile`, function bodies, and case clauses hold their
292
+ * declarations as `statements`/`clauses` we scan directly.
293
+ */
294
+ function variableDeclarationsIn(container) {
295
+ const declarations = [];
296
+ const statements = statementsOf(container);
297
+ for (const statement of statements) {
298
+ if (!ts.isVariableStatement(statement))
299
+ continue;
300
+ for (const declaration of statement.declarationList.declarations) {
301
+ declarations.push(declaration);
302
+ }
303
+ }
304
+ return declarations;
305
+ }
306
+ /** The lexical statement list a container exposes, or `[]` when it holds none. */
307
+ function statementsOf(container) {
308
+ if (ts.isSourceFile(container) || ts.isBlock(container) || ts.isModuleBlock(container)) {
309
+ return container.statements;
310
+ }
311
+ if (ts.isCaseClause(container) || ts.isDefaultClause(container)) {
312
+ return container.statements;
313
+ }
314
+ return [];
315
+ }
316
+ /**
317
+ * The numeric value of a `const k = <numeric literal>` /
318
+ * `const k = <unary ± numeric literal>` initialiser, or `null` for any
319
+ * other initialiser (no binary folding here — that is Task 2).
320
+ */
321
+ function readNumericLiteralInit(declaration) {
322
+ const initializer = declaration.initializer;
323
+ if (!initializer)
324
+ return null;
325
+ const expr = unwrapParens(initializer);
326
+ if (ts.isNumericLiteral(expr))
327
+ return Number(expr.text);
328
+ if (ts.isPrefixUnaryExpression(expr) &&
329
+ (expr.operator === ts.SyntaxKind.MinusToken || expr.operator === ts.SyntaxKind.PlusToken) &&
330
+ ts.isNumericLiteral(expr.operand)) {
331
+ const value = Number(expr.operand.text);
332
+ return expr.operator === ts.SyntaxKind.MinusToken ? -value : value;
333
+ }
334
+ return null;
335
+ }
336
+ //# sourceMappingURL=resolveIndexBound.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveIndexBound.js","sourceRoot":"","sources":["../../src/analysis/resolveIndexBound.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nimport ts from \"typescript\";\n\nimport {\n type BoundedForLoop,\n boundedLoopVarId,\n parseBoundedForLoop,\n unwrapParens,\n} from \"./loopBounds.js\";\n\n/**\n * Compile-time context for resolving a series index's upper bound.\n *\n * @since 0.1\n * @stable\n * @example\n * const ctx: IndexBoundContext = {\n * constEnv: new Map([[\"k\", 3]]),\n * checker, // ts.TypeChecker\n * };\n * void ctx;\n */\nexport type IndexBoundContext = Readonly<{\n /** `const <id> = <numeric literal>` bindings visible at the index use site. */\n constEnv: ReadonlyMap<string, number>;\n /** Checker used to avoid resolving loop variables through a shadowed name. */\n checker: ts.TypeChecker;\n}>;\n\n/**\n * The compile-time integer range an index sub-expression can span. Every\n * input is an integer and `+`/`−`/`*`/unary-`±` preserve integers, so the\n * endpoints are exact integers — no rounding and no `Number.isInteger`\n * guard is needed.\n */\ntype Interval = Readonly<{ lo: number; hi: number }>;\n\n/**\n * The provable maximum non-negative integer a series-index expression\n * can reach at runtime, or `null` when no sound upper bound exists.\n * Over-approximates: a result is always `>=` the true max index, so the\n * runtime buffer (sized `maxLookback + 1`) never under-sizes. `null`\n * signals the caller to fall back to the 5000-slot dynamic buffer.\n *\n * Resolves any expression built from numeric literals, `const`\n * numeric-literal bindings (`ctx.constEnv`), bounded-loop induction\n * variables (resolved to their full range), the binary operators `+`,\n * `−`, `*`, unary `±`, and parentheses, by computing its integer\n * interval and returning the **upper** endpoint. Any other node (another\n * identifier, a call, `/`, `%`, `**`, a bitwise op, a non-numeric\n * literal) collapses the containing interval — and thus the whole\n * index — to `null`.\n *\n * @since 0.1\n * @stable\n * @example\n * // for (let i = 0; i < 5; i++) { series[i + 1]; }\n * // resolveIndexUpperBound(<the `i + 1` arg>, <access node>, ctx) → 5\n * const fn: typeof resolveIndexUpperBound = resolveIndexUpperBound;\n * void fn;\n */\nexport function resolveIndexUpperBound(\n argument: ts.Expression,\n node: ts.Node,\n ctx: IndexBoundContext,\n): number | null {\n const interval = evalInterval(argument, node, ctx);\n return interval === null ? null : interval.hi;\n}\n\n/**\n * The integer interval an index sub-expression spans, or `null` when any\n * leaf or operator cannot be soundly bounded. The single evaluator that\n * subsumes the leaf cases (literal / bounded-loop var / `const` number)\n * and their affine combinations (`+`, `−`, `*`, unary `±`, parens).\n */\nfunction evalInterval(expr: ts.Expression, node: ts.Node, ctx: IndexBoundContext): Interval | null {\n const inner = unwrapParens(expr);\n\n if (ts.isNumericLiteral(inner)) {\n const value = Number(inner.text);\n return finiteInterval(value, value);\n }\n\n if (ts.isIdentifier(inner)) {\n const loopInterval = resolveLoopVarInterval(inner, node, ctx.checker);\n if (loopInterval !== null) return loopInterval;\n const constValue = ctx.constEnv.get(inner.text);\n return constValue === undefined ? null : finiteInterval(constValue, constValue);\n }\n\n if (ts.isPrefixUnaryExpression(inner)) {\n if (inner.operator === ts.SyntaxKind.PlusToken) {\n return evalInterval(inner.operand, node, ctx);\n }\n if (inner.operator === ts.SyntaxKind.MinusToken) {\n const operand = evalInterval(inner.operand, node, ctx);\n return operand === null ? null : finiteInterval(-operand.hi, -operand.lo);\n }\n return null;\n }\n\n if (ts.isBinaryExpression(inner)) {\n return evalBinaryInterval(inner, node, ctx);\n }\n\n return null;\n}\n\n/**\n * The interval of a `+`/`−`/`*` over two sub-intervals, or `null` when\n * either operand is unbounded or the operator is unsupported (`/`, `%`,\n * `**`, bitwise, …). Multiplication takes the min/max of the four\n * endpoint products so the bound is correct for any sign combination.\n */\nfunction evalBinaryInterval(\n expr: ts.BinaryExpression,\n node: ts.Node,\n ctx: IndexBoundContext,\n): Interval | null {\n const left = evalInterval(expr.left, node, ctx);\n if (left === null) return null;\n const right = evalInterval(expr.right, node, ctx);\n if (right === null) return null;\n\n switch (expr.operatorToken.kind) {\n case ts.SyntaxKind.PlusToken:\n return finiteInterval(left.lo + right.lo, left.hi + right.hi);\n case ts.SyntaxKind.MinusToken:\n return finiteInterval(left.lo - right.hi, left.hi - right.lo);\n case ts.SyntaxKind.AsteriskToken: {\n const products = [\n left.lo * right.lo,\n left.lo * right.hi,\n left.hi * right.lo,\n left.hi * right.hi,\n ];\n return finiteInterval(Math.min(...products), Math.max(...products));\n }\n default:\n return null;\n }\n}\n\n/**\n * An interval with finite endpoints, or `null` when either endpoint is\n * non-finite. A defensive guard against a pathological literal\n * (`1e400` → `Infinity`) or an overflow product feeding a non-finite\n * `hi` into `maxLookback`; integer-ness needs no check (see `Interval`).\n */\nfunction finiteInterval(lo: number, hi: number): Interval | null {\n if (!Number.isFinite(lo) || !Number.isFinite(hi)) return null;\n // Normalise `-0` (e.g. `-2 * 0`) to `0` so a resolved bound is never the\n // negative zero a downstream `Object.is`/strict consumer would distinguish.\n return { lo: lo + 0, hi: hi + 0 };\n}\n\n/**\n * The index range a bare bounded-loop induction variable can span, or\n * `null` when the identifier is not the induction variable of an\n * enclosing legal `for`, is reassigned in the body, runs a\n * non-terminating `>`/`>=` loop, or is a shadowed name that does not\n * resolve to the loop's own declaration. Affine forms need the full\n * range (`K - i` is largest when `i` is smallest); a bare loop-var read\n * still takes `interval.hi`, identical to the leaf max.\n */\nfunction resolveLoopVarInterval(\n id: ts.Identifier,\n node: ts.Node,\n checker: ts.TypeChecker,\n): Interval | null {\n const idSymbol = checker.getSymbolAtLocation(id);\n\n let current: ts.Node | undefined = node;\n while (current) {\n if (ts.isForStatement(current)) {\n const loopVarId = boundedLoopVarId(current);\n if (loopVarId && loopVarId.text === id.text) {\n // Confirm the use refers to THIS loop's induction variable\n // and not a nested binding that shadows the same text.\n const loopSymbol = checker.getSymbolAtLocation(loopVarId);\n if (!idSymbol || !loopSymbol || idSymbol !== loopSymbol) return null;\n const loop = parseBoundedForLoop(current);\n if (loop === null) return null;\n if (isLoopVarReassigned(current, loop.varName)) return null;\n return loopVarInterval(loop);\n }\n }\n current = current.parent;\n }\n return null;\n}\n\n/**\n * The full index range a terminating `for` reaches (`[start, max]`), or\n * `null` for a non-terminating (`>`/`>=` with `i++`) header the resolver\n * cannot bound. `<` reaches `limit - 1`; `<=` reaches `limit`.\n */\nfunction loopVarInterval(loop: BoundedForLoop): Interval | null {\n if (loop.op === ts.SyntaxKind.LessThanToken) {\n return finiteInterval(loop.start, loop.limit - 1);\n }\n if (loop.op === ts.SyntaxKind.LessThanEqualsToken) {\n return finiteInterval(loop.start, loop.limit);\n }\n return null;\n}\n\n/**\n * Whether the loop body reassigns `varName` beyond the header `i++`\n * incrementor (a plain `=`, a compound assignment, or an extra `++`/`--`\n * whose target is the induction variable). Such a body breaks the\n * `limit`-based bound, so the resolver refuses to size the read.\n */\nfunction isLoopVarReassigned(loop: ts.ForStatement, varName: string): boolean {\n const body = loop.statement;\n let reassigned = false;\n const visit = (node: ts.Node): void => {\n if (reassigned) return;\n if (\n ts.isBinaryExpression(node) &&\n isAssignmentOperator(node.operatorToken.kind) &&\n ts.isIdentifier(node.left) &&\n node.left.text === varName\n ) {\n reassigned = true;\n return;\n }\n if (\n (ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node)) &&\n (node.operator === ts.SyntaxKind.PlusPlusToken ||\n node.operator === ts.SyntaxKind.MinusMinusToken) &&\n ts.isIdentifier(node.operand) &&\n node.operand.text === varName\n ) {\n reassigned = true;\n return;\n }\n ts.forEachChild(node, visit);\n };\n visit(body);\n return reassigned;\n}\n\n/** Whether a binary operator token writes to its left operand. */\nfunction isAssignmentOperator(kind: ts.SyntaxKind): boolean {\n return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;\n}\n\n/**\n * The `const <id> = <numeric literal>` bindings lexically visible at a\n * specific series-index expression. Only `const` initialised with a\n * numeric literal — or a unary `+`/`-` on one — is included (mirroring\n * `extractInputs.readLiteral`'s numeric handling); a binary initialiser\n * is left for Task 2's interval evaluator and is simply omitted. The walk\n * runs from `useSite` outward through its lexical containers up to\n * `scopeRoot`, collecting only declarations that occur before\n * `useSite.pos` within each container, so it never sees a declaration\n * after the read, inside a sibling block, or in a nested function/class\n * that does not contain `useSite`. The innermost visible binding for a\n * name wins (normal shadowing) — including binders that are not\n * `var`/`let`/`const` statements: a `for`-init induction variable and a\n * function parameter shadow an outer numeric `const` of the same name\n * (`markContainerBinders`), so a reassigned `for (let i …)` index or a\n * `request.security((k) => series[k])` callback parameter can never leak\n * an unrelated outer `const k`'s value into the bound (which would\n * under-size the buffer).\n *\n * @since 0.1\n * @stable\n * @example\n * // const k = 3; series[k];\n * // collectConstNumberEnv(<the `k` arg>, scope).get(\"k\") → 3\n * const fn: typeof collectConstNumberEnv = collectConstNumberEnv;\n * void fn;\n */\nexport function collectConstNumberEnv(\n useSite: ts.Node,\n scopeRoot: ts.Node,\n): ReadonlyMap<string, number> {\n const env = new Map<string, number>();\n // Every `var`/`let`/`const` name bound at or inside a nearer container,\n // numeric or not. A nearer binding shadows an outer one even when it is\n // a `let` or a non-numeric `const`, so an outer `const k = 5` must not\n // leak through it — once a name is `seen`, no outer container can set it.\n const seen = new Set<string>();\n\n let container: ts.Node | undefined = useSite.parent;\n while (container) {\n // A `for`-init variable / function parameter introduced by this\n // container shadows any same-named outer `const`, even though it is\n // not a `var`/`let`/`const` statement `variableDeclarationsIn` scans.\n markContainerBinders(container, seen);\n for (const declaration of variableDeclarationsIn(container)) {\n if (declaration.pos >= useSite.pos) continue;\n if (!ts.isIdentifier(declaration.name)) continue;\n const name = declaration.name.text;\n if (seen.has(name)) continue;\n seen.add(name);\n const list = declaration.parent;\n if (ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0) {\n const value = readNumericLiteralInit(declaration);\n if (value !== null) env.set(name, value);\n }\n }\n if (container === scopeRoot) break;\n container = container.parent;\n }\n\n return env;\n}\n\n/**\n * Mark every binding name a container introduces at its OWN level — a\n * `for` initializer's induction variable and a function-like's parameters\n * — as `seen`, so an outer numeric `const` of the same name cannot leak\n * past it. These binders are never numeric `const`s the resolver trusts,\n * so marking them only blocks unsound shadow leaks: a reassigned\n * `for (let i …)` index or a callback parameter (`(k) => series[k]`) that\n * collides with an outer `const i`/`const k`. Only identifier binders\n * matter here (a numeric series index is an identifier); destructured\n * parameter/loop patterns bind no numeric index and are skipped.\n */\nfunction markContainerBinders(container: ts.Node, seen: Set<string>): void {\n if (ts.isForStatement(container)) {\n const init = container.initializer;\n if (init && ts.isVariableDeclarationList(init)) {\n for (const declaration of init.declarations) {\n if (ts.isIdentifier(declaration.name)) seen.add(declaration.name.text);\n }\n }\n return;\n }\n if (ts.isFunctionLike(container)) {\n for (const parameter of container.parameters) {\n if (ts.isIdentifier(parameter.name)) seen.add(parameter.name.text);\n }\n }\n}\n\n/**\n * The direct `var`/`let`/`const` `VariableDeclaration`s of a container —\n * of every declaration kind, so the caller can detect a nearer binding\n * that shadows an outer numeric `const` — without descending into nested\n * functions, classes, or blocks (those are handled by their own\n * enclosing-container pass when they actually contain the use site).\n * `Block`, `SourceFile`, function bodies, and case clauses hold their\n * declarations as `statements`/`clauses` we scan directly.\n */\nfunction variableDeclarationsIn(container: ts.Node): ReadonlyArray<ts.VariableDeclaration> {\n const declarations: ts.VariableDeclaration[] = [];\n const statements = statementsOf(container);\n for (const statement of statements) {\n if (!ts.isVariableStatement(statement)) continue;\n for (const declaration of statement.declarationList.declarations) {\n declarations.push(declaration);\n }\n }\n return declarations;\n}\n\n/** The lexical statement list a container exposes, or `[]` when it holds none. */\nfunction statementsOf(container: ts.Node): ReadonlyArray<ts.Statement> {\n if (ts.isSourceFile(container) || ts.isBlock(container) || ts.isModuleBlock(container)) {\n return container.statements;\n }\n if (ts.isCaseClause(container) || ts.isDefaultClause(container)) {\n return container.statements;\n }\n return [];\n}\n\n/**\n * The numeric value of a `const k = <numeric literal>` /\n * `const k = <unary ± numeric literal>` initialiser, or `null` for any\n * other initialiser (no binary folding here — that is Task 2).\n */\nfunction readNumericLiteralInit(declaration: ts.VariableDeclaration): number | null {\n const initializer = declaration.initializer;\n if (!initializer) return null;\n const expr = unwrapParens(initializer);\n if (ts.isNumericLiteral(expr)) return Number(expr.text);\n if (\n ts.isPrefixUnaryExpression(expr) &&\n (expr.operator === ts.SyntaxKind.MinusToken || expr.operator === ts.SyntaxKind.PlusToken) &&\n ts.isNumericLiteral(expr.operand)\n ) {\n const value = Number(expr.operand.text);\n return expr.operator === ts.SyntaxKind.MinusToken ? -value : value;\n }\n return null;\n}\n"]}
@@ -0,0 +1,58 @@
1
+ import ts from "typescript";
2
+ import { type CompileDiagnostic } from "../diagnostics.js";
3
+ /**
4
+ * The largest `capacity` a `state.array<T>(capacity)` allocation may request.
5
+ * The collection is bounded-execution-safe only because its size is fixed at
6
+ * compile time: a hard ceiling caps memory, caps the JSON snapshot size, and —
7
+ * crucially — caps the per-tick two-ring `Float64Array` copy the runtime does
8
+ * on every tick rollback. `100_000` is generous (the dominant rolling-window /
9
+ * event-log cases are ≤ a few hundred) while still keeping the snapshot and
10
+ * the per-tick copy bounded.
11
+ *
12
+ * @since 1.3
13
+ * @stable
14
+ * @example
15
+ * // state.array<number>(20) → 20 <= MAX_STATE_ARRAY_CAPACITY (OK)
16
+ * // state.array<number>(1_000_000) → exceeds the cap (error)
17
+ * const cap: number = MAX_STATE_ARRAY_CAPACITY;
18
+ * void cap;
19
+ */
20
+ export declare const MAX_STATE_ARRAY_CAPACITY = 100000;
21
+ /**
22
+ * Walk the source file and flag every `state.array<T>(capacity)` allocation
23
+ * whose `capacity` argument is not a compile-time-resolvable positive integer
24
+ * within `MAX_STATE_ARRAY_CAPACITY`. This pins the bounded-execution +
25
+ * bounded-snapshot invariant at the compiler boundary: a non-literal capacity
26
+ * would make the backing ring's size — and therefore its snapshot size and
27
+ * per-tick rollback cost — non-deterministic.
28
+ *
29
+ * Capacity resolution reuses `resolveIndexUpperBound` + `collectConstNumberEnv`
30
+ * (the same machinery that sizes a series index), so a bare numeric literal,
31
+ * a parenthesised / unary-`±` literal, and a `const` numeric-literal binding
32
+ * (`const K = 20; state.array(K)`) are all accepted; a `let`, an input, or any
33
+ * runtime expression resolves to `null` and is rejected.
34
+ *
35
+ * Two error codes:
36
+ * - `state-array-capacity-not-literal` — the capacity is missing or does not
37
+ * resolve to a compile-time number.
38
+ * - `state-array-capacity-exceeds-max` — the capacity resolves but is `<= 0`,
39
+ * non-integer, or `> MAX_STATE_ARRAY_CAPACITY`.
40
+ *
41
+ * The walk runs on the **original** AST (positions match the user's source,
42
+ * and — running pre-injection — the capacity is `node.arguments[0]`, before
43
+ * the slot-id literal is injected as the leading argument). The element-access
44
+ * form `state["array"](cap)` is rejected upstream as
45
+ * `stateful-call-element-access` and never matches `"state.array"` here, so it
46
+ * is not double-reported. A `state.array(...)` inside a loop additionally
47
+ * errors `stateful-call-inside-loop`; both passes are independent.
48
+ *
49
+ * @since 1.3
50
+ * @example
51
+ * // const diagnostics = runStateArrayCapacity(
52
+ * // sourceFile, checker, "demo.chart.ts",
53
+ * // );
54
+ * const fn: typeof runStateArrayCapacity = runStateArrayCapacity;
55
+ * void fn;
56
+ */
57
+ export declare function runStateArrayCapacity(sourceFile: ts.SourceFile, checker: ts.TypeChecker, sourcePath: string): ReadonlyArray<CompileDiagnostic>;
58
+ //# sourceMappingURL=stateArrayCapacity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stateArrayCapacity.d.ts","sourceRoot":"","sources":["../../src/analysis/stateArrayCapacity.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAI7E;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,wBAAwB,SAAU,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,qBAAqB,CACjC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,OAAO,EAAE,EAAE,CAAC,WAAW,EACvB,UAAU,EAAE,MAAM,GACnB,aAAa,CAAC,iBAAiB,CAAC,CAwDlC"}
@@ -0,0 +1,108 @@
1
+ // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
+ // See the LICENSE file in the repo root for full license text.
3
+ import ts from "typescript";
4
+ import { createDiagnostic } from "../diagnostics.js";
5
+ import { resolveCalleeName } from "../transformers/resolveCallee.js";
6
+ import { collectConstNumberEnv, resolveIndexUpperBound } from "./resolveIndexBound.js";
7
+ /**
8
+ * The largest `capacity` a `state.array<T>(capacity)` allocation may request.
9
+ * The collection is bounded-execution-safe only because its size is fixed at
10
+ * compile time: a hard ceiling caps memory, caps the JSON snapshot size, and —
11
+ * crucially — caps the per-tick two-ring `Float64Array` copy the runtime does
12
+ * on every tick rollback. `100_000` is generous (the dominant rolling-window /
13
+ * event-log cases are ≤ a few hundred) while still keeping the snapshot and
14
+ * the per-tick copy bounded.
15
+ *
16
+ * @since 1.3
17
+ * @stable
18
+ * @example
19
+ * // state.array<number>(20) → 20 <= MAX_STATE_ARRAY_CAPACITY (OK)
20
+ * // state.array<number>(1_000_000) → exceeds the cap (error)
21
+ * const cap: number = MAX_STATE_ARRAY_CAPACITY;
22
+ * void cap;
23
+ */
24
+ export const MAX_STATE_ARRAY_CAPACITY = 100_000;
25
+ /**
26
+ * Walk the source file and flag every `state.array<T>(capacity)` allocation
27
+ * whose `capacity` argument is not a compile-time-resolvable positive integer
28
+ * within `MAX_STATE_ARRAY_CAPACITY`. This pins the bounded-execution +
29
+ * bounded-snapshot invariant at the compiler boundary: a non-literal capacity
30
+ * would make the backing ring's size — and therefore its snapshot size and
31
+ * per-tick rollback cost — non-deterministic.
32
+ *
33
+ * Capacity resolution reuses `resolveIndexUpperBound` + `collectConstNumberEnv`
34
+ * (the same machinery that sizes a series index), so a bare numeric literal,
35
+ * a parenthesised / unary-`±` literal, and a `const` numeric-literal binding
36
+ * (`const K = 20; state.array(K)`) are all accepted; a `let`, an input, or any
37
+ * runtime expression resolves to `null` and is rejected.
38
+ *
39
+ * Two error codes:
40
+ * - `state-array-capacity-not-literal` — the capacity is missing or does not
41
+ * resolve to a compile-time number.
42
+ * - `state-array-capacity-exceeds-max` — the capacity resolves but is `<= 0`,
43
+ * non-integer, or `> MAX_STATE_ARRAY_CAPACITY`.
44
+ *
45
+ * The walk runs on the **original** AST (positions match the user's source,
46
+ * and — running pre-injection — the capacity is `node.arguments[0]`, before
47
+ * the slot-id literal is injected as the leading argument). The element-access
48
+ * form `state["array"](cap)` is rejected upstream as
49
+ * `stateful-call-element-access` and never matches `"state.array"` here, so it
50
+ * is not double-reported. A `state.array(...)` inside a loop additionally
51
+ * errors `stateful-call-inside-loop`; both passes are independent.
52
+ *
53
+ * @since 1.3
54
+ * @example
55
+ * // const diagnostics = runStateArrayCapacity(
56
+ * // sourceFile, checker, "demo.chart.ts",
57
+ * // );
58
+ * const fn: typeof runStateArrayCapacity = runStateArrayCapacity;
59
+ * void fn;
60
+ */
61
+ export function runStateArrayCapacity(sourceFile, checker, sourcePath) {
62
+ const diagnostics = [];
63
+ const visit = (node) => {
64
+ if (ts.isCallExpression(node) && resolveCalleeName(node, checker) === "state.array") {
65
+ const capacity = node.arguments[0];
66
+ if (capacity === undefined) {
67
+ diagnostics.push(createDiagnostic({
68
+ severity: "error",
69
+ code: "state-array-capacity-not-literal",
70
+ message: "`state.array` requires a numeric-literal capacity (a `const` numeric binding is accepted).",
71
+ file: sourcePath,
72
+ node,
73
+ sourceFile,
74
+ }));
75
+ }
76
+ else {
77
+ const constEnv = collectConstNumberEnv(capacity, sourceFile);
78
+ const bound = resolveIndexUpperBound(capacity, node, { constEnv, checker });
79
+ if (bound === null) {
80
+ diagnostics.push(createDiagnostic({
81
+ severity: "error",
82
+ code: "state-array-capacity-not-literal",
83
+ message: "`state.array` capacity must be a numeric literal (a `const` numeric binding is accepted), not a runtime value.",
84
+ file: sourcePath,
85
+ node: capacity,
86
+ sourceFile,
87
+ }));
88
+ }
89
+ else if (bound <= 0 ||
90
+ !Number.isInteger(bound) ||
91
+ bound > MAX_STATE_ARRAY_CAPACITY) {
92
+ diagnostics.push(createDiagnostic({
93
+ severity: "error",
94
+ code: "state-array-capacity-exceeds-max",
95
+ message: `\`state.array\` capacity must be a positive integer in 1..${MAX_STATE_ARRAY_CAPACITY}; got ${bound}.`,
96
+ file: sourcePath,
97
+ node: capacity,
98
+ sourceFile,
99
+ }));
100
+ }
101
+ }
102
+ }
103
+ ts.forEachChild(node, visit);
104
+ };
105
+ ts.forEachChild(sourceFile, visit);
106
+ return Object.freeze(diagnostics.slice());
107
+ }
108
+ //# sourceMappingURL=stateArrayCapacity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stateArrayCapacity.js","sourceRoot":"","sources":["../../src/analysis/stateArrayCapacity.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+DAA+D;AAE/D,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAA0B,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEvF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,OAAO,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,qBAAqB,CACjC,UAAyB,EACzB,OAAuB,EACvB,UAAkB;IAElB,MAAM,WAAW,GAAwB,EAAE,CAAC;IAE5C,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;QAClC,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,iBAAiB,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,aAAa,EAAE,CAAC;YAClF,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YACnC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBACzB,WAAW,CAAC,IAAI,CACZ,gBAAgB,CAAC;oBACb,QAAQ,EAAE,OAAO;oBACjB,IAAI,EAAE,kCAAkC;oBACxC,OAAO,EACH,4FAA4F;oBAChG,IAAI,EAAE,UAAU;oBAChB,IAAI;oBACJ,UAAU;iBACb,CAAC,CACL,CAAC;YACN,CAAC;iBAAM,CAAC;gBACJ,MAAM,QAAQ,GAAG,qBAAqB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;gBAC7D,MAAM,KAAK,GAAG,sBAAsB,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;gBAC5E,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;oBACjB,WAAW,CAAC,IAAI,CACZ,gBAAgB,CAAC;wBACb,QAAQ,EAAE,OAAO;wBACjB,IAAI,EAAE,kCAAkC;wBACxC,OAAO,EACH,gHAAgH;wBACpH,IAAI,EAAE,UAAU;wBAChB,IAAI,EAAE,QAAQ;wBACd,UAAU;qBACb,CAAC,CACL,CAAC;gBACN,CAAC;qBAAM,IACH,KAAK,IAAI,CAAC;oBACV,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC;oBACxB,KAAK,GAAG,wBAAwB,EAClC,CAAC;oBACC,WAAW,CAAC,IAAI,CACZ,gBAAgB,CAAC;wBACb,QAAQ,EAAE,OAAO;wBACjB,IAAI,EAAE,kCAAkC;wBACxC,OAAO,EAAE,6DAA6D,wBAAwB,SAAS,KAAK,GAAG;wBAC/G,IAAI,EAAE,UAAU;wBAChB,IAAI,EAAE,QAAQ;wBACd,UAAU;qBACb,CAAC,CACL,CAAC;gBACN,CAAC;YACL,CAAC;QACL,CAAC;QACD,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACjC,CAAC,CAAC;IAEF,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC;AAC9C,CAAC","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nimport ts from \"typescript\";\n\nimport { type CompileDiagnostic, createDiagnostic } from \"../diagnostics.js\";\nimport { resolveCalleeName } from \"../transformers/resolveCallee.js\";\nimport { collectConstNumberEnv, resolveIndexUpperBound } from \"./resolveIndexBound.js\";\n\n/**\n * The largest `capacity` a `state.array<T>(capacity)` allocation may request.\n * The collection is bounded-execution-safe only because its size is fixed at\n * compile time: a hard ceiling caps memory, caps the JSON snapshot size, and —\n * crucially — caps the per-tick two-ring `Float64Array` copy the runtime does\n * on every tick rollback. `100_000` is generous (the dominant rolling-window /\n * event-log cases are ≤ a few hundred) while still keeping the snapshot and\n * the per-tick copy bounded.\n *\n * @since 1.3\n * @stable\n * @example\n * // state.array<number>(20) → 20 <= MAX_STATE_ARRAY_CAPACITY (OK)\n * // state.array<number>(1_000_000) → exceeds the cap (error)\n * const cap: number = MAX_STATE_ARRAY_CAPACITY;\n * void cap;\n */\nexport const MAX_STATE_ARRAY_CAPACITY = 100_000;\n\n/**\n * Walk the source file and flag every `state.array<T>(capacity)` allocation\n * whose `capacity` argument is not a compile-time-resolvable positive integer\n * within `MAX_STATE_ARRAY_CAPACITY`. This pins the bounded-execution +\n * bounded-snapshot invariant at the compiler boundary: a non-literal capacity\n * would make the backing ring's size — and therefore its snapshot size and\n * per-tick rollback cost — non-deterministic.\n *\n * Capacity resolution reuses `resolveIndexUpperBound` + `collectConstNumberEnv`\n * (the same machinery that sizes a series index), so a bare numeric literal,\n * a parenthesised / unary-`±` literal, and a `const` numeric-literal binding\n * (`const K = 20; state.array(K)`) are all accepted; a `let`, an input, or any\n * runtime expression resolves to `null` and is rejected.\n *\n * Two error codes:\n * - `state-array-capacity-not-literal` — the capacity is missing or does not\n * resolve to a compile-time number.\n * - `state-array-capacity-exceeds-max` — the capacity resolves but is `<= 0`,\n * non-integer, or `> MAX_STATE_ARRAY_CAPACITY`.\n *\n * The walk runs on the **original** AST (positions match the user's source,\n * and — running pre-injection — the capacity is `node.arguments[0]`, before\n * the slot-id literal is injected as the leading argument). The element-access\n * form `state[\"array\"](cap)` is rejected upstream as\n * `stateful-call-element-access` and never matches `\"state.array\"` here, so it\n * is not double-reported. A `state.array(...)` inside a loop additionally\n * errors `stateful-call-inside-loop`; both passes are independent.\n *\n * @since 1.3\n * @example\n * // const diagnostics = runStateArrayCapacity(\n * // sourceFile, checker, \"demo.chart.ts\",\n * // );\n * const fn: typeof runStateArrayCapacity = runStateArrayCapacity;\n * void fn;\n */\nexport function runStateArrayCapacity(\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker,\n sourcePath: string,\n): ReadonlyArray<CompileDiagnostic> {\n const diagnostics: CompileDiagnostic[] = [];\n\n const visit = (node: ts.Node): void => {\n if (ts.isCallExpression(node) && resolveCalleeName(node, checker) === \"state.array\") {\n const capacity = node.arguments[0];\n if (capacity === undefined) {\n diagnostics.push(\n createDiagnostic({\n severity: \"error\",\n code: \"state-array-capacity-not-literal\",\n message:\n \"`state.array` requires a numeric-literal capacity (a `const` numeric binding is accepted).\",\n file: sourcePath,\n node,\n sourceFile,\n }),\n );\n } else {\n const constEnv = collectConstNumberEnv(capacity, sourceFile);\n const bound = resolveIndexUpperBound(capacity, node, { constEnv, checker });\n if (bound === null) {\n diagnostics.push(\n createDiagnostic({\n severity: \"error\",\n code: \"state-array-capacity-not-literal\",\n message:\n \"`state.array` capacity must be a numeric literal (a `const` numeric binding is accepted), not a runtime value.\",\n file: sourcePath,\n node: capacity,\n sourceFile,\n }),\n );\n } else if (\n bound <= 0 ||\n !Number.isInteger(bound) ||\n bound > MAX_STATE_ARRAY_CAPACITY\n ) {\n diagnostics.push(\n createDiagnostic({\n severity: \"error\",\n code: \"state-array-capacity-exceeds-max\",\n message: `\\`state.array\\` capacity must be a positive integer in 1..${MAX_STATE_ARRAY_CAPACITY}; got ${bound}.`,\n file: sourcePath,\n node: capacity,\n sourceFile,\n }),\n );\n }\n }\n }\n ts.forEachChild(node, visit);\n };\n\n ts.forEachChild(sourceFile, visit);\n return Object.freeze(diagnostics.slice());\n}\n"]}
@@ -0,0 +1,25 @@
1
+ import ts from "typescript";
2
+ import { type CompileDiagnostic } from "../diagnostics.js";
3
+ /**
4
+ * Validate that a `request.security({ interval }, (bar) => …)` expression
5
+ * callback references only its `bar` parameter (and locals derived from it),
6
+ * the ambient `ta` namespace, `inputs`, safe `Math.*` globals, and literal
7
+ * constants. Any other free identifier is a captured outer binding — it would
8
+ * smuggle the main-timeline clock into the higher-timeframe expression — and
9
+ * is rejected with `request-security-expr-captures-local`.
10
+ *
11
+ * Function / arrow expressions nested deeper inside the callback are out of
12
+ * the v1 subset and rejected too (keeps the expression unit flat), as is a
13
+ * `this` reference. Parameter default initialisers are walked alongside the
14
+ * body so a default that captures an outer binding is flagged too.
15
+ *
16
+ * @since 0.7
17
+ * @stable
18
+ * @example
19
+ * // Inside extractRequestAnalysis, once per expression callsite:
20
+ * // validateSecurityExpr(callback, checker, diagnostics, sourcePath);
21
+ * const fn: typeof validateSecurityExpr = validateSecurityExpr;
22
+ * void fn;
23
+ */
24
+ export declare function validateSecurityExpr(callback: ts.ArrowFunction | ts.FunctionExpression, checker: ts.TypeChecker, diagnostics: CompileDiagnostic[], sourcePath: string): void;
25
+ //# sourceMappingURL=validateSecurityExpr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateSecurityExpr.d.ts","sourceRoot":"","sources":["../../src/analysis/validateSecurityExpr.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAG7E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAChC,QAAQ,EAAE,EAAE,CAAC,aAAa,GAAG,EAAE,CAAC,kBAAkB,EAClD,OAAO,EAAE,EAAE,CAAC,WAAW,EACvB,WAAW,EAAE,iBAAiB,EAAE,EAChC,UAAU,EAAE,MAAM,GACnB,IAAI,CA2DN"}