@invinite-org/chartlang-compiler 1.2.1 → 1.3.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 (50) hide show
  1. package/CHANGELOG.md +246 -0
  2. package/dist/analysis/extractMaxLookback.d.ts +2 -1
  3. package/dist/analysis/extractMaxLookback.d.ts.map +1 -1
  4. package/dist/analysis/extractMaxLookback.js +90 -6
  5. package/dist/analysis/extractMaxLookback.js.map +1 -1
  6. package/dist/analysis/extractRequestedIntervals.d.ts +43 -1
  7. package/dist/analysis/extractRequestedIntervals.d.ts.map +1 -1
  8. package/dist/analysis/extractRequestedIntervals.js +95 -10
  9. package/dist/analysis/extractRequestedIntervals.js.map +1 -1
  10. package/dist/analysis/forbiddenConstructs.d.ts.map +1 -1
  11. package/dist/analysis/forbiddenConstructs.js +2 -41
  12. package/dist/analysis/forbiddenConstructs.js.map +1 -1
  13. package/dist/analysis/index.d.ts +3 -1
  14. package/dist/analysis/index.d.ts.map +1 -1
  15. package/dist/analysis/index.js +2 -1
  16. package/dist/analysis/index.js.map +1 -1
  17. package/dist/analysis/loopBounds.d.ts +91 -0
  18. package/dist/analysis/loopBounds.d.ts.map +1 -0
  19. package/dist/analysis/loopBounds.js +132 -0
  20. package/dist/analysis/loopBounds.js.map +1 -0
  21. package/dist/analysis/resolveIndexBound.d.ts +73 -0
  22. package/dist/analysis/resolveIndexBound.d.ts.map +1 -0
  23. package/dist/analysis/resolveIndexBound.js +336 -0
  24. package/dist/analysis/resolveIndexBound.js.map +1 -0
  25. package/dist/analysis/validateSecurityExpr.d.ts +25 -0
  26. package/dist/analysis/validateSecurityExpr.d.ts.map +1 -0
  27. package/dist/analysis/validateSecurityExpr.js +154 -0
  28. package/dist/analysis/validateSecurityExpr.js.map +1 -0
  29. package/dist/api.d.ts.map +1 -1
  30. package/dist/api.js +13 -3
  31. package/dist/api.js.map +1 -1
  32. package/dist/diagnostics.d.ts +4 -2
  33. package/dist/diagnostics.d.ts.map +1 -1
  34. package/dist/diagnostics.js.map +1 -1
  35. package/dist/manifest.d.ts +2 -1
  36. package/dist/manifest.d.ts.map +1 -1
  37. package/dist/manifest.js +7 -0
  38. package/dist/manifest.js.map +1 -1
  39. package/dist/program.d.ts.map +1 -1
  40. package/dist/program.js +91 -14
  41. package/dist/program.js.map +1 -1
  42. package/dist/transformers/callsiteIdInjection.d.ts +21 -0
  43. package/dist/transformers/callsiteIdInjection.d.ts.map +1 -1
  44. package/dist/transformers/callsiteIdInjection.js +26 -3
  45. package/dist/transformers/callsiteIdInjection.js.map +1 -1
  46. package/dist/transformers/resolveCallee.d.ts +21 -0
  47. package/dist/transformers/resolveCallee.d.ts.map +1 -1
  48. package/dist/transformers/resolveCallee.js +14 -1
  49. package/dist/transformers/resolveCallee.js.map +1 -1
  50. package/package.json +2 -2
@@ -2,34 +2,119 @@
2
2
  // See the LICENSE file in the repo root for full license text.
3
3
  import ts from "typescript";
4
4
  import { createDiagnostic } from "../diagnostics.js";
5
+ import { callsiteIdFor } from "../transformers/callsiteIdInjection.js";
5
6
  import { resolveCalleeName } from "../transformers/resolveCallee.js";
7
+ import { validateSecurityExpr } from "./validateSecurityExpr.js";
6
8
  /**
7
9
  * Walk a script's AST and collect every static `interval` argument to
8
- * `request.security({ interval: ... })` and `request.lowerTf(...)`. Dynamic
9
- * arguments emit `request-security-interval-not-literal` (for `request.security`)
10
- * or `request-lower-tf-interval-not-literal` (for `request.lowerTf`) and are
11
- * excluded.
10
+ * `request.security({ interval: ... })` and `request.lowerTf(...)`, plus every
11
+ * `request.security` *expression* callsite (a second arrow/function argument).
12
+ * Dynamic intervals emit `request-security-interval-not-literal` (for
13
+ * `request.security`) or `request-lower-tf-interval-not-literal` (for
14
+ * `request.lowerTf`) and are excluded.
12
15
  *
13
- * @since 0.4
16
+ * Each expression callsite is recorded as a {@link SecurityExpressionDescriptor}
17
+ * keyed by the same `slotId` the callsite-id transformer injects (via the
18
+ * shared `callsiteIdFor` helper) so the runtime can match the manifest entry
19
+ * to the inlined callback. When `validateExpressions` is `true`, each callback
20
+ * is also run through {@link validateSecurityExpr}, pushing
21
+ * `request-security-expr-captures-local` for any out-of-subset reference.
22
+ *
23
+ * @since 0.7
24
+ * @stable
14
25
  * @example
15
- * // const intervals = extractRequestedIntervals(sf, checker, inputs, diagnostics);
16
- * // intervals === ["1D", "5m"];
17
- * const fn: typeof extractRequestedIntervals = extractRequestedIntervals;
26
+ * // const { intervals, securityExpressions } =
27
+ * // extractRequestAnalysis(sf, checker, inputs, diagnostics, path, true);
28
+ * const fn: typeof extractRequestAnalysis = extractRequestAnalysis;
18
29
  * void fn;
19
30
  */
20
- export function extractRequestedIntervals(sourceFile, checker, inputs, diagnostics, sourcePath = sourceFile.fileName) {
31
+ export function extractRequestAnalysis(sourceFile, checker, inputs, diagnostics, sourcePath = sourceFile.fileName, validateExpressions = false) {
21
32
  const intervals = new Set();
33
+ const securityExpressions = [];
22
34
  const visit = (node) => {
23
35
  if (ts.isCallExpression(node)) {
24
36
  const calleeName = resolveCalleeName(node, checker);
25
37
  if (calleeName === "request.security" || calleeName === "request.lowerTf") {
26
38
  readRequestInterval(node, calleeName, sourceFile, sourcePath, inputs, diagnostics, intervals);
27
39
  }
40
+ if (calleeName === "request.security") {
41
+ readSecurityExpression(node, sourceFile, sourcePath, checker, diagnostics, validateExpressions, securityExpressions);
42
+ }
28
43
  }
29
44
  ts.forEachChild(node, visit);
30
45
  };
31
46
  ts.forEachChild(sourceFile, visit);
32
- return Object.freeze(Array.from(intervals).sort());
47
+ securityExpressions.sort((a, b) => a.slotId.localeCompare(b.slotId));
48
+ return Object.freeze({
49
+ intervals: Object.freeze(Array.from(intervals).sort()),
50
+ securityExpressions: Object.freeze(securityExpressions.slice()),
51
+ });
52
+ }
53
+ /**
54
+ * Walk a script's AST and collect every static `interval` argument to
55
+ * `request.security({ interval: ... })` and `request.lowerTf(...)`. Dynamic
56
+ * arguments emit `request-security-interval-not-literal` (for `request.security`)
57
+ * or `request-lower-tf-interval-not-literal` (for `request.lowerTf`) and are
58
+ * excluded. Thin delegate over {@link extractRequestAnalysis} kept for callers
59
+ * that only need the interval list.
60
+ *
61
+ * @since 0.4
62
+ * @example
63
+ * // const intervals = extractRequestedIntervals(sf, checker, inputs, diagnostics);
64
+ * // intervals === ["1D", "5m"];
65
+ * const fn: typeof extractRequestedIntervals = extractRequestedIntervals;
66
+ * void fn;
67
+ */
68
+ export function extractRequestedIntervals(sourceFile, checker, inputs, diagnostics, sourcePath = sourceFile.fileName) {
69
+ return extractRequestAnalysis(sourceFile, checker, inputs, diagnostics, sourcePath).intervals;
70
+ }
71
+ /**
72
+ * Detect and record a `request.security` expression callsite — a second
73
+ * argument that is an arrow or function expression. Mints the descriptor's
74
+ * `slotId` via `callsiteIdFor` (lockstep with the injector), reads the literal
75
+ * `interval` and the callback's single parameter name, and — when
76
+ * `validate` — runs the capture check. A callsite whose interval is not a
77
+ * compile-time literal already emitted `request-security-interval-not-literal`
78
+ * via `readRequestInterval`; it is skipped here (no descriptor).
79
+ */
80
+ function readSecurityExpression(call, sourceFile, sourcePath, checker, diagnostics, validate, out) {
81
+ const callback = call.arguments[1];
82
+ if (callback === undefined ||
83
+ !(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
84
+ return;
85
+ }
86
+ if (validate) {
87
+ validateSecurityExpr(callback, checker, diagnostics, sourcePath);
88
+ }
89
+ const interval = readLiteralInterval(call);
90
+ if (interval === null)
91
+ return;
92
+ const firstParam = callback.parameters[0];
93
+ const paramName = firstParam !== undefined && ts.isIdentifier(firstParam.name) ? firstParam.name.text : "";
94
+ out.push(Object.freeze({
95
+ slotId: callsiteIdFor(sourceFile, call, sourcePath),
96
+ interval,
97
+ paramName,
98
+ }));
99
+ }
100
+ /**
101
+ * Read the literal `interval` string off a `request.security` call's opts
102
+ * object, or `null` when it is absent or non-literal. Only string-literal
103
+ * intervals key an expression unit; an `input.enum` interval expands to
104
+ * multiple intervals for the requested-interval list but cannot anchor a
105
+ * single expression clock, so it is treated as non-literal here.
106
+ */
107
+ function readLiteralInterval(call) {
108
+ const opts = call.arguments[0];
109
+ if (opts === undefined || !ts.isObjectLiteralExpression(opts))
110
+ return null;
111
+ const intervalProperty = opts.properties
112
+ .filter(ts.isPropertyAssignment)
113
+ .find((property) => ts.isIdentifier(property.name) && property.name.text === "interval");
114
+ if (intervalProperty === undefined)
115
+ return null;
116
+ const initializer = intervalProperty.initializer;
117
+ return ts.isStringLiteral(initializer) ? initializer.text : null;
33
118
  }
34
119
  function readRequestInterval(call, calleeName, sourceFile, sourcePath, inputs, diagnostics, intervals) {
35
120
  const opts = call.arguments[0];
@@ -1 +1 @@
1
- {"version":3,"file":"extractRequestedIntervals.js","sourceRoot":"","sources":["../../src/analysis/extractRequestedIntervals.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 { type CompileDiagnostic, createDiagnostic } from \"../diagnostics.js\";\nimport { resolveCalleeName } from \"../transformers/resolveCallee.js\";\nimport type { ExtractedDescriptor } from \"./extractInputs.js\";\n\n/**\n * Walk a script's AST and collect every static `interval` argument to\n * `request.security({ interval: ... })` and `request.lowerTf(...)`. Dynamic\n * arguments emit `request-security-interval-not-literal` (for `request.security`)\n * or `request-lower-tf-interval-not-literal` (for `request.lowerTf`) and are\n * excluded.\n *\n * @since 0.4\n * @example\n * // const intervals = extractRequestedIntervals(sf, checker, inputs, diagnostics);\n * // intervals === [\"1D\", \"5m\"];\n * const fn: typeof extractRequestedIntervals = extractRequestedIntervals;\n * void fn;\n */\nexport function extractRequestedIntervals(\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n diagnostics: CompileDiagnostic[],\n sourcePath: string = sourceFile.fileName,\n): ReadonlyArray<string> {\n const intervals = new Set<string>();\n\n const visit = (node: ts.Node): void => {\n if (ts.isCallExpression(node)) {\n const calleeName = resolveCalleeName(node, checker);\n if (calleeName === \"request.security\" || calleeName === \"request.lowerTf\") {\n readRequestInterval(\n node,\n calleeName,\n sourceFile,\n sourcePath,\n inputs,\n diagnostics,\n intervals,\n );\n }\n }\n ts.forEachChild(node, visit);\n };\n\n ts.forEachChild(sourceFile, visit);\n return Object.freeze(Array.from(intervals).sort());\n}\n\nfunction readRequestInterval(\n call: ts.CallExpression,\n calleeName: \"request.security\" | \"request.lowerTf\",\n sourceFile: ts.SourceFile,\n sourcePath: string,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n diagnostics: CompileDiagnostic[],\n intervals: Set<string>,\n): void {\n const opts = call.arguments[0];\n if (opts === undefined || !ts.isObjectLiteralExpression(opts)) return;\n const intervalProperty = opts.properties\n .filter(ts.isPropertyAssignment)\n .find((property) => ts.isIdentifier(property.name) && property.name.text === \"interval\");\n if (intervalProperty === undefined) return;\n\n const initializer = intervalProperty.initializer;\n if (ts.isStringLiteral(initializer)) {\n intervals.add(initializer.text);\n return;\n }\n\n const enumOptions = getInputsEnumOptions(initializer, inputs);\n if (enumOptions !== null) {\n for (const option of enumOptions) intervals.add(option);\n return;\n }\n\n diagnostics.push(\n createDiagnostic({\n severity: \"error\",\n code:\n calleeName === \"request.lowerTf\"\n ? \"request-lower-tf-interval-not-literal\"\n : \"request-security-interval-not-literal\",\n message: `${calleeName}({ interval }) must be a string literal or input.enum value`,\n file: sourcePath,\n node: initializer,\n sourceFile,\n }),\n );\n}\n\nfunction getInputsEnumOptions(\n expr: ts.Expression,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n): ReadonlyArray<string> | null {\n if (\n !ts.isPropertyAccessExpression(expr) ||\n !ts.isIdentifier(expr.expression) ||\n expr.expression.text !== \"inputs\"\n ) {\n return null;\n }\n const descriptor = inputs[expr.name.text];\n if (descriptor === undefined || descriptor.kind !== \"enum\") return null;\n const options = descriptor.options;\n if (!Array.isArray(options)) return null;\n const strings: string[] = [];\n for (const option of options) {\n if (typeof option !== \"string\") return null;\n strings.push(option);\n }\n return Object.freeze(strings);\n}\n"]}
1
+ {"version":3,"file":"extractRequestedIntervals.js","sourceRoot":"","sources":["../../src/analysis/extractRequestedIntervals.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 type { SecurityExpressionDescriptor } from \"@invinite-org/chartlang-core\";\nimport ts from \"typescript\";\n\nimport { type CompileDiagnostic, createDiagnostic } from \"../diagnostics.js\";\nimport { callsiteIdFor } from \"../transformers/callsiteIdInjection.js\";\nimport { resolveCalleeName } from \"../transformers/resolveCallee.js\";\nimport type { ExtractedDescriptor } from \"./extractInputs.js\";\nimport { validateSecurityExpr } from \"./validateSecurityExpr.js\";\n\n/**\n * Combined result of the `request.*` analysis pass: the sorted, deduped list\n * of requested intervals plus one {@link SecurityExpressionDescriptor} per\n * `request.security({ interval }, (bar) => …)` expression callsite (sorted by\n * `slotId`).\n *\n * @since 0.7\n * @stable\n * @example\n * const r: RequestAnalysis = { intervals: [\"1W\"], securityExpressions: [] };\n * void r;\n */\nexport type RequestAnalysis = Readonly<{\n intervals: ReadonlyArray<string>;\n securityExpressions: ReadonlyArray<SecurityExpressionDescriptor>;\n}>;\n\n/**\n * Walk a script's AST and collect every static `interval` argument to\n * `request.security({ interval: ... })` and `request.lowerTf(...)`, plus every\n * `request.security` *expression* callsite (a second arrow/function argument).\n * Dynamic intervals emit `request-security-interval-not-literal` (for\n * `request.security`) or `request-lower-tf-interval-not-literal` (for\n * `request.lowerTf`) and are excluded.\n *\n * Each expression callsite is recorded as a {@link SecurityExpressionDescriptor}\n * keyed by the same `slotId` the callsite-id transformer injects (via the\n * shared `callsiteIdFor` helper) so the runtime can match the manifest entry\n * to the inlined callback. When `validateExpressions` is `true`, each callback\n * is also run through {@link validateSecurityExpr}, pushing\n * `request-security-expr-captures-local` for any out-of-subset reference.\n *\n * @since 0.7\n * @stable\n * @example\n * // const { intervals, securityExpressions } =\n * // extractRequestAnalysis(sf, checker, inputs, diagnostics, path, true);\n * const fn: typeof extractRequestAnalysis = extractRequestAnalysis;\n * void fn;\n */\nexport function extractRequestAnalysis(\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n diagnostics: CompileDiagnostic[],\n sourcePath: string = sourceFile.fileName,\n validateExpressions = false,\n): RequestAnalysis {\n const intervals = new Set<string>();\n const securityExpressions: SecurityExpressionDescriptor[] = [];\n\n const visit = (node: ts.Node): void => {\n if (ts.isCallExpression(node)) {\n const calleeName = resolveCalleeName(node, checker);\n if (calleeName === \"request.security\" || calleeName === \"request.lowerTf\") {\n readRequestInterval(\n node,\n calleeName,\n sourceFile,\n sourcePath,\n inputs,\n diagnostics,\n intervals,\n );\n }\n if (calleeName === \"request.security\") {\n readSecurityExpression(\n node,\n sourceFile,\n sourcePath,\n checker,\n diagnostics,\n validateExpressions,\n securityExpressions,\n );\n }\n }\n ts.forEachChild(node, visit);\n };\n\n ts.forEachChild(sourceFile, visit);\n securityExpressions.sort((a, b) => a.slotId.localeCompare(b.slotId));\n return Object.freeze({\n intervals: Object.freeze(Array.from(intervals).sort()),\n securityExpressions: Object.freeze(securityExpressions.slice()),\n });\n}\n\n/**\n * Walk a script's AST and collect every static `interval` argument to\n * `request.security({ interval: ... })` and `request.lowerTf(...)`. Dynamic\n * arguments emit `request-security-interval-not-literal` (for `request.security`)\n * or `request-lower-tf-interval-not-literal` (for `request.lowerTf`) and are\n * excluded. Thin delegate over {@link extractRequestAnalysis} kept for callers\n * that only need the interval list.\n *\n * @since 0.4\n * @example\n * // const intervals = extractRequestedIntervals(sf, checker, inputs, diagnostics);\n * // intervals === [\"1D\", \"5m\"];\n * const fn: typeof extractRequestedIntervals = extractRequestedIntervals;\n * void fn;\n */\nexport function extractRequestedIntervals(\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n diagnostics: CompileDiagnostic[],\n sourcePath: string = sourceFile.fileName,\n): ReadonlyArray<string> {\n return extractRequestAnalysis(sourceFile, checker, inputs, diagnostics, sourcePath).intervals;\n}\n\n/**\n * Detect and record a `request.security` expression callsite — a second\n * argument that is an arrow or function expression. Mints the descriptor's\n * `slotId` via `callsiteIdFor` (lockstep with the injector), reads the literal\n * `interval` and the callback's single parameter name, and — when\n * `validate` — runs the capture check. A callsite whose interval is not a\n * compile-time literal already emitted `request-security-interval-not-literal`\n * via `readRequestInterval`; it is skipped here (no descriptor).\n */\nfunction readSecurityExpression(\n call: ts.CallExpression,\n sourceFile: ts.SourceFile,\n sourcePath: string,\n checker: ts.TypeChecker,\n diagnostics: CompileDiagnostic[],\n validate: boolean,\n out: SecurityExpressionDescriptor[],\n): void {\n const callback = call.arguments[1];\n if (\n callback === undefined ||\n !(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))\n ) {\n return;\n }\n if (validate) {\n validateSecurityExpr(callback, checker, diagnostics, sourcePath);\n }\n const interval = readLiteralInterval(call);\n if (interval === null) return;\n const firstParam = callback.parameters[0];\n const paramName =\n firstParam !== undefined && ts.isIdentifier(firstParam.name) ? firstParam.name.text : \"\";\n out.push(\n Object.freeze({\n slotId: callsiteIdFor(sourceFile, call, sourcePath),\n interval,\n paramName,\n }),\n );\n}\n\n/**\n * Read the literal `interval` string off a `request.security` call's opts\n * object, or `null` when it is absent or non-literal. Only string-literal\n * intervals key an expression unit; an `input.enum` interval expands to\n * multiple intervals for the requested-interval list but cannot anchor a\n * single expression clock, so it is treated as non-literal here.\n */\nfunction readLiteralInterval(call: ts.CallExpression): string | null {\n const opts = call.arguments[0];\n if (opts === undefined || !ts.isObjectLiteralExpression(opts)) return null;\n const intervalProperty = opts.properties\n .filter(ts.isPropertyAssignment)\n .find((property) => ts.isIdentifier(property.name) && property.name.text === \"interval\");\n if (intervalProperty === undefined) return null;\n const initializer = intervalProperty.initializer;\n return ts.isStringLiteral(initializer) ? initializer.text : null;\n}\n\nfunction readRequestInterval(\n call: ts.CallExpression,\n calleeName: \"request.security\" | \"request.lowerTf\",\n sourceFile: ts.SourceFile,\n sourcePath: string,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n diagnostics: CompileDiagnostic[],\n intervals: Set<string>,\n): void {\n const opts = call.arguments[0];\n if (opts === undefined || !ts.isObjectLiteralExpression(opts)) return;\n const intervalProperty = opts.properties\n .filter(ts.isPropertyAssignment)\n .find((property) => ts.isIdentifier(property.name) && property.name.text === \"interval\");\n if (intervalProperty === undefined) return;\n\n const initializer = intervalProperty.initializer;\n if (ts.isStringLiteral(initializer)) {\n intervals.add(initializer.text);\n return;\n }\n\n const enumOptions = getInputsEnumOptions(initializer, inputs);\n if (enumOptions !== null) {\n for (const option of enumOptions) intervals.add(option);\n return;\n }\n\n diagnostics.push(\n createDiagnostic({\n severity: \"error\",\n code:\n calleeName === \"request.lowerTf\"\n ? \"request-lower-tf-interval-not-literal\"\n : \"request-security-interval-not-literal\",\n message: `${calleeName}({ interval }) must be a string literal or input.enum value`,\n file: sourcePath,\n node: initializer,\n sourceFile,\n }),\n );\n}\n\nfunction getInputsEnumOptions(\n expr: ts.Expression,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n): ReadonlyArray<string> | null {\n if (\n !ts.isPropertyAccessExpression(expr) ||\n !ts.isIdentifier(expr.expression) ||\n expr.expression.text !== \"inputs\"\n ) {\n return null;\n }\n const descriptor = inputs[expr.name.text];\n if (descriptor === undefined || descriptor.kind !== \"enum\") return null;\n const options = descriptor.options;\n if (!Array.isArray(options)) return null;\n const strings: string[] = [];\n for (const option of options) {\n if (typeof option !== \"string\") return null;\n strings.push(option);\n }\n return Object.freeze(strings);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"forbiddenConstructs.d.ts","sourceRoot":"","sources":["../../src/analysis/forbiddenConstructs.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAwB7E;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CAClC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,UAAU,EAAE,MAAM,GACnB,aAAa,CAAC,iBAAiB,CAAC,CAoLlC"}
1
+ {"version":3,"file":"forbiddenConstructs.d.ts","sourceRoot":"","sources":["../../src/analysis/forbiddenConstructs.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAkB7E;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CAClC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,UAAU,EAAE,MAAM,GACnB,aAAa,CAAC,iBAAiB,CAAC,CA8JlC"}
@@ -2,6 +2,7 @@
2
2
  // See the LICENSE file in the repo root for full license text.
3
3
  import ts from "typescript";
4
4
  import { createDiagnostic } from "../diagnostics.js";
5
+ import { parseBoundedForLoop } from "./loopBounds.js";
5
6
  const HOSTILE_GLOBAL_NAMES = new Set([
6
7
  "fetch",
7
8
  "setTimeout",
@@ -16,12 +17,6 @@ const HOSTILE_GLOBAL_NAMES = new Set([
16
17
  // this helper; user scripts must not name-collide with the slot.
17
18
  "__chartlang_depOutput",
18
19
  ]);
19
- const COMPARISON_OPS = new Set([
20
- ts.SyntaxKind.LessThanToken,
21
- ts.SyntaxKind.LessThanEqualsToken,
22
- ts.SyntaxKind.GreaterThanToken,
23
- ts.SyntaxKind.GreaterThanEqualsToken,
24
- ]);
25
20
  /**
26
21
  * Walk the source file and emit a diagnostic for every forbidden construct:
27
22
  *
@@ -51,40 +46,6 @@ export function runForbiddenConstructs(sourceFile, sourcePath) {
51
46
  sourceFile,
52
47
  }));
53
48
  }
54
- function checkForStatement(node) {
55
- const init = node.initializer;
56
- const condition = node.condition;
57
- const incrementor = node.incrementor;
58
- if (!init || !condition || !incrementor)
59
- return false;
60
- if (!ts.isVariableDeclarationList(init))
61
- return false;
62
- if (init.declarations.length !== 1)
63
- return false;
64
- const declaration = init.declarations[0];
65
- if (!declaration || !ts.isIdentifier(declaration.name))
66
- return false;
67
- const initializer = declaration.initializer;
68
- if (!initializer || !ts.isNumericLiteral(initializer))
69
- return false;
70
- if (!ts.isBinaryExpression(condition))
71
- return false;
72
- if (!COMPARISON_OPS.has(condition.operatorToken.kind))
73
- return false;
74
- if (!ts.isNumericLiteral(condition.right))
75
- return false;
76
- if (!ts.isIdentifier(condition.left))
77
- return false;
78
- if (condition.left.text !== declaration.name.text)
79
- return false;
80
- if (!ts.isPostfixUnaryExpression(incrementor))
81
- return false;
82
- if (!ts.isIdentifier(incrementor.operand))
83
- return false;
84
- if (incrementor.operand.text !== declaration.name.text)
85
- return false;
86
- return true;
87
- }
88
49
  function isInsideAncestor(node, predicate) {
89
50
  let current = node.parent;
90
51
  while (current) {
@@ -140,7 +101,7 @@ export function runForbiddenConstructs(sourceFile, sourcePath) {
140
101
  emit(node, "unbounded-loop", "`for…in` loops are not allowed.");
141
102
  }
142
103
  else if (ts.isForStatement(node)) {
143
- if (!checkForStatement(node)) {
104
+ if (parseBoundedForLoop(node) === null) {
144
105
  emit(node, "unbounded-loop", "`for` loops must use literal numeric bounds: for (let i = <num>; i </<= <num>; i++).");
145
106
  }
146
107
  }
@@ -1 +1 @@
1
- {"version":3,"file":"forbiddenConstructs.js","sourceRoot":"","sources":["../../src/analysis/forbiddenConstructs.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;AAE7E,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAAC;IACjC,OAAO;IACP,YAAY;IACZ,aAAa;IACb,gBAAgB;IAChB,SAAS;IACT,uBAAuB;IACvB,MAAM;IACN,MAAM;IACN,SAAS;IACT,oEAAoE;IACpE,iEAAiE;IACjE,uBAAuB;CAC1B,CAAC,CAAC;AAEH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAgB;IAC1C,EAAE,CAAC,UAAU,CAAC,aAAa;IAC3B,EAAE,CAAC,UAAU,CAAC,mBAAmB;IACjC,EAAE,CAAC,UAAU,CAAC,gBAAgB;IAC9B,EAAE,CAAC,UAAU,CAAC,sBAAsB;CACvC,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,sBAAsB,CAClC,UAAyB,EACzB,UAAkB;IAElB,MAAM,WAAW,GAAwB,EAAE,CAAC;IAE5C,SAAS,IAAI,CACT,IAAa,EACb,IAAmE,EACnE,OAAe;QAEf,WAAW,CAAC,IAAI,CACZ,gBAAgB,CAAC;YACb,QAAQ,EAAE,OAAO;YACjB,IAAI;YACJ,OAAO;YACP,IAAI,EAAE,UAAU;YAChB,IAAI;YACJ,UAAU;SACb,CAAC,CACL,CAAC;IACN,CAAC;IAED,SAAS,iBAAiB,CAAC,IAAqB;QAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC;QAC9B,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QACjC,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACrC,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,IAAI,CAAC,WAAW;YAAE,OAAO,KAAK,CAAC;QACtD,IAAI,CAAC,EAAE,CAAC,yBAAyB,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QACtD,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QACjD,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACzC,IAAI,CAAC,WAAW,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,WAAW,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QACrE,MAAM,WAAW,GAAG,WAAW,CAAC,WAAW,CAAC;QAC5C,IAAI,CAAC,WAAW,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,WAAW,CAAC;YAAE,OAAO,KAAK,CAAC;QACpE,IAAI,CAAC,EAAE,CAAC,kBAAkB,CAAC,SAAS,CAAC;YAAE,OAAO,KAAK,CAAC;QACpD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,aAAa,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QACpE,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,SAAS,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QACxD,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QACnD,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,KAAK,WAAW,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO,KAAK,CAAC;QAChE,IAAI,CAAC,EAAE,CAAC,wBAAwB,CAAC,WAAW,CAAC;YAAE,OAAO,KAAK,CAAC;QAC5D,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,WAAW,CAAC,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC;QACxD,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,KAAK,WAAW,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO,KAAK,CAAC;QACrE,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,SAAS,gBAAgB,CAAC,IAAa,EAAE,SAAuC;QAC5E,IAAI,OAAO,GAAwB,IAAI,CAAC,MAAM,CAAC;QAC/C,OAAO,OAAO,EAAE,CAAC;YACb,IAAI,SAAS,CAAC,OAAO,CAAC;gBAAE,OAAO,IAAI,CAAC;YACpC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,SAAS,iBAAiB,CAAC,MAAe,EAAE,IAAmB;QAC3D,MAAM,KAAK,GAAG,MAAqC,CAAC;QACpD,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QACtC,OAAO,CACH,EAAE,CAAC,qBAAqB,CAAC,MAAM,CAAC;YAChC,EAAE,CAAC,oBAAoB,CAAC,MAAM,CAAC;YAC/B,EAAE,CAAC,kBAAkB,CAAC,MAAM,CAAC;YAC7B,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC;YAC5B,EAAE,CAAC,qBAAqB,CAAC,MAAM,CAAC;YAChC,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC;YACtB,EAAE,CAAC,sBAAsB,CAAC,MAAM,CAAC;YACjC,EAAE,CAAC,sBAAsB,CAAC,MAAM,CAAC;YACjC,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC;YAC5B,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,CACjC,CAAC;IACN,CAAC;IAED,SAAS,qBAAqB,CAAC,IAAa;QACxC,IAAI,OAAO,GAAwB,IAAI,CAAC,MAAM,CAAC;QAC/C,OAAO,OAAO,EAAE,CAAC;YACb,IAAI,EAAE,CAAC,qBAAqB,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;gBACpD,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;YAC7B,CAAC;YACD,IAAI,EAAE,CAAC,qBAAqB,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrE,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;gBACxC,IACI,WAAW;oBACX,CAAC,EAAE,CAAC,oBAAoB,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC,EAC3E,CAAC;oBACC,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;gBAC7B,CAAC;YACL,CAAC;YACD,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;QAClC,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,gCAAgC,CAAC,CAAC;QACnE,CAAC;aAAM,IAAI,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,mCAAmC,CAAC,CAAC;QACtE,CAAC;aAAM,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,4CAA4C,CAAC,CAAC;QAC/E,CAAC;aAAM,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,iCAAiC,CAAC,CAAC;QACpE,CAAC;aAAM,IAAI,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3B,IAAI,CACA,IAAI,EACJ,gBAAgB,EAChB,sFAAsF,CACzF,CAAC;YACN,CAAC;QACL,CAAC;aAAM,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;YACnC,IAAI,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC9B,IAAI,UAAU,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;oBAC7B,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,wBAAwB,CAAC,CAAC;gBAC3D,CAAC;qBAAM,IAAI,UAAU,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;oBACvC,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,2BAA2B,CAAC,CAAC;gBAC9D,CAAC;YACL,CAAC;iBAAM,IAAI,UAAU,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,aAAa,EAAE,CAAC;gBACzD,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,oCAAoC,CAAC,CAAC;YACvE,CAAC;YACD,MAAM,YAAY,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;YACjD,IACI,YAAY,KAAK,IAAI;gBACrB,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC;gBAC3B,UAAU,CAAC,IAAI,KAAK,YAAY,EAClC,CAAC;gBACC,IAAI,CACA,IAAI,EACJ,uBAAuB,EACvB,4BAA4B,YAAY,oBAAoB,CAC/D,CAAC;YACN,CAAC;QACL,CAAC;aAAM,IAAI,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;YACnC,IAAI,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBAChE,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,qCAAqC,CAAC,CAAC;YACxE,CAAC;QACL,CAAC;aAAM,IAAI,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7C,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;YACnC,IAAI,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBAC5D,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBAC9B,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,+BAA+B,CAAC,CAAC;gBAClE,CAAC;YACL,CAAC;iBAAM,IAAI,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBACnE,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,0BAA0B,CAAC,CAAC;YAC7D,CAAC;QACL,CAAC;aAAM,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAC7B,OAAO;YACX,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;YAC3B,IAAI,MAAM,IAAI,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;gBAC5C,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAC7B,OAAO;YACX,CAAC;YACD,IACI,MAAM;gBACN,CAAC,EAAE,CAAC,0BAA0B,CAAC,MAAM,CAAC;oBAClC,EAAE,CAAC,oBAAoB,CAAC,MAAM,CAAC;oBAC/B,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC;oBAC9B,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC;oBAC3B,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC;oBAC5B,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,EACnC,CAAC;gBACC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAC7B,OAAO;YACX,CAAC;YACD,MAAM,mBAAmB,GAAG,gBAAgB,CACxC,IAAI,EACJ,CAAC,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAC5E,CAAC;YACF,IAAI,mBAAmB,EAAE,CAAC;gBACtB,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAC7B,OAAO;YACX,CAAC;YACD,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC,IAAI,oBAAoB,CAAC,CAAC;QACrE,CAAC;QACD,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACjC,CAAC,CAAC;IAEF,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAEnC,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\";\n\nconst HOSTILE_GLOBAL_NAMES = new Set([\n \"fetch\",\n \"setTimeout\",\n \"setInterval\",\n \"queueMicrotask\",\n \"Promise\",\n \"requestAnimationFrame\",\n \"Date\",\n \"eval\",\n \"require\",\n // Phase 7 — the indicator-composition rewriter synthesises calls to\n // this helper; user scripts must not name-collide with the slot.\n \"__chartlang_depOutput\",\n]);\n\nconst COMPARISON_OPS = new Set<ts.SyntaxKind>([\n ts.SyntaxKind.LessThanToken,\n ts.SyntaxKind.LessThanEqualsToken,\n ts.SyntaxKind.GreaterThanToken,\n ts.SyntaxKind.GreaterThanEqualsToken,\n]);\n\n/**\n * Walk the source file and emit a diagnostic for every forbidden construct:\n *\n * - `while` / `do-while` / `for-of` / `for-in` / unbounded `for` →\n * `unbounded-loop`.\n * - Self-recursive function declaration → `recursion-not-allowed`.\n * - References to hostile globals (`Math.random`, `Date.*`, `fetch`,\n * `setTimeout`, `setInterval`, `queueMicrotask`, `Promise`,\n * `requestAnimationFrame`), plus `require(...)`, dynamic `import(...)`,\n * `eval(...)`, `new Function(...)` → `hostile-global`.\n *\n * @since 0.1\n * @example\n * // const diagnostics = runForbiddenConstructs(sourceFile, \"demo.chart.ts\");\n * const fn: typeof runForbiddenConstructs = runForbiddenConstructs;\n * void fn;\n */\nexport function runForbiddenConstructs(\n sourceFile: ts.SourceFile,\n sourcePath: string,\n): ReadonlyArray<CompileDiagnostic> {\n const diagnostics: CompileDiagnostic[] = [];\n\n function emit(\n node: ts.Node,\n code: \"unbounded-loop\" | \"recursion-not-allowed\" | \"hostile-global\",\n message: string,\n ): void {\n diagnostics.push(\n createDiagnostic({\n severity: \"error\",\n code,\n message,\n file: sourcePath,\n node,\n sourceFile,\n }),\n );\n }\n\n function checkForStatement(node: ts.ForStatement): boolean {\n const init = node.initializer;\n const condition = node.condition;\n const incrementor = node.incrementor;\n if (!init || !condition || !incrementor) return false;\n if (!ts.isVariableDeclarationList(init)) return false;\n if (init.declarations.length !== 1) return false;\n const declaration = init.declarations[0];\n if (!declaration || !ts.isIdentifier(declaration.name)) return false;\n const initializer = declaration.initializer;\n if (!initializer || !ts.isNumericLiteral(initializer)) return false;\n if (!ts.isBinaryExpression(condition)) return false;\n if (!COMPARISON_OPS.has(condition.operatorToken.kind)) return false;\n if (!ts.isNumericLiteral(condition.right)) return false;\n if (!ts.isIdentifier(condition.left)) return false;\n if (condition.left.text !== declaration.name.text) return false;\n if (!ts.isPostfixUnaryExpression(incrementor)) return false;\n if (!ts.isIdentifier(incrementor.operand)) return false;\n if (incrementor.operand.text !== declaration.name.text) return false;\n return true;\n }\n\n function isInsideAncestor(node: ts.Node, predicate: (parent: ts.Node) => boolean): boolean {\n let current: ts.Node | undefined = node.parent;\n while (current) {\n if (predicate(current)) return true;\n current = current.parent;\n }\n return false;\n }\n\n function isDeclarationName(parent: ts.Node, node: ts.Identifier): boolean {\n const named = parent as { readonly name?: ts.Node };\n if (named.name !== node) return false;\n return (\n ts.isFunctionDeclaration(parent) ||\n ts.isFunctionExpression(parent) ||\n ts.isClassDeclaration(parent) ||\n ts.isClassExpression(parent) ||\n ts.isVariableDeclaration(parent) ||\n ts.isParameter(parent) ||\n ts.isInterfaceDeclaration(parent) ||\n ts.isTypeAliasDeclaration(parent) ||\n ts.isEnumDeclaration(parent) ||\n ts.isModuleDeclaration(parent)\n );\n }\n\n function enclosingFunctionName(node: ts.Node): string | null {\n let current: ts.Node | undefined = node.parent;\n while (current) {\n if (ts.isFunctionDeclaration(current) && current.name) {\n return current.name.text;\n }\n if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name)) {\n const initializer = current.initializer;\n if (\n initializer &&\n (ts.isFunctionExpression(initializer) || ts.isArrowFunction(initializer))\n ) {\n return current.name.text;\n }\n }\n current = current.parent;\n }\n return null;\n }\n\n const visit = (node: ts.Node): void => {\n if (ts.isWhileStatement(node)) {\n emit(node, \"unbounded-loop\", \"`while` loops are not allowed.\");\n } else if (ts.isDoStatement(node)) {\n emit(node, \"unbounded-loop\", \"`do…while` loops are not allowed.\");\n } else if (ts.isForOfStatement(node)) {\n emit(node, \"unbounded-loop\", \"`for…of` loops are not allowed in Phase 1.\");\n } else if (ts.isForInStatement(node)) {\n emit(node, \"unbounded-loop\", \"`for…in` loops are not allowed.\");\n } else if (ts.isForStatement(node)) {\n if (!checkForStatement(node)) {\n emit(\n node,\n \"unbounded-loop\",\n \"`for` loops must use literal numeric bounds: for (let i = <num>; i </<= <num>; i++).\",\n );\n }\n } else if (ts.isCallExpression(node)) {\n const expression = node.expression;\n if (ts.isIdentifier(expression)) {\n if (expression.text === \"eval\") {\n emit(node, \"hostile-global\", \"`eval` is not allowed.\");\n } else if (expression.text === \"require\") {\n emit(node, \"hostile-global\", \"`require` is not allowed.\");\n }\n } else if (expression.kind === ts.SyntaxKind.ImportKeyword) {\n emit(node, \"hostile-global\", \"Dynamic `import()` is not allowed.\");\n }\n const functionName = enclosingFunctionName(node);\n if (\n functionName !== null &&\n ts.isIdentifier(expression) &&\n expression.text === functionName\n ) {\n emit(\n node,\n \"recursion-not-allowed\",\n `Self-recursive call to \\`${functionName}\\` is not allowed.`,\n );\n }\n } else if (ts.isNewExpression(node)) {\n const expression = node.expression;\n if (ts.isIdentifier(expression) && expression.text === \"Function\") {\n emit(node, \"hostile-global\", \"`new Function(...)` is not allowed.\");\n }\n } else if (ts.isPropertyAccessExpression(node)) {\n const objectName = node.expression;\n if (ts.isIdentifier(objectName) && objectName.text === \"Math\") {\n if (node.name.text === \"random\") {\n emit(node, \"hostile-global\", \"`Math.random` is not allowed.\");\n }\n } else if (ts.isIdentifier(objectName) && objectName.text === \"Date\") {\n emit(node, \"hostile-global\", \"`Date.*` is not allowed.\");\n }\n } else if (ts.isIdentifier(node)) {\n if (!HOSTILE_GLOBAL_NAMES.has(node.text)) {\n ts.forEachChild(node, visit);\n return;\n }\n const parent = node.parent;\n if (parent && isDeclarationName(parent, node)) {\n ts.forEachChild(node, visit);\n return;\n }\n if (\n parent &&\n (ts.isPropertyAccessExpression(parent) ||\n ts.isPropertyAssignment(parent) ||\n ts.isPropertySignature(parent) ||\n ts.isBindingElement(parent) ||\n ts.isImportSpecifier(parent) ||\n ts.isExportSpecifier(parent))\n ) {\n ts.forEachChild(node, visit);\n return;\n }\n const isInsideTypeContext = isInsideAncestor(\n node,\n (ancestor) => ts.isTypeNode(ancestor) || ts.isTypeReferenceNode(ancestor),\n );\n if (isInsideTypeContext) {\n ts.forEachChild(node, visit);\n return;\n }\n emit(node, \"hostile-global\", `\\`${node.text}\\` is not allowed.`);\n }\n ts.forEachChild(node, visit);\n };\n\n ts.forEachChild(sourceFile, visit);\n\n return Object.freeze(diagnostics.slice());\n}\n"]}
1
+ {"version":3,"file":"forbiddenConstructs.js","sourceRoot":"","sources":["../../src/analysis/forbiddenConstructs.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 { type CompileDiagnostic, createDiagnostic } from \"../diagnostics.js\";\nimport { parseBoundedForLoop } from \"./loopBounds.js\";\n\nconst HOSTILE_GLOBAL_NAMES = new Set([\n \"fetch\",\n \"setTimeout\",\n \"setInterval\",\n \"queueMicrotask\",\n \"Promise\",\n \"requestAnimationFrame\",\n \"Date\",\n \"eval\",\n \"require\",\n // Phase 7 — the indicator-composition rewriter synthesises calls to\n // this helper; user scripts must not name-collide with the slot.\n \"__chartlang_depOutput\",\n]);\n\n/**\n * Walk the source file and emit a diagnostic for every forbidden construct:\n *\n * - `while` / `do-while` / `for-of` / `for-in` / unbounded `for` →\n * `unbounded-loop`.\n * - Self-recursive function declaration → `recursion-not-allowed`.\n * - References to hostile globals (`Math.random`, `Date.*`, `fetch`,\n * `setTimeout`, `setInterval`, `queueMicrotask`, `Promise`,\n * `requestAnimationFrame`), plus `require(...)`, dynamic `import(...)`,\n * `eval(...)`, `new Function(...)` → `hostile-global`.\n *\n * @since 0.1\n * @example\n * // const diagnostics = runForbiddenConstructs(sourceFile, \"demo.chart.ts\");\n * const fn: typeof runForbiddenConstructs = runForbiddenConstructs;\n * void fn;\n */\nexport function runForbiddenConstructs(\n sourceFile: ts.SourceFile,\n sourcePath: string,\n): ReadonlyArray<CompileDiagnostic> {\n const diagnostics: CompileDiagnostic[] = [];\n\n function emit(\n node: ts.Node,\n code: \"unbounded-loop\" | \"recursion-not-allowed\" | \"hostile-global\",\n message: string,\n ): void {\n diagnostics.push(\n createDiagnostic({\n severity: \"error\",\n code,\n message,\n file: sourcePath,\n node,\n sourceFile,\n }),\n );\n }\n\n function isInsideAncestor(node: ts.Node, predicate: (parent: ts.Node) => boolean): boolean {\n let current: ts.Node | undefined = node.parent;\n while (current) {\n if (predicate(current)) return true;\n current = current.parent;\n }\n return false;\n }\n\n function isDeclarationName(parent: ts.Node, node: ts.Identifier): boolean {\n const named = parent as { readonly name?: ts.Node };\n if (named.name !== node) return false;\n return (\n ts.isFunctionDeclaration(parent) ||\n ts.isFunctionExpression(parent) ||\n ts.isClassDeclaration(parent) ||\n ts.isClassExpression(parent) ||\n ts.isVariableDeclaration(parent) ||\n ts.isParameter(parent) ||\n ts.isInterfaceDeclaration(parent) ||\n ts.isTypeAliasDeclaration(parent) ||\n ts.isEnumDeclaration(parent) ||\n ts.isModuleDeclaration(parent)\n );\n }\n\n function enclosingFunctionName(node: ts.Node): string | null {\n let current: ts.Node | undefined = node.parent;\n while (current) {\n if (ts.isFunctionDeclaration(current) && current.name) {\n return current.name.text;\n }\n if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name)) {\n const initializer = current.initializer;\n if (\n initializer &&\n (ts.isFunctionExpression(initializer) || ts.isArrowFunction(initializer))\n ) {\n return current.name.text;\n }\n }\n current = current.parent;\n }\n return null;\n }\n\n const visit = (node: ts.Node): void => {\n if (ts.isWhileStatement(node)) {\n emit(node, \"unbounded-loop\", \"`while` loops are not allowed.\");\n } else if (ts.isDoStatement(node)) {\n emit(node, \"unbounded-loop\", \"`do…while` loops are not allowed.\");\n } else if (ts.isForOfStatement(node)) {\n emit(node, \"unbounded-loop\", \"`for…of` loops are not allowed in Phase 1.\");\n } else if (ts.isForInStatement(node)) {\n emit(node, \"unbounded-loop\", \"`for…in` loops are not allowed.\");\n } else if (ts.isForStatement(node)) {\n if (parseBoundedForLoop(node) === null) {\n emit(\n node,\n \"unbounded-loop\",\n \"`for` loops must use literal numeric bounds: for (let i = <num>; i </<= <num>; i++).\",\n );\n }\n } else if (ts.isCallExpression(node)) {\n const expression = node.expression;\n if (ts.isIdentifier(expression)) {\n if (expression.text === \"eval\") {\n emit(node, \"hostile-global\", \"`eval` is not allowed.\");\n } else if (expression.text === \"require\") {\n emit(node, \"hostile-global\", \"`require` is not allowed.\");\n }\n } else if (expression.kind === ts.SyntaxKind.ImportKeyword) {\n emit(node, \"hostile-global\", \"Dynamic `import()` is not allowed.\");\n }\n const functionName = enclosingFunctionName(node);\n if (\n functionName !== null &&\n ts.isIdentifier(expression) &&\n expression.text === functionName\n ) {\n emit(\n node,\n \"recursion-not-allowed\",\n `Self-recursive call to \\`${functionName}\\` is not allowed.`,\n );\n }\n } else if (ts.isNewExpression(node)) {\n const expression = node.expression;\n if (ts.isIdentifier(expression) && expression.text === \"Function\") {\n emit(node, \"hostile-global\", \"`new Function(...)` is not allowed.\");\n }\n } else if (ts.isPropertyAccessExpression(node)) {\n const objectName = node.expression;\n if (ts.isIdentifier(objectName) && objectName.text === \"Math\") {\n if (node.name.text === \"random\") {\n emit(node, \"hostile-global\", \"`Math.random` is not allowed.\");\n }\n } else if (ts.isIdentifier(objectName) && objectName.text === \"Date\") {\n emit(node, \"hostile-global\", \"`Date.*` is not allowed.\");\n }\n } else if (ts.isIdentifier(node)) {\n if (!HOSTILE_GLOBAL_NAMES.has(node.text)) {\n ts.forEachChild(node, visit);\n return;\n }\n const parent = node.parent;\n if (parent && isDeclarationName(parent, node)) {\n ts.forEachChild(node, visit);\n return;\n }\n if (\n parent &&\n (ts.isPropertyAccessExpression(parent) ||\n ts.isPropertyAssignment(parent) ||\n ts.isPropertySignature(parent) ||\n ts.isBindingElement(parent) ||\n ts.isImportSpecifier(parent) ||\n ts.isExportSpecifier(parent))\n ) {\n ts.forEachChild(node, visit);\n return;\n }\n const isInsideTypeContext = isInsideAncestor(\n node,\n (ancestor) => ts.isTypeNode(ancestor) || ts.isTypeReferenceNode(ancestor),\n );\n if (isInsideTypeContext) {\n ts.forEachChild(node, visit);\n return;\n }\n emit(node, \"hostile-global\", `\\`${node.text}\\` is not allowed.`);\n }\n ts.forEachChild(node, visit);\n };\n\n ts.forEachChild(sourceFile, visit);\n\n return Object.freeze(diagnostics.slice());\n}\n"]}
@@ -7,7 +7,9 @@ export { extractMaxLookback } from "./extractMaxLookback.js";
7
7
  export type { ExtractMaxLookbackResult } from "./extractMaxLookback.js";
8
8
  export { extractInputs } from "./extractInputs.js";
9
9
  export type { ExtractedDescriptor, ExtractInputsResult } from "./extractInputs.js";
10
- export { extractRequestedIntervals } from "./extractRequestedIntervals.js";
10
+ export { extractRequestAnalysis, extractRequestedIntervals } from "./extractRequestedIntervals.js";
11
+ export type { RequestAnalysis } from "./extractRequestedIntervals.js";
12
+ export { validateSecurityExpr } from "./validateSecurityExpr.js";
11
13
  export { validateLowerTfIntervals } from "./validateLowerTfIntervals.js";
12
14
  export { extractRequiresIntervals } from "./extractRequiresIntervals.js";
13
15
  export { extractAlertConditions } from "./extractAlertConditions.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analysis/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC1F,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EACR,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,eAAe,GAClB,MAAM,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analysis/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC1F,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AACnG,YAAY,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EACR,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,eAAe,GAClB,MAAM,6BAA6B,CAAC"}
@@ -6,7 +6,8 @@ export { runStatefulCallInLoop } from "./statefulCallInLoop.js";
6
6
  export { extractCapabilities } from "./extractCapabilities.js";
7
7
  export { extractMaxLookback } from "./extractMaxLookback.js";
8
8
  export { extractInputs } from "./extractInputs.js";
9
- export { extractRequestedIntervals } from "./extractRequestedIntervals.js";
9
+ export { extractRequestAnalysis, extractRequestedIntervals } from "./extractRequestedIntervals.js";
10
+ export { validateSecurityExpr } from "./validateSecurityExpr.js";
10
11
  export { validateLowerTfIntervals } from "./validateLowerTfIntervals.js";
11
12
  export { extractRequiresIntervals } from "./extractRequiresIntervals.js";
12
13
  export { extractAlertConditions } from "./extractAlertConditions.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/analysis/index.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+DAA+D;AAE/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,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\nexport { runStructuralChecks } from \"./structuralChecks.js\";\nexport type { StructuralBindingInfo, StructuralCheckResult } from \"./structuralChecks.js\";\nexport { runForbiddenConstructs } from \"./forbiddenConstructs.js\";\nexport { runStatefulCallInLoop } from \"./statefulCallInLoop.js\";\nexport { extractCapabilities } from \"./extractCapabilities.js\";\nexport { extractMaxLookback } from \"./extractMaxLookback.js\";\nexport type { ExtractMaxLookbackResult } from \"./extractMaxLookback.js\";\nexport { extractInputs } from \"./extractInputs.js\";\nexport type { ExtractedDescriptor, ExtractInputsResult } from \"./extractInputs.js\";\nexport { extractRequestedIntervals } from \"./extractRequestedIntervals.js\";\nexport { validateLowerTfIntervals } from \"./validateLowerTfIntervals.js\";\nexport { extractRequiresIntervals } from \"./extractRequiresIntervals.js\";\nexport { extractAlertConditions } from \"./extractAlertConditions.js\";\nexport type { ExtractAlertConditionsResult } from \"./extractAlertConditions.js\";\nexport { extractDependencyGraph } from \"./extractDependencyGraph.js\";\nexport type {\n DepConsumesEntry,\n DepGraph,\n DrawnScript,\n PrivateDep,\n ProducerRef,\n ProducerSnapshot,\n ResolveProducer,\n} from \"./extractDependencyGraph.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/analysis/index.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+DAA+D;AAE/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAEnG,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,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\nexport { runStructuralChecks } from \"./structuralChecks.js\";\nexport type { StructuralBindingInfo, StructuralCheckResult } from \"./structuralChecks.js\";\nexport { runForbiddenConstructs } from \"./forbiddenConstructs.js\";\nexport { runStatefulCallInLoop } from \"./statefulCallInLoop.js\";\nexport { extractCapabilities } from \"./extractCapabilities.js\";\nexport { extractMaxLookback } from \"./extractMaxLookback.js\";\nexport type { ExtractMaxLookbackResult } from \"./extractMaxLookback.js\";\nexport { extractInputs } from \"./extractInputs.js\";\nexport type { ExtractedDescriptor, ExtractInputsResult } from \"./extractInputs.js\";\nexport { extractRequestAnalysis, extractRequestedIntervals } from \"./extractRequestedIntervals.js\";\nexport type { RequestAnalysis } from \"./extractRequestedIntervals.js\";\nexport { validateSecurityExpr } from \"./validateSecurityExpr.js\";\nexport { validateLowerTfIntervals } from \"./validateLowerTfIntervals.js\";\nexport { extractRequiresIntervals } from \"./extractRequiresIntervals.js\";\nexport { extractAlertConditions } from \"./extractAlertConditions.js\";\nexport type { ExtractAlertConditionsResult } from \"./extractAlertConditions.js\";\nexport { extractDependencyGraph } from \"./extractDependencyGraph.js\";\nexport type {\n DepConsumesEntry,\n DepGraph,\n DrawnScript,\n PrivateDep,\n ProducerRef,\n ProducerSnapshot,\n ResolveProducer,\n} from \"./extractDependencyGraph.js\";\n"]}
@@ -0,0 +1,91 @@
1
+ import ts from "typescript";
2
+ /**
3
+ * The comparison operators a legal chartlang `for` condition may use.
4
+ * Shared by `parseBoundedForLoop` (which captures the operator so a sizer
5
+ * can derive the loop's max index) and `forbiddenConstructs` (which rejects
6
+ * any other condition shape) so the two passes recognise the same set.
7
+ *
8
+ * @since 0.1
9
+ * @stable
10
+ * @example
11
+ * COMPARISON_OPS.has(ts.SyntaxKind.LessThanToken); // → true
12
+ */
13
+ export declare const COMPARISON_OPS: ReadonlySet<ts.SyntaxKind>;
14
+ /**
15
+ * The parsed shape of a legal chartlang `for` loop.
16
+ *
17
+ * @since 0.1
18
+ * @stable
19
+ * @example
20
+ * const loop: BoundedForLoop = {
21
+ * varName: "i",
22
+ * start: 0,
23
+ * op: ts.SyntaxKind.LessThanToken,
24
+ * limit: 5,
25
+ * };
26
+ * void loop;
27
+ */
28
+ export type BoundedForLoop = Readonly<{
29
+ /** The induction variable name (the `i` in `for (let i = …)`). */
30
+ varName: string;
31
+ /** The literal initial value (`for (let i = <start>; …)`). */
32
+ start: number;
33
+ /** The comparison operator token used in the condition. */
34
+ op: ts.SyntaxKind;
35
+ /** The literal right-hand bound (`… i <op> <limit>; …`). */
36
+ limit: number;
37
+ }>;
38
+ /**
39
+ * Parse a `ts.ForStatement` into its `BoundedForLoop` shape, or `null`
40
+ * when it is not the one legal chartlang loop form
41
+ * (`for (let i = <numLit>; i <comparison> <numLit>; i++)` — single
42
+ * `let` init, id-on-left/literal-on-right condition, postfix `i++`).
43
+ * The single source of truth for "what is a bounded loop"; both
44
+ * `forbiddenConstructs` (reject everything else) and
45
+ * `resolveIndexUpperBound` (size the index range) call it so the two
46
+ * passes can never disagree.
47
+ *
48
+ * @since 0.1
49
+ * @stable
50
+ * @example
51
+ * // for (let i = 0; i < 5; i++) → { varName: "i", start: 0,
52
+ * // op: LessThanToken, limit: 5 }
53
+ * const fn: typeof parseBoundedForLoop = parseBoundedForLoop;
54
+ * void fn;
55
+ */
56
+ export declare function parseBoundedForLoop(node: ts.ForStatement): BoundedForLoop | null;
57
+ /**
58
+ * The induction variable's **declaration** identifier of the single legal
59
+ * chartlang loop *initializer* shape, or `null` otherwise. A sizer calls
60
+ * this directly when it needs the declaration node (not just the `varName`
61
+ * text) to ask the type checker whether an index use resolves to this
62
+ * loop's own binding rather than a nested shadow of the same name. Shares
63
+ * `parseBoundedForLoop`'s initializer acceptance via `parseLoopInit`.
64
+ *
65
+ * @since 0.1
66
+ * @stable
67
+ * @example
68
+ * // for (let i = 0; i < 5; i++) → the `i` declaration identifier
69
+ * const fn: typeof boundedLoopVarId = boundedLoopVarId;
70
+ * void fn;
71
+ */
72
+ export declare function boundedLoopVarId(node: ts.ForStatement): ts.Identifier | null;
73
+ /**
74
+ * Unwrap any number of nested parentheses around an expression. The Pine
75
+ * converter emits a historical bar offset as the parenthesised form
76
+ * `bar.point(-(N), …)` (see the converter's `anchorToWorldPoint`), so the
77
+ * lookback recogniser must peel the parens before matching the literal;
78
+ * the index-bound resolver does the same before matching a numeric leaf.
79
+ * Housed here — a leaf module with no analysis-package imports — so both
80
+ * `extractMaxLookback` and `resolveIndexBound` can share it without a
81
+ * circular import.
82
+ *
83
+ * @since 0.1
84
+ * @stable
85
+ * @example
86
+ * // unwrapParens of `((7))` → the `7` numeric-literal node
87
+ * const fn: typeof unwrapParens = unwrapParens;
88
+ * void fn;
89
+ */
90
+ export declare function unwrapParens(node: ts.Expression): ts.Expression;
91
+ //# sourceMappingURL=loopBounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopBounds.d.ts","sourceRoot":"","sources":["../../src/analysis/loopBounds.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,EAAE,WAAW,CAAC,EAAE,CAAC,UAAU,CAKpD,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IAClC,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC;IAClB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,GAAG,cAAc,GAAG,IAAI,CAoBhF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,GAAG,EAAE,CAAC,UAAU,GAAG,IAAI,CAE5E;AAsBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,GAAG,EAAE,CAAC,UAAU,CAI/D"}
@@ -0,0 +1,132 @@
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
+ /**
5
+ * The comparison operators a legal chartlang `for` condition may use.
6
+ * Shared by `parseBoundedForLoop` (which captures the operator so a sizer
7
+ * can derive the loop's max index) and `forbiddenConstructs` (which rejects
8
+ * any other condition shape) so the two passes recognise the same set.
9
+ *
10
+ * @since 0.1
11
+ * @stable
12
+ * @example
13
+ * COMPARISON_OPS.has(ts.SyntaxKind.LessThanToken); // → true
14
+ */
15
+ export const COMPARISON_OPS = new Set([
16
+ ts.SyntaxKind.LessThanToken,
17
+ ts.SyntaxKind.LessThanEqualsToken,
18
+ ts.SyntaxKind.GreaterThanToken,
19
+ ts.SyntaxKind.GreaterThanEqualsToken,
20
+ ]);
21
+ /**
22
+ * Parse a `ts.ForStatement` into its `BoundedForLoop` shape, or `null`
23
+ * when it is not the one legal chartlang loop form
24
+ * (`for (let i = <numLit>; i <comparison> <numLit>; i++)` — single
25
+ * `let` init, id-on-left/literal-on-right condition, postfix `i++`).
26
+ * The single source of truth for "what is a bounded loop"; both
27
+ * `forbiddenConstructs` (reject everything else) and
28
+ * `resolveIndexUpperBound` (size the index range) call it so the two
29
+ * passes can never disagree.
30
+ *
31
+ * @since 0.1
32
+ * @stable
33
+ * @example
34
+ * // for (let i = 0; i < 5; i++) → { varName: "i", start: 0,
35
+ * // op: LessThanToken, limit: 5 }
36
+ * const fn: typeof parseBoundedForLoop = parseBoundedForLoop;
37
+ * void fn;
38
+ */
39
+ export function parseBoundedForLoop(node) {
40
+ const init = parseLoopInit(node);
41
+ if (init === null)
42
+ return null;
43
+ const condition = node.condition;
44
+ const incrementor = node.incrementor;
45
+ if (!condition || !incrementor)
46
+ return null;
47
+ if (!ts.isBinaryExpression(condition))
48
+ return null;
49
+ if (!COMPARISON_OPS.has(condition.operatorToken.kind))
50
+ return null;
51
+ if (!ts.isNumericLiteral(condition.right))
52
+ return null;
53
+ if (!ts.isIdentifier(condition.left))
54
+ return null;
55
+ if (condition.left.text !== init.varId.text)
56
+ return null;
57
+ if (!ts.isPostfixUnaryExpression(incrementor))
58
+ return null;
59
+ if (!ts.isIdentifier(incrementor.operand))
60
+ return null;
61
+ if (incrementor.operand.text !== init.varId.text)
62
+ return null;
63
+ return {
64
+ varName: init.varId.text,
65
+ start: Number(init.start.text),
66
+ op: condition.operatorToken.kind,
67
+ limit: Number(condition.right.text),
68
+ };
69
+ }
70
+ /**
71
+ * The induction variable's **declaration** identifier of the single legal
72
+ * chartlang loop *initializer* shape, or `null` otherwise. A sizer calls
73
+ * this directly when it needs the declaration node (not just the `varName`
74
+ * text) to ask the type checker whether an index use resolves to this
75
+ * loop's own binding rather than a nested shadow of the same name. Shares
76
+ * `parseBoundedForLoop`'s initializer acceptance via `parseLoopInit`.
77
+ *
78
+ * @since 0.1
79
+ * @stable
80
+ * @example
81
+ * // for (let i = 0; i < 5; i++) → the `i` declaration identifier
82
+ * const fn: typeof boundedLoopVarId = boundedLoopVarId;
83
+ * void fn;
84
+ */
85
+ export function boundedLoopVarId(node) {
86
+ return parseLoopInit(node)?.varId ?? null;
87
+ }
88
+ /**
89
+ * The accepted `for (let i = <numLit>; …)` initializer — a single-
90
+ * declaration `let`/`const` list whose name is an identifier with a
91
+ * numeric-literal start value — captured as both nodes, or `null`. The one
92
+ * place the initializer shape is recognised; `parseBoundedForLoop` and
93
+ * `boundedLoopVarId` both build on it (no narrowing casts in either).
94
+ */
95
+ function parseLoopInit(node) {
96
+ const init = node.initializer;
97
+ if (!init || !ts.isVariableDeclarationList(init))
98
+ return null;
99
+ if (init.declarations.length !== 1)
100
+ return null;
101
+ const declaration = init.declarations[0];
102
+ if (!declaration || !ts.isIdentifier(declaration.name))
103
+ return null;
104
+ const start = declaration.initializer;
105
+ if (!start || !ts.isNumericLiteral(start))
106
+ return null;
107
+ return { varId: declaration.name, start };
108
+ }
109
+ /**
110
+ * Unwrap any number of nested parentheses around an expression. The Pine
111
+ * converter emits a historical bar offset as the parenthesised form
112
+ * `bar.point(-(N), …)` (see the converter's `anchorToWorldPoint`), so the
113
+ * lookback recogniser must peel the parens before matching the literal;
114
+ * the index-bound resolver does the same before matching a numeric leaf.
115
+ * Housed here — a leaf module with no analysis-package imports — so both
116
+ * `extractMaxLookback` and `resolveIndexBound` can share it without a
117
+ * circular import.
118
+ *
119
+ * @since 0.1
120
+ * @stable
121
+ * @example
122
+ * // unwrapParens of `((7))` → the `7` numeric-literal node
123
+ * const fn: typeof unwrapParens = unwrapParens;
124
+ * void fn;
125
+ */
126
+ export function unwrapParens(node) {
127
+ let current = node;
128
+ while (ts.isParenthesizedExpression(current))
129
+ current = current.expression;
130
+ return current;
131
+ }
132
+ //# sourceMappingURL=loopBounds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopBounds.js","sourceRoot":"","sources":["../../src/analysis/loopBounds.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\n/**\n * The comparison operators a legal chartlang `for` condition may use.\n * Shared by `parseBoundedForLoop` (which captures the operator so a sizer\n * can derive the loop's max index) and `forbiddenConstructs` (which rejects\n * any other condition shape) so the two passes recognise the same set.\n *\n * @since 0.1\n * @stable\n * @example\n * COMPARISON_OPS.has(ts.SyntaxKind.LessThanToken); // → true\n */\nexport const COMPARISON_OPS: ReadonlySet<ts.SyntaxKind> = new Set<ts.SyntaxKind>([\n ts.SyntaxKind.LessThanToken,\n ts.SyntaxKind.LessThanEqualsToken,\n ts.SyntaxKind.GreaterThanToken,\n ts.SyntaxKind.GreaterThanEqualsToken,\n]);\n\n/**\n * The parsed shape of a legal chartlang `for` loop.\n *\n * @since 0.1\n * @stable\n * @example\n * const loop: BoundedForLoop = {\n * varName: \"i\",\n * start: 0,\n * op: ts.SyntaxKind.LessThanToken,\n * limit: 5,\n * };\n * void loop;\n */\nexport type BoundedForLoop = Readonly<{\n /** The induction variable name (the `i` in `for (let i = …)`). */\n varName: string;\n /** The literal initial value (`for (let i = <start>; …)`). */\n start: number;\n /** The comparison operator token used in the condition. */\n op: ts.SyntaxKind;\n /** The literal right-hand bound (`… i <op> <limit>; …`). */\n limit: number;\n}>;\n\n/**\n * Parse a `ts.ForStatement` into its `BoundedForLoop` shape, or `null`\n * when it is not the one legal chartlang loop form\n * (`for (let i = <numLit>; i <comparison> <numLit>; i++)` — single\n * `let` init, id-on-left/literal-on-right condition, postfix `i++`).\n * The single source of truth for \"what is a bounded loop\"; both\n * `forbiddenConstructs` (reject everything else) and\n * `resolveIndexUpperBound` (size the index range) call it so the two\n * passes can never disagree.\n *\n * @since 0.1\n * @stable\n * @example\n * // for (let i = 0; i < 5; i++) → { varName: \"i\", start: 0,\n * // op: LessThanToken, limit: 5 }\n * const fn: typeof parseBoundedForLoop = parseBoundedForLoop;\n * void fn;\n */\nexport function parseBoundedForLoop(node: ts.ForStatement): BoundedForLoop | null {\n const init = parseLoopInit(node);\n if (init === null) return null;\n const condition = node.condition;\n const incrementor = node.incrementor;\n if (!condition || !incrementor) return null;\n if (!ts.isBinaryExpression(condition)) return null;\n if (!COMPARISON_OPS.has(condition.operatorToken.kind)) return null;\n if (!ts.isNumericLiteral(condition.right)) return null;\n if (!ts.isIdentifier(condition.left)) return null;\n if (condition.left.text !== init.varId.text) return null;\n if (!ts.isPostfixUnaryExpression(incrementor)) return null;\n if (!ts.isIdentifier(incrementor.operand)) return null;\n if (incrementor.operand.text !== init.varId.text) return null;\n return {\n varName: init.varId.text,\n start: Number(init.start.text),\n op: condition.operatorToken.kind,\n limit: Number(condition.right.text),\n };\n}\n\n/**\n * The induction variable's **declaration** identifier of the single legal\n * chartlang loop *initializer* shape, or `null` otherwise. A sizer calls\n * this directly when it needs the declaration node (not just the `varName`\n * text) to ask the type checker whether an index use resolves to this\n * loop's own binding rather than a nested shadow of the same name. Shares\n * `parseBoundedForLoop`'s initializer acceptance via `parseLoopInit`.\n *\n * @since 0.1\n * @stable\n * @example\n * // for (let i = 0; i < 5; i++) → the `i` declaration identifier\n * const fn: typeof boundedLoopVarId = boundedLoopVarId;\n * void fn;\n */\nexport function boundedLoopVarId(node: ts.ForStatement): ts.Identifier | null {\n return parseLoopInit(node)?.varId ?? null;\n}\n\n/**\n * The accepted `for (let i = <numLit>; …)` initializer — a single-\n * declaration `let`/`const` list whose name is an identifier with a\n * numeric-literal start value — captured as both nodes, or `null`. The one\n * place the initializer shape is recognised; `parseBoundedForLoop` and\n * `boundedLoopVarId` both build on it (no narrowing casts in either).\n */\nfunction parseLoopInit(\n node: ts.ForStatement,\n): Readonly<{ varId: ts.Identifier; start: ts.NumericLiteral }> | null {\n const init = node.initializer;\n if (!init || !ts.isVariableDeclarationList(init)) return null;\n if (init.declarations.length !== 1) return null;\n const declaration = init.declarations[0];\n if (!declaration || !ts.isIdentifier(declaration.name)) return null;\n const start = declaration.initializer;\n if (!start || !ts.isNumericLiteral(start)) return null;\n return { varId: declaration.name, start };\n}\n\n/**\n * Unwrap any number of nested parentheses around an expression. The Pine\n * converter emits a historical bar offset as the parenthesised form\n * `bar.point(-(N), …)` (see the converter's `anchorToWorldPoint`), so the\n * lookback recogniser must peel the parens before matching the literal;\n * the index-bound resolver does the same before matching a numeric leaf.\n * Housed here — a leaf module with no analysis-package imports — so both\n * `extractMaxLookback` and `resolveIndexBound` can share it without a\n * circular import.\n *\n * @since 0.1\n * @stable\n * @example\n * // unwrapParens of `((7))` → the `7` numeric-literal node\n * const fn: typeof unwrapParens = unwrapParens;\n * void fn;\n */\nexport function unwrapParens(node: ts.Expression): ts.Expression {\n let current = node;\n while (ts.isParenthesizedExpression(current)) current = current.expression;\n return current;\n}\n"]}