@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
@@ -1 +1 @@
1
- {"version":3,"file":"extractMaxLookback.js","sourceRoot":"","sources":["../../src/analysis/extractMaxLookback.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\";\n\nconst OHLCV_FIELDS = new Set([\"close\", \"open\", \"high\", \"low\", \"volume\", \"time\"]);\n\n/**\n * Maximum literal lookback `N` discovered across every series read in the\n * source plus the inferred `seriesCapacities` record. `dynamicFallback`\n * captures the §6.6 contract: any non-literal series index contributes\n * `5000` so the runtime can size its ring buffers safely.\n *\n * @since 0.1\n * @example\n * const r: ExtractMaxLookbackResult = {\n * maxLookback: 20,\n * seriesCapacities: {},\n * diagnostics: [],\n * };\n * void r;\n */\nexport type ExtractMaxLookbackResult = Readonly<{\n maxLookback: number;\n seriesCapacities: Readonly<Record<string, number>>;\n diagnostics: ReadonlyArray<CompileDiagnostic>;\n}>;\n\n/**\n * Walk the source file's `ElementAccessExpression` nodes and infer\n * `maxLookback` plus any `dynamicFallback` capacity from non-literal index\n * reads on Phase-1 series shapes: `bar.<ohlcv>[N]`, `ta.<name>(...)[N]`,\n * and identifier-bound series variables (`const e = ta.ema(...); e[N];`).\n *\n * The optional `scope` parameter narrows both the series-variable\n * collection and the lookback walk to a single AST subtree (typically\n * one binding's `defineCall`) so multi-export files derive per-binding\n * `maxLookback` values. Defaults to the whole `sourceFile`.\n *\n * @since 0.1\n * @example\n * // const { maxLookback, seriesCapacities, diagnostics } =\n * // extractMaxLookback(sourceFile, checker, \"demo.chart.ts\");\n * const fn: typeof extractMaxLookback = extractMaxLookback;\n * void fn;\n */\nexport function extractMaxLookback(\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker,\n sourcePath: string,\n scope: ts.Node = sourceFile,\n): ExtractMaxLookbackResult {\n let maxLookback = 0;\n const seriesCapacities: Record<string, number> = {};\n const diagnostics: CompileDiagnostic[] = [];\n\n const seriesVarNames = collectSeriesVarNames(scope, checker);\n\n const visit = (node: ts.Node): void => {\n if (ts.isElementAccessExpression(node)) {\n if (isSeriesShapedAccess(node, checker, seriesVarNames)) {\n const argument = node.argumentExpression;\n if (ts.isNumericLiteral(argument)) {\n const n = Number(argument.text);\n if (Number.isFinite(n) && n > maxLookback) maxLookback = n;\n } else {\n diagnostics.push(\n createDiagnostic({\n severity: \"warning\",\n code: \"dynamic-series-index\",\n message:\n \"Non-literal series index — runtime will use the 5000-slot dynamic fallback buffer.\",\n file: sourcePath,\n node: argument,\n sourceFile,\n }),\n );\n seriesCapacities.dynamicFallback = 5000;\n }\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(scope);\n\n return Object.freeze({\n maxLookback,\n seriesCapacities: Object.freeze({ ...seriesCapacities }),\n diagnostics: Object.freeze(diagnostics.slice()),\n });\n}\n\nfunction collectSeriesVarNames(scope: ts.Node, checker: ts.TypeChecker): ReadonlySet<string> {\n const names = new Set<string>();\n const visit = (node: ts.Node): void => {\n if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {\n const initializer = node.initializer;\n if (initializer && ts.isCallExpression(initializer)) {\n const calleeName = resolveCalleeName(initializer, checker);\n if (calleeName?.startsWith(\"ta.\")) {\n names.add(node.name.text);\n }\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(scope);\n return names;\n}\n\nfunction isSeriesShapedAccess(\n node: ts.ElementAccessExpression,\n checker: ts.TypeChecker,\n seriesVarNames: ReadonlySet<string>,\n): boolean {\n const expression = node.expression;\n if (ts.isPropertyAccessExpression(expression)) {\n if (OHLCV_FIELDS.has(expression.name.text)) return true;\n }\n if (ts.isCallExpression(expression)) {\n const calleeName = resolveCalleeName(expression, checker);\n if (calleeName?.startsWith(\"ta.\")) return true;\n }\n if (ts.isIdentifier(expression)) {\n if (seriesVarNames.has(expression.text)) return true;\n }\n return false;\n}\n"]}
1
+ {"version":3,"file":"extractMaxLookback.js","sourceRoot":"","sources":["../../src/analysis/extractMaxLookback.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 { unwrapParens } from \"./loopBounds.js\";\nimport { collectConstNumberEnv, resolveIndexUpperBound } from \"./resolveIndexBound.js\";\n\nconst OHLCV_FIELDS = new Set([\"close\", \"open\", \"high\", \"low\", \"volume\", \"time\"]);\n\n/**\n * Maximum literal lookback `N` discovered across every series read in the\n * source plus the inferred `seriesCapacities` record. `dynamicFallback`\n * captures the §6.6 contract: any non-literal series index contributes\n * `5000` so the runtime can size its ring buffers safely.\n *\n * @since 0.1\n * @example\n * const r: ExtractMaxLookbackResult = {\n * maxLookback: 20,\n * seriesCapacities: {},\n * diagnostics: [],\n * };\n * void r;\n */\nexport type ExtractMaxLookbackResult = Readonly<{\n maxLookback: number;\n seriesCapacities: Readonly<Record<string, number>>;\n diagnostics: ReadonlyArray<CompileDiagnostic>;\n}>;\n\n/**\n * Walk the source file's `ElementAccessExpression` nodes and infer\n * `maxLookback` plus any `dynamicFallback` capacity from non-literal index\n * reads on Phase-1 series shapes: `bar.<ohlcv>[N]`, `ta.<name>(...)[N]`,\n * and identifier-bound series variables (`const e = ta.ema(...); e[N];` or\n * `const s = state.series(...); s[N];`).\n *\n * The optional `scope` parameter narrows both the series-variable\n * collection and the lookback walk to a single AST subtree (typically\n * one binding's `defineCall`) so multi-export files derive per-binding\n * `maxLookback` values. Defaults to the whole `sourceFile`.\n *\n * @since 0.1\n * @example\n * // const { maxLookback, seriesCapacities, diagnostics } =\n * // extractMaxLookback(sourceFile, checker, \"demo.chart.ts\");\n * const fn: typeof extractMaxLookback = extractMaxLookback;\n * void fn;\n */\nexport function extractMaxLookback(\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker,\n sourcePath: string,\n scope: ts.Node = sourceFile,\n): ExtractMaxLookbackResult {\n let maxLookback = 0;\n const seriesCapacities: Record<string, number> = {};\n const diagnostics: CompileDiagnostic[] = [];\n\n const seriesVarNames = collectSeriesVarNames(scope, checker);\n\n const visit = (node: ts.Node): void => {\n if (ts.isCallExpression(node)) {\n const calleeName = resolveCalleeName(node, checker);\n if (calleeName?.startsWith(\"ta.\")) {\n const barsDepth = readHighestLowestBarsDepth(calleeName, node);\n if (barsDepth > maxLookback) maxLookback = barsDepth;\n }\n if (isBarPointCall(node)) {\n const depth = readBarPointLookback(node);\n if (depth > maxLookback) maxLookback = depth;\n }\n }\n if (ts.isElementAccessExpression(node)) {\n if (isSeriesShapedAccess(node, checker, seriesVarNames)) {\n const argument = node.argumentExpression;\n const constEnv = collectConstNumberEnv(argument, scope);\n const bound = resolveIndexUpperBound(argument, node, { constEnv, checker });\n if (bound !== null) {\n if (bound > maxLookback) maxLookback = bound;\n } else {\n diagnostics.push(\n createDiagnostic({\n severity: \"warning\",\n code: \"dynamic-series-index\",\n message:\n \"Non-literal series index — runtime will use the 5000-slot dynamic fallback buffer.\",\n file: sourcePath,\n node: argument,\n sourceFile,\n }),\n );\n seriesCapacities.dynamicFallback = 5000;\n }\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(scope);\n\n return Object.freeze({\n maxLookback,\n seriesCapacities: Object.freeze({ ...seriesCapacities }),\n diagnostics: Object.freeze(diagnostics.slice()),\n });\n}\n\n/**\n * Whether a call is a `bar.point(…)` invocation. Matched textually on the\n * `bar.point` property-access shape — the same OHLCV-style textual recognition\n * `isSeriesShapedAccess` uses — so it fires for both the destructured\n * `compute({ bar })` binding and a `declare const bar: Bar` test fixture.\n */\nfunction isBarPointCall(call: ts.CallExpression): boolean {\n const expression = call.expression;\n return (\n ts.isPropertyAccessExpression(expression) &&\n expression.name.text === \"point\" &&\n ts.isIdentifier(expression.expression) &&\n expression.expression.text === \"bar\"\n );\n}\n\n/**\n * The historical-lookback depth a `bar.point(offset, …)` call contributes,\n * or `0` when it reads the current / a future bar. A negative integer-literal\n * first argument (`bar.point(-N, …)` — or the converter's parenthesised\n * `bar.point(-(N), …)`) anchors `N` bars back, so the runtime's time ring\n * buffer must retain `N` extra slots — exactly like a `series[N]` lookback.\n * `bar.point(0, …)` (current) and positive offsets (future, extrapolated, no\n * buffer depth) contribute `0`; a non-literal / dynamic offset (e.g. a bound\n * `-k` or a computed `-(2 + 3)`) cannot be sized at compile time and also\n * contributes `0` (reads past retention degrade to a NaN time at runtime, per\n * `bar.point`'s contract).\n */\nfunction readBarPointLookback(call: ts.CallExpression): number {\n const first = call.arguments[0];\n if (first === undefined) return 0;\n const expr = unwrapParens(first);\n if (ts.isPrefixUnaryExpression(expr) && expr.operator === ts.SyntaxKind.MinusToken) {\n const operand = unwrapParens(expr.operand);\n if (ts.isNumericLiteral(operand)) {\n const n = Number(operand.text);\n if (Number.isFinite(n) && n > 0) return n;\n }\n }\n return 0;\n}\n\nfunction collectSeriesVarNames(scope: ts.Node, checker: ts.TypeChecker): ReadonlySet<string> {\n const names = new Set<string>();\n const visit = (node: ts.Node): void => {\n if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {\n const initializer = node.initializer;\n if (initializer && ts.isCallExpression(initializer)) {\n const calleeName = resolveCalleeName(initializer, checker);\n // A `state.series(...)`-bound variable is series-shaped just\n // like a `ta.*`-bound one: `s[N]` reads the slot's ring buffer,\n // so its literal index must fold into `maxLookback`. Matched on\n // the resolved callee name (the slot-injection path) so an\n // element-access form like `state[\"series\"](...)` is not\n // recognised — that form is rejected upstream as\n // `stateful-call-element-access`.\n if (calleeName?.startsWith(\"ta.\") || calleeName === \"state.series\") {\n names.add(node.name.text);\n }\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(scope);\n return names;\n}\n\n/**\n * The historical-lookback depth a `ta.highestbars` / `ta.lowestbars` call\n * contributes. Both primitives return the bar OFFSET (≤ 0) to the extreme\n * over the trailing `length`-bar window, so the deepest offset they can\n * return is `-(length − 1)`. A downstream `bar.point(<that offset>, …)`\n * anchor reads `time.at(length − 1)`, so the runtime's time ring buffer\n * must retain `length − 1` slots. Only a LITERAL second positional `length`\n * arg can be sized at compile time; a non-literal length contributes `0`.\n */\nfunction readHighestLowestBarsDepth(calleeName: string, call: ts.CallExpression): number {\n if (calleeName !== \"ta.highestbars\" && calleeName !== \"ta.lowestbars\") return 0;\n const lengthArg = call.arguments[1];\n if (lengthArg === undefined || !ts.isNumericLiteral(lengthArg)) return 0;\n const length = Number(lengthArg.text);\n if (!Number.isFinite(length) || length <= 1) return 0;\n return length - 1;\n}\n\nfunction isSeriesShapedAccess(\n node: ts.ElementAccessExpression,\n checker: ts.TypeChecker,\n seriesVarNames: ReadonlySet<string>,\n): boolean {\n const expression = node.expression;\n if (ts.isPropertyAccessExpression(expression)) {\n if (OHLCV_FIELDS.has(expression.name.text)) return true;\n }\n if (ts.isCallExpression(expression)) {\n const calleeName = resolveCalleeName(expression, checker);\n if (calleeName?.startsWith(\"ta.\")) return true;\n }\n if (ts.isIdentifier(expression)) {\n if (seriesVarNames.has(expression.text)) return true;\n }\n return false;\n}\n"]}
@@ -1,12 +1,74 @@
1
+ import { type RequestedFeed, type SecurityExpressionDescriptor } from "@invinite-org/chartlang-core";
1
2
  import ts from "typescript";
2
3
  import { type CompileDiagnostic } from "../diagnostics.js";
3
4
  import type { ExtractedDescriptor } from "./extractInputs.js";
5
+ /**
6
+ * Combined result of the `request.*` analysis pass: the sorted, deduped list
7
+ * of requested intervals (the **main-symbol** projection), the sorted, deduped
8
+ * list of requested `(symbol?, interval)` {@link RequestedFeed | feeds} (the
9
+ * superset), plus one {@link SecurityExpressionDescriptor} per
10
+ * `request.security({ interval }, (bar) => …)` expression callsite (sorted by
11
+ * `slotId`).
12
+ *
13
+ * `intervals` keeps its exact existing meaning — the symbol-omitted
14
+ * (chart-symbol) higher-timeframe intervals — so existing manifests stay
15
+ * byte-identical. `feeds` adds the symbol dimension: one entry per distinct
16
+ * `(symbol, interval)` pair, deduped + ordered by the shared
17
+ * `feedKey(symbol, interval)` so the printed manifest is byte-stable.
18
+ *
19
+ * @since 0.7
20
+ * @stable
21
+ * @example
22
+ * const r: RequestAnalysis = { intervals: ["1W"], feeds: [], securityExpressions: [] };
23
+ * void r;
24
+ */
25
+ export type RequestAnalysis = Readonly<{
26
+ intervals: ReadonlyArray<string>;
27
+ feeds: ReadonlyArray<RequestedFeed>;
28
+ securityExpressions: ReadonlyArray<SecurityExpressionDescriptor>;
29
+ }>;
30
+ /**
31
+ * Walk a script's AST and collect every static `interval` argument to
32
+ * `request.security({ interval: ... })` and `request.lowerTf(...)`, every
33
+ * distinct requested `(symbol?, interval)` feed (`request.security` only —
34
+ * `request.lowerTf` has no symbol), plus every `request.security` *expression*
35
+ * callsite (a second arrow/function argument). Dynamic intervals emit
36
+ * `request-security-interval-not-literal` (for `request.security`) or
37
+ * `request-lower-tf-interval-not-literal` (for `request.lowerTf`); a dynamic
38
+ * `request.security` symbol emits `request-security-symbol-not-literal`. Either
39
+ * dynamic axis is excluded.
40
+ *
41
+ * The `symbol` opt is read the same three ways `interval` is — a string literal,
42
+ * an `inputs.<enum>` access (expanded to all options), or an `inputs.<name>`
43
+ * `input.symbol` default literal — and the cartesian product of resolved
44
+ * symbols × intervals is deduped into `feeds` via the shared
45
+ * `feedKey(symbol, interval)`. A symbol-omitted (or empty-literal) feed keeps its
46
+ * interval in `intervals` (the main-symbol projection); a present-symbol feed
47
+ * does not.
48
+ *
49
+ * Each expression callsite is recorded as a {@link SecurityExpressionDescriptor}
50
+ * keyed by the same `slotId` the callsite-id transformer injects (via the
51
+ * shared `callsiteIdFor` helper) so the runtime can match the manifest entry
52
+ * to the inlined callback. When `validateExpressions` is `true`, each callback
53
+ * is also run through {@link validateSecurityExpr}, pushing
54
+ * `request-security-expr-captures-local` for any out-of-subset reference.
55
+ *
56
+ * @since 0.7
57
+ * @stable
58
+ * @example
59
+ * // const { intervals, feeds, securityExpressions } =
60
+ * // extractRequestAnalysis(sf, checker, inputs, diagnostics, path, true);
61
+ * const fn: typeof extractRequestAnalysis = extractRequestAnalysis;
62
+ * void fn;
63
+ */
64
+ export declare function extractRequestAnalysis(sourceFile: ts.SourceFile, checker: ts.TypeChecker, inputs: Readonly<Record<string, ExtractedDescriptor>>, diagnostics: CompileDiagnostic[], sourcePath?: string, validateExpressions?: boolean): RequestAnalysis;
4
65
  /**
5
66
  * Walk a script's AST and collect every static `interval` argument to
6
67
  * `request.security({ interval: ... })` and `request.lowerTf(...)`. Dynamic
7
68
  * arguments emit `request-security-interval-not-literal` (for `request.security`)
8
69
  * or `request-lower-tf-interval-not-literal` (for `request.lowerTf`) and are
9
- * excluded.
70
+ * excluded. Thin delegate over {@link extractRequestAnalysis} kept for callers
71
+ * that only need the interval list.
10
72
  *
11
73
  * @since 0.4
12
74
  * @example
@@ -1 +1 @@
1
- {"version":3,"file":"extractRequestedIntervals.d.ts","sourceRoot":"","sources":["../../src/analysis/extractRequestedIntervals.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAE7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACrC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,OAAO,EAAE,EAAE,CAAC,WAAW,EACvB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,EACrD,WAAW,EAAE,iBAAiB,EAAE,EAChC,UAAU,GAAE,MAA4B,GACzC,aAAa,CAAC,MAAM,CAAC,CAuBvB"}
1
+ {"version":3,"file":"extractRequestedIntervals.d.ts","sourceRoot":"","sources":["../../src/analysis/extractRequestedIntervals.ts"],"names":[],"mappings":"AAGA,OAAO,EAEH,KAAK,aAAa,EAClB,KAAK,4BAA4B,EACpC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAG7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAG9D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACnC,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACjC,KAAK,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IACpC,mBAAmB,EAAE,aAAa,CAAC,4BAA4B,CAAC,CAAC;CACpE,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,sBAAsB,CAClC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,OAAO,EAAE,EAAE,CAAC,WAAW,EACvB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,EACrD,WAAW,EAAE,iBAAiB,EAAE,EAChC,UAAU,GAAE,MAA4B,EACxC,mBAAmB,UAAQ,GAC5B,eAAe,CAgDjB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,yBAAyB,CACrC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,OAAO,EAAE,EAAE,CAAC,WAAW,EACvB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,EACrD,WAAW,EAAE,iBAAiB,EAAE,EAChC,UAAU,GAAE,MAA4B,GACzC,aAAa,CAAC,MAAM,CAAC,CAEvB"}
@@ -1,66 +1,282 @@
1
1
  // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
2
  // See the LICENSE file in the repo root for full license text.
3
+ import { feedKey, } from "@invinite-org/chartlang-core";
3
4
  import ts from "typescript";
4
5
  import { createDiagnostic } from "../diagnostics.js";
6
+ import { callsiteIdFor } from "../transformers/callsiteIdInjection.js";
5
7
  import { resolveCalleeName } from "../transformers/resolveCallee.js";
8
+ import { validateSecurityExpr } from "./validateSecurityExpr.js";
6
9
  /**
7
10
  * 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.
11
+ * `request.security({ interval: ... })` and `request.lowerTf(...)`, every
12
+ * distinct requested `(symbol?, interval)` feed (`request.security` only —
13
+ * `request.lowerTf` has no symbol), plus every `request.security` *expression*
14
+ * callsite (a second arrow/function argument). Dynamic intervals emit
15
+ * `request-security-interval-not-literal` (for `request.security`) or
16
+ * `request-lower-tf-interval-not-literal` (for `request.lowerTf`); a dynamic
17
+ * `request.security` symbol emits `request-security-symbol-not-literal`. Either
18
+ * dynamic axis is excluded.
12
19
  *
13
- * @since 0.4
20
+ * The `symbol` opt is read the same three ways `interval` is — a string literal,
21
+ * an `inputs.<enum>` access (expanded to all options), or an `inputs.<name>`
22
+ * `input.symbol` default literal — and the cartesian product of resolved
23
+ * symbols × intervals is deduped into `feeds` via the shared
24
+ * `feedKey(symbol, interval)`. A symbol-omitted (or empty-literal) feed keeps its
25
+ * interval in `intervals` (the main-symbol projection); a present-symbol feed
26
+ * does not.
27
+ *
28
+ * Each expression callsite is recorded as a {@link SecurityExpressionDescriptor}
29
+ * keyed by the same `slotId` the callsite-id transformer injects (via the
30
+ * shared `callsiteIdFor` helper) so the runtime can match the manifest entry
31
+ * to the inlined callback. When `validateExpressions` is `true`, each callback
32
+ * is also run through {@link validateSecurityExpr}, pushing
33
+ * `request-security-expr-captures-local` for any out-of-subset reference.
34
+ *
35
+ * @since 0.7
36
+ * @stable
14
37
  * @example
15
- * // const intervals = extractRequestedIntervals(sf, checker, inputs, diagnostics);
16
- * // intervals === ["1D", "5m"];
17
- * const fn: typeof extractRequestedIntervals = extractRequestedIntervals;
38
+ * // const { intervals, feeds, securityExpressions } =
39
+ * // extractRequestAnalysis(sf, checker, inputs, diagnostics, path, true);
40
+ * const fn: typeof extractRequestAnalysis = extractRequestAnalysis;
18
41
  * void fn;
19
42
  */
20
- export function extractRequestedIntervals(sourceFile, checker, inputs, diagnostics, sourcePath = sourceFile.fileName) {
43
+ export function extractRequestAnalysis(sourceFile, checker, inputs, diagnostics, sourcePath = sourceFile.fileName, validateExpressions = false) {
21
44
  const intervals = new Set();
45
+ // Keyed by `feedKey(symbol, interval)` so the dedup format matches the
46
+ // runtime/host stream key exactly and the sort below is byte-stable.
47
+ const feeds = new Map();
48
+ const securityExpressions = [];
22
49
  const visit = (node) => {
23
50
  if (ts.isCallExpression(node)) {
24
51
  const calleeName = resolveCalleeName(node, checker);
25
52
  if (calleeName === "request.security" || calleeName === "request.lowerTf") {
26
- readRequestInterval(node, calleeName, sourceFile, sourcePath, inputs, diagnostics, intervals);
53
+ readRequestInterval(node, calleeName, sourceFile, sourcePath, inputs, diagnostics, intervals, feeds);
54
+ }
55
+ if (calleeName === "request.security") {
56
+ readSecurityExpression(node, sourceFile, sourcePath, checker, diagnostics, validateExpressions, inputs, securityExpressions);
27
57
  }
28
58
  }
29
59
  ts.forEachChild(node, visit);
30
60
  };
31
61
  ts.forEachChild(sourceFile, visit);
32
- return Object.freeze(Array.from(intervals).sort());
62
+ securityExpressions.sort((a, b) => a.slotId.localeCompare(b.slotId));
63
+ const sortedFeeds = Array.from(feeds.entries())
64
+ .sort(([a], [b]) => a.localeCompare(b))
65
+ .map(([, feed]) => feed);
66
+ return Object.freeze({
67
+ intervals: Object.freeze(Array.from(intervals).sort()),
68
+ feeds: Object.freeze(sortedFeeds),
69
+ securityExpressions: Object.freeze(securityExpressions.slice()),
70
+ });
33
71
  }
34
- function readRequestInterval(call, calleeName, sourceFile, sourcePath, inputs, diagnostics, intervals) {
72
+ /**
73
+ * Walk a script's AST and collect every static `interval` argument to
74
+ * `request.security({ interval: ... })` and `request.lowerTf(...)`. Dynamic
75
+ * arguments emit `request-security-interval-not-literal` (for `request.security`)
76
+ * or `request-lower-tf-interval-not-literal` (for `request.lowerTf`) and are
77
+ * excluded. Thin delegate over {@link extractRequestAnalysis} kept for callers
78
+ * that only need the interval list.
79
+ *
80
+ * @since 0.4
81
+ * @example
82
+ * // const intervals = extractRequestedIntervals(sf, checker, inputs, diagnostics);
83
+ * // intervals === ["1D", "5m"];
84
+ * const fn: typeof extractRequestedIntervals = extractRequestedIntervals;
85
+ * void fn;
86
+ */
87
+ export function extractRequestedIntervals(sourceFile, checker, inputs, diagnostics, sourcePath = sourceFile.fileName) {
88
+ return extractRequestAnalysis(sourceFile, checker, inputs, diagnostics, sourcePath).intervals;
89
+ }
90
+ /**
91
+ * Detect and record a `request.security` expression callsite — a second
92
+ * argument that is an arrow or function expression. Mints the descriptor's
93
+ * `slotId` via `callsiteIdFor` (lockstep with the injector), reads the literal
94
+ * `interval`, the literal `symbol` (string literal or `input.symbol` default —
95
+ * an `input.enum`/dynamic symbol can't anchor a single expression clock, so it
96
+ * is omitted, mirroring how an `input.enum` interval can't anchor one), and the
97
+ * callback's single parameter name, and — when `validate` — runs the capture
98
+ * check. A callsite whose interval is not a compile-time literal already emitted
99
+ * `request-security-interval-not-literal` via `readRequestInterval`; it is
100
+ * skipped here (no descriptor).
101
+ */
102
+ function readSecurityExpression(call, sourceFile, sourcePath, checker, diagnostics, validate, inputs, out) {
103
+ const callback = call.arguments[1];
104
+ if (callback === undefined ||
105
+ !(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
106
+ return;
107
+ }
108
+ if (validate) {
109
+ validateSecurityExpr(callback, checker, diagnostics, sourcePath);
110
+ }
35
111
  const opts = call.arguments[0];
36
112
  if (opts === undefined || !ts.isObjectLiteralExpression(opts))
37
113
  return;
114
+ const interval = readLiteralInterval(opts);
115
+ if (interval === null)
116
+ return;
117
+ const symbol = readLiteralSymbol(opts, inputs);
118
+ const firstParam = callback.parameters[0];
119
+ const paramName = firstParam !== undefined && ts.isIdentifier(firstParam.name) ? firstParam.name.text : "";
120
+ out.push(Object.freeze({
121
+ slotId: callsiteIdFor(sourceFile, call, sourcePath),
122
+ ...(symbol === undefined ? {} : { symbol }),
123
+ interval,
124
+ paramName,
125
+ }));
126
+ }
127
+ /**
128
+ * Read the literal `interval` string off a `request.security` opts object, or
129
+ * `null` when it is absent or non-literal. Only string-literal intervals key an
130
+ * expression unit; an `input.enum` interval expands to multiple intervals for
131
+ * the requested-interval list but cannot anchor a single expression clock, so it
132
+ * is treated as non-literal here.
133
+ */
134
+ function readLiteralInterval(opts) {
38
135
  const intervalProperty = opts.properties
39
136
  .filter(ts.isPropertyAssignment)
40
137
  .find((property) => ts.isIdentifier(property.name) && property.name.text === "interval");
41
138
  if (intervalProperty === undefined)
42
- return;
139
+ return null;
43
140
  const initializer = intervalProperty.initializer;
44
- if (ts.isStringLiteral(initializer)) {
45
- intervals.add(initializer.text);
46
- return;
141
+ return ts.isStringLiteral(initializer) ? initializer.text : null;
142
+ }
143
+ /**
144
+ * Read the literal `symbol` off a `request.security` opts object for the
145
+ * expression-descriptor anchor: a string literal or an `input.symbol` default
146
+ * resolves to a concrete symbol; an empty literal, an `input.enum`/dynamic
147
+ * symbol, or an absent property resolves to `undefined` (the chart symbol —
148
+ * an enum/dynamic symbol can't anchor a single expression clock). Never pushes
149
+ * a diagnostic; `readRequestInterval` already reported any dynamic symbol.
150
+ */
151
+ function readLiteralSymbol(opts, inputs) {
152
+ const resolved = resolveOptString(opts, "symbol", inputs);
153
+ if (resolved.kind === "literal" || resolved.kind === "input-default") {
154
+ return resolved.value === "" ? undefined : resolved.value;
47
155
  }
156
+ return undefined;
157
+ }
158
+ function resolveOptString(opts, propName, inputs) {
159
+ const property = opts.properties
160
+ .filter(ts.isPropertyAssignment)
161
+ .find((p) => ts.isIdentifier(p.name) && p.name.text === propName);
162
+ if (property === undefined)
163
+ return { kind: "absent" };
164
+ const initializer = property.initializer;
165
+ if (ts.isStringLiteral(initializer))
166
+ return { kind: "literal", value: initializer.text };
48
167
  const enumOptions = getInputsEnumOptions(initializer, inputs);
49
- if (enumOptions !== null) {
50
- for (const option of enumOptions)
51
- intervals.add(option);
168
+ if (enumOptions !== null)
169
+ return { kind: "enum", values: enumOptions };
170
+ const symbolDefault = getInputSymbolDefault(initializer, inputs);
171
+ if (symbolDefault !== null)
172
+ return { kind: "input-default", value: symbolDefault };
173
+ return { kind: "dynamic", node: initializer };
174
+ }
175
+ function readRequestInterval(call, calleeName, sourceFile, sourcePath, inputs, diagnostics, intervals, feeds) {
176
+ const opts = call.arguments[0];
177
+ if (opts === undefined || !ts.isObjectLiteralExpression(opts))
52
178
  return;
179
+ const intervalProperty = opts.properties
180
+ .filter(ts.isPropertyAssignment)
181
+ .find((property) => ts.isIdentifier(property.name) && property.name.text === "interval");
182
+ if (intervalProperty === undefined)
183
+ return;
184
+ const resolvedIntervals = resolveIntervals(intervalProperty.initializer, inputs);
185
+ if (resolvedIntervals === null) {
186
+ diagnostics.push(createDiagnostic({
187
+ severity: "error",
188
+ code: calleeName === "request.lowerTf"
189
+ ? "request-lower-tf-interval-not-literal"
190
+ : "request-security-interval-not-literal",
191
+ message: `${calleeName}({ interval }) must be a string literal or input.enum value`,
192
+ file: sourcePath,
193
+ node: intervalProperty.initializer,
194
+ sourceFile,
195
+ }));
53
196
  }
54
- diagnostics.push(createDiagnostic({
55
- severity: "error",
56
- code: calleeName === "request.lowerTf"
57
- ? "request-lower-tf-interval-not-literal"
58
- : "request-security-interval-not-literal",
59
- message: `${calleeName}({ interval }) must be a string literal or input.enum value`,
60
- file: sourcePath,
61
- node: initializer,
62
- sourceFile,
63
- }));
197
+ // `request.lowerTf` has no symbol dimension: it only ever feeds intervals
198
+ // (the chart-symbol HTF projection), never `feeds`. Preserve its existing
199
+ // interval-only behavior exactly.
200
+ if (calleeName === "request.lowerTf") {
201
+ for (const interval of resolvedIntervals ?? [])
202
+ intervals.add(interval);
203
+ return;
204
+ }
205
+ const resolvedSymbols = resolveSymbols(opts, inputs, sourceFile, sourcePath, diagnostics);
206
+ for (const symbol of resolvedSymbols) {
207
+ for (const interval of resolvedIntervals ?? []) {
208
+ // A symbol-omitted (chart-symbol) feed keeps its interval in the
209
+ // main-symbol projection; a present-symbol feed does not.
210
+ if (symbol === undefined)
211
+ intervals.add(interval);
212
+ feeds.set(feedKey(symbol, interval), {
213
+ ...(symbol === undefined ? {} : { symbol }),
214
+ interval,
215
+ });
216
+ }
217
+ }
218
+ }
219
+ /**
220
+ * Resolve a `request.*` `interval` initializer to its concrete interval list —
221
+ * a single-element list for a string literal, all options for an `inputs.<enum>`
222
+ * access — or `null` for a genuinely-dynamic interval (the caller pushes the
223
+ * appropriate diagnostic). `interval` never uses the `input.symbol`-default path:
224
+ * `input.interval` is the main-chart interval, not a feed interval.
225
+ */
226
+ function resolveIntervals(initializer, inputs) {
227
+ if (ts.isStringLiteral(initializer))
228
+ return [initializer.text];
229
+ return getInputsEnumOptions(initializer, inputs);
230
+ }
231
+ /**
232
+ * Resolve a `request.security` opts object's `symbol` axis to the list of
233
+ * requested symbols (`undefined` ⇒ the chart's own symbol): `[undefined]` when
234
+ * absent or an empty literal, `[value]` for a string literal or `input.symbol`
235
+ * default, all options for an `inputs.<enum>` access, or `[]` (excluded, after
236
+ * pushing `request-security-symbol-not-literal`) for a dynamic symbol.
237
+ */
238
+ function resolveSymbols(opts, inputs, sourceFile, sourcePath, diagnostics) {
239
+ const resolved = resolveOptString(opts, "symbol", inputs);
240
+ switch (resolved.kind) {
241
+ case "absent":
242
+ return [undefined];
243
+ case "literal":
244
+ // An empty-literal symbol collapses to the chart symbol, matching
245
+ // `feedKey`'s empty-collapse.
246
+ return [resolved.value === "" ? undefined : resolved.value];
247
+ case "input-default":
248
+ return [resolved.value];
249
+ case "enum":
250
+ return resolved.values;
251
+ default:
252
+ diagnostics.push(createDiagnostic({
253
+ severity: "error",
254
+ code: "request-security-symbol-not-literal",
255
+ message: "request.security({ symbol }) must be a string literal, an input.symbol default, or an input.enum value",
256
+ file: sourcePath,
257
+ node: resolved.node,
258
+ sourceFile,
259
+ }));
260
+ return [];
261
+ }
262
+ }
263
+ /**
264
+ * Resolve an `inputs.<name>` access whose descriptor is an `input.symbol` to its
265
+ * `defaultValue` string, or `null` when the access is not an `inputs.<name>`
266
+ * property access, the descriptor is missing / not a `symbol` kind, or its
267
+ * `defaultValue` is not a string.
268
+ */
269
+ function getInputSymbolDefault(expr, inputs) {
270
+ if (!ts.isPropertyAccessExpression(expr) ||
271
+ !ts.isIdentifier(expr.expression) ||
272
+ expr.expression.text !== "inputs") {
273
+ return null;
274
+ }
275
+ const descriptor = inputs[expr.name.text];
276
+ if (descriptor === undefined || descriptor.kind !== "symbol")
277
+ return null;
278
+ const defaultValue = descriptor.defaultValue;
279
+ return typeof defaultValue === "string" ? defaultValue : null;
64
280
  }
65
281
  function getInputsEnumOptions(expr, inputs) {
66
282
  if (!ts.isPropertyAccessExpression(expr) ||
@@ -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 {\n feedKey,\n type RequestedFeed,\n type SecurityExpressionDescriptor,\n} 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 (the **main-symbol** projection), the sorted, deduped\n * list of requested `(symbol?, interval)` {@link RequestedFeed | feeds} (the\n * superset), plus one {@link SecurityExpressionDescriptor} per\n * `request.security({ interval }, (bar) => …)` expression callsite (sorted by\n * `slotId`).\n *\n * `intervals` keeps its exact existing meaning — the symbol-omitted\n * (chart-symbol) higher-timeframe intervals — so existing manifests stay\n * byte-identical. `feeds` adds the symbol dimension: one entry per distinct\n * `(symbol, interval)` pair, deduped + ordered by the shared\n * `feedKey(symbol, interval)` so the printed manifest is byte-stable.\n *\n * @since 0.7\n * @stable\n * @example\n * const r: RequestAnalysis = { intervals: [\"1W\"], feeds: [], securityExpressions: [] };\n * void r;\n */\nexport type RequestAnalysis = Readonly<{\n intervals: ReadonlyArray<string>;\n feeds: ReadonlyArray<RequestedFeed>;\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(...)`, every\n * distinct requested `(symbol?, interval)` feed (`request.security` only —\n * `request.lowerTf` has no symbol), plus every `request.security` *expression*\n * callsite (a second arrow/function argument). Dynamic intervals emit\n * `request-security-interval-not-literal` (for `request.security`) or\n * `request-lower-tf-interval-not-literal` (for `request.lowerTf`); a dynamic\n * `request.security` symbol emits `request-security-symbol-not-literal`. Either\n * dynamic axis is excluded.\n *\n * The `symbol` opt is read the same three ways `interval` is — a string literal,\n * an `inputs.<enum>` access (expanded to all options), or an `inputs.<name>`\n * `input.symbol` default literal — and the cartesian product of resolved\n * symbols × intervals is deduped into `feeds` via the shared\n * `feedKey(symbol, interval)`. A symbol-omitted (or empty-literal) feed keeps its\n * interval in `intervals` (the main-symbol projection); a present-symbol feed\n * does not.\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, feeds, 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 // Keyed by `feedKey(symbol, interval)` so the dedup format matches the\n // runtime/host stream key exactly and the sort below is byte-stable.\n const feeds = new Map<string, RequestedFeed>();\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 feeds,\n );\n }\n if (calleeName === \"request.security\") {\n readSecurityExpression(\n node,\n sourceFile,\n sourcePath,\n checker,\n diagnostics,\n validateExpressions,\n inputs,\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 const sortedFeeds = Array.from(feeds.entries())\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([, feed]) => feed);\n return Object.freeze({\n intervals: Object.freeze(Array.from(intervals).sort()),\n feeds: Object.freeze(sortedFeeds),\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`, the literal `symbol` (string literal or `input.symbol` default —\n * an `input.enum`/dynamic symbol can't anchor a single expression clock, so it\n * is omitted, mirroring how an `input.enum` interval can't anchor one), and the\n * callback's single parameter name, and — when `validate` — runs the capture\n * check. A callsite whose interval is not a compile-time literal already emitted\n * `request-security-interval-not-literal` via `readRequestInterval`; it is\n * 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 inputs: Readonly<Record<string, ExtractedDescriptor>>,\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 opts = call.arguments[0];\n if (opts === undefined || !ts.isObjectLiteralExpression(opts)) return;\n const interval = readLiteralInterval(opts);\n if (interval === null) return;\n const symbol = readLiteralSymbol(opts, inputs);\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 ...(symbol === undefined ? {} : { symbol }),\n interval,\n paramName,\n }),\n );\n}\n\n/**\n * Read the literal `interval` string off a `request.security` opts object, or\n * `null` when it is absent or non-literal. Only string-literal intervals key an\n * expression unit; an `input.enum` interval expands to multiple intervals for\n * the requested-interval list but cannot anchor a single expression clock, so it\n * is treated as non-literal here.\n */\nfunction readLiteralInterval(opts: ts.ObjectLiteralExpression): string | 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\n/**\n * Read the literal `symbol` off a `request.security` opts object for the\n * expression-descriptor anchor: a string literal or an `input.symbol` default\n * resolves to a concrete symbol; an empty literal, an `input.enum`/dynamic\n * symbol, or an absent property resolves to `undefined` (the chart symbol —\n * an enum/dynamic symbol can't anchor a single expression clock). Never pushes\n * a diagnostic; `readRequestInterval` already reported any dynamic symbol.\n */\nfunction readLiteralSymbol(\n opts: ts.ObjectLiteralExpression,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n): string | undefined {\n const resolved = resolveOptString(opts, \"symbol\", inputs);\n if (resolved.kind === \"literal\" || resolved.kind === \"input-default\") {\n return resolved.value === \"\" ? undefined : resolved.value;\n }\n return undefined;\n}\n\n/**\n * Resolution of an opts string property read three ways (mirroring `interval`,\n * plus the `input.symbol`-default path symbols need): a string literal, the\n * options of an `inputs.<enum>` access, the default of an `inputs.<name>`\n * `input.symbol` access, an absent property, or a genuinely-dynamic expression.\n */\ntype ResolvedOptString =\n | Readonly<{ kind: \"literal\"; value: string }>\n | Readonly<{ kind: \"enum\"; values: ReadonlyArray<string> }>\n | Readonly<{ kind: \"input-default\"; value: string }>\n | Readonly<{ kind: \"absent\" }>\n | Readonly<{ kind: \"dynamic\"; node: ts.Expression }>;\n\nfunction resolveOptString(\n opts: ts.ObjectLiteralExpression,\n propName: string,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n): ResolvedOptString {\n const property = opts.properties\n .filter(ts.isPropertyAssignment)\n .find((p) => ts.isIdentifier(p.name) && p.name.text === propName);\n if (property === undefined) return { kind: \"absent\" };\n\n const initializer = property.initializer;\n if (ts.isStringLiteral(initializer)) return { kind: \"literal\", value: initializer.text };\n\n const enumOptions = getInputsEnumOptions(initializer, inputs);\n if (enumOptions !== null) return { kind: \"enum\", values: enumOptions };\n\n const symbolDefault = getInputSymbolDefault(initializer, inputs);\n if (symbolDefault !== null) return { kind: \"input-default\", value: symbolDefault };\n\n return { kind: \"dynamic\", node: initializer };\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 feeds: Map<string, RequestedFeed>,\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 resolvedIntervals = resolveIntervals(intervalProperty.initializer, inputs);\n if (resolvedIntervals === null) {\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: intervalProperty.initializer,\n sourceFile,\n }),\n );\n }\n\n // `request.lowerTf` has no symbol dimension: it only ever feeds intervals\n // (the chart-symbol HTF projection), never `feeds`. Preserve its existing\n // interval-only behavior exactly.\n if (calleeName === \"request.lowerTf\") {\n for (const interval of resolvedIntervals ?? []) intervals.add(interval);\n return;\n }\n\n const resolvedSymbols = resolveSymbols(opts, inputs, sourceFile, sourcePath, diagnostics);\n for (const symbol of resolvedSymbols) {\n for (const interval of resolvedIntervals ?? []) {\n // A symbol-omitted (chart-symbol) feed keeps its interval in the\n // main-symbol projection; a present-symbol feed does not.\n if (symbol === undefined) intervals.add(interval);\n feeds.set(feedKey(symbol, interval), {\n ...(symbol === undefined ? {} : { symbol }),\n interval,\n });\n }\n }\n}\n\n/**\n * Resolve a `request.*` `interval` initializer to its concrete interval list —\n * a single-element list for a string literal, all options for an `inputs.<enum>`\n * access — or `null` for a genuinely-dynamic interval (the caller pushes the\n * appropriate diagnostic). `interval` never uses the `input.symbol`-default path:\n * `input.interval` is the main-chart interval, not a feed interval.\n */\nfunction resolveIntervals(\n initializer: ts.Expression,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n): ReadonlyArray<string> | null {\n if (ts.isStringLiteral(initializer)) return [initializer.text];\n return getInputsEnumOptions(initializer, inputs);\n}\n\n/**\n * Resolve a `request.security` opts object's `symbol` axis to the list of\n * requested symbols (`undefined` ⇒ the chart's own symbol): `[undefined]` when\n * absent or an empty literal, `[value]` for a string literal or `input.symbol`\n * default, all options for an `inputs.<enum>` access, or `[]` (excluded, after\n * pushing `request-security-symbol-not-literal`) for a dynamic symbol.\n */\nfunction resolveSymbols(\n opts: ts.ObjectLiteralExpression,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n sourceFile: ts.SourceFile,\n sourcePath: string,\n diagnostics: CompileDiagnostic[],\n): ReadonlyArray<string | undefined> {\n const resolved = resolveOptString(opts, \"symbol\", inputs);\n switch (resolved.kind) {\n case \"absent\":\n return [undefined];\n case \"literal\":\n // An empty-literal symbol collapses to the chart symbol, matching\n // `feedKey`'s empty-collapse.\n return [resolved.value === \"\" ? undefined : resolved.value];\n case \"input-default\":\n return [resolved.value];\n case \"enum\":\n return resolved.values;\n default:\n diagnostics.push(\n createDiagnostic({\n severity: \"error\",\n code: \"request-security-symbol-not-literal\",\n message:\n \"request.security({ symbol }) must be a string literal, an input.symbol default, or an input.enum value\",\n file: sourcePath,\n node: resolved.node,\n sourceFile,\n }),\n );\n return [];\n }\n}\n\n/**\n * Resolve an `inputs.<name>` access whose descriptor is an `input.symbol` to its\n * `defaultValue` string, or `null` when the access is not an `inputs.<name>`\n * property access, the descriptor is missing / not a `symbol` kind, or its\n * `defaultValue` is not a string.\n */\nfunction getInputSymbolDefault(\n expr: ts.Expression,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n): 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 !== \"symbol\") return null;\n const defaultValue = descriptor.defaultValue;\n return typeof defaultValue === \"string\" ? defaultValue : null;\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
  }