@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,251 @@
1
1
  # @invinite-org/chartlang-compiler
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 850ae21: Add `bar.point(offset, price)` — index authoring sugar for anchoring drawings
8
+ by bar offset instead of an absolute timestamp.
9
+
10
+ `bar.point` resolves the offset to the existing time-based `WorldPoint`
11
+ (`{ time, price }`) at compute time, so it composes directly with every
12
+ `draw.*` anchor argument and introduces no new wire format or anchor union:
13
+
14
+ - `bar.point(0, price)` — the current bar.
15
+ - `bar.point(-n, price)` — `n` bars back, using the real historical timestamp
16
+ from the runtime's time ring buffer (`NaN` time past retained history; never
17
+ throws).
18
+ - `bar.point(n, price)` — a future bar, with the time extrapolated from the
19
+ median recent bar spacing (falling back to the parsed bar interval when
20
+ fewer than two bars are retained).
21
+
22
+ The compiler's max-lookback analysis now counts a negative integer-literal
23
+ `bar.point(-n, …)` offset toward `maxLookback` exactly like a `series[n]`
24
+ lookback, so the runtime sizes the time buffer deeply enough; positive (future)
25
+ offsets and dynamic offsets contribute no extra depth. The recogniser peels
26
+ parentheses, so the converter's emitted form `bar.point(-(n), …)` is sized
27
+ identically to a hand-written `bar.point(-n, …)` (without it, a converted
28
+ historical tracking line sized its buffer to 0 and resolved to a NaN anchor).
29
+
30
+ The Pine v6 converter now lowers `bar_index` drawing anchors to
31
+ `bar.point(<signed offset>, <price>)` and drops the dead `__BAR_INTERVAL_MS`
32
+ sentinel and its `bar.time ± (N * __BAR_INTERVAL_MS)` arithmetic — future
33
+ anchors resolve at runtime instead of needing a host-supplied bar interval.
34
+
35
+ - ca19e20: Bidirectional plot `offset` — negative offsets shift a plotted series left.
36
+
37
+ `offset` becomes a presentation-only **display shift** in bars with the
38
+ fixed sign convention `+n` = right (future), `−n` = left (past); the
39
+ numeric series value is unshifted. This replaces the old value-read model
40
+ (where a positive offset made `series.current` read the value N bars ago
41
+ and a negative offset resolved to `NaN`). The `*Opts` `offset` JSDoc (and
42
+ ALMA's `barShift`) now describe both directions and drop the old
43
+ "negative ⇒ NaN" wording (`AlmaOpts.offset`, the Gaussian-centre
44
+ position, is unchanged).
45
+
46
+ `PlotEmission` gains an optional presentation field `xShift?: number`
47
+ (signed integer bars; omitted/`0` ≡ no shift, so a no-shift emission is
48
+ byte-identical to today). `validateEmission` rejects a non-integer
49
+ `xShift`. The compiler no longer counts `offset` toward `maxLookback`
50
+ (the value is no longer read from a deeper slot). The runtime threads the
51
+ declared offset onto the emission as `xShift` (reading a
52
+ `WeakMap<Series, number>` offset tag set by `makeShiftedSeriesView`; ALMA
53
+ tags `opts.barShift`) and stops the old value-read shift so
54
+ `series.current` is unshifted; the reference adapter renders it by
55
+ projecting `xShift` onto the x-axis (extending the viewport for
56
+ future-shifted points).
57
+
58
+ The Pine converter now maps `plot(<ta.* call>, offset=N)` onto the
59
+ emitted `ta.*` call's `offset` opt (signed, both directions); a plot
60
+ whose value is not a direct `ta.*` call drops the offset and emits the
61
+ new `plot-offset-needs-ta-call` warning, and a plot-level offset
62
+ replacing the ta call's own `offset=` emits `plot-offset-overrides-ta-offset`.
63
+
64
+ The conformance harness's `plot-field` assertion gains an `xShift` field,
65
+ and a new scenario pins both shift directions plus the unshifted value
66
+ series.
67
+
68
+ - 3541445: Size series-index buffers precisely for provably-bounded indices.
69
+
70
+ `extractMaxLookback` now resolves a series read at a literal, a
71
+ bounded-`for` induction variable (`for (let i = 0; i < N; i++) src[i]`),
72
+ a `const` numeric literal, or an affine combination of those
73
+ (`src[i + 1]`, `src[K - i]`, `src[2 * i]`) to its exact `maxLookback`
74
+ contribution via a new compile-time interval resolver
75
+ (`resolveIndexUpperBound`) sharing one `parseBoundedForLoop` helper with
76
+ `forbiddenConstructs`. These indices no longer emit the
77
+ `dynamic-series-index` warning or force the 5000-slot `dynamicFallback`
78
+ buffer — they size the ring buffer exactly like a literal lookback. The
79
+ resolver over-approximates (never under-sizes); genuinely dynamic indices
80
+ (unbounded variables, unsupported operators, non-terminating loops,
81
+ reassigned loop variables) keep the warning + fallback. A new
82
+ `loop-sma` conformance scenario pins a `for`-loop SMA as bar-for-bar
83
+ identical to `ta.sma(close, 5)`.
84
+
85
+ - 6235ad7: Make the compute bar's OHLCV + derived fields directly indexable as a series.
86
+
87
+ `bar.close`, `bar.open`, `bar.high`, `bar.low`, `bar.volume`, and the derived
88
+ `bar.hl2` / `bar.hlc3` / `bar.ohlc4` / `bar.hlcc4` are now `PriceSeries` /
89
+ `VolumeSeries` (`number & Series<number>`) on the bar passed to `compute`
90
+ (`ComputeContext.bar`, typed as the new `BarSeries`). Each field is **both** a
91
+ scalar — `bar.close * 2`, `plot(bar.close)`, `ta.ema(bar.close, 20)` keep
92
+ working unchanged — **and** an indexable series, so a script can read prior
93
+ bars directly:
94
+
95
+ ```ts
96
+ const sma5 =
97
+ (bar.close[0] + bar.close[1] + bar.close[2] + bar.close[3] + bar.close[4]) /
98
+ 5;
99
+ ```
100
+
101
+ This removes the `ta.ema(bar.close, 1)` identity-trick that scripts previously
102
+ needed to "republish" a scalar price as an indexable `Series`.
103
+
104
+ The adapter-supplied candle type `Bar` (and `request.lowerTf` intrabar bars) is
105
+ unchanged — it stays scalar OHLCV; only the streaming `compute` bar gains the
106
+ series shape. `request.security`'s higher-timeframe bar remains the separate
107
+ `SecurityBar`.
108
+
109
+ Migration note: because the field is now an object, `Number.isFinite(bar.close)`
110
+ is always `false` (it does not coerce) and `bar.close === 42` is `false` (object
111
+ vs number). Use `bar.close.current` or `+bar.close` in those raw-number
112
+ contexts. `bar.point(0, bar.close)` continues to work — the runtime coerces the
113
+ anchor price to a scalar.
114
+
115
+ - 3bf391a: Add the `draw.fillBetween(edgeA, edgeB, opts?)` drawing primitive — a
116
+ native filled ribbon between two edges (the closed polygon `edgeA`
117
+ forward then `edgeB` reversed). It is the chartlang equivalent of Pine's
118
+ `linefill.new(line1, line2, color)` / `fill(plot1, plot2)`. The
119
+ pine-converter now lowers static two-line `linefill.new` to it instead of
120
+ approximating with `draw.rotatedRectangle`, retiring the
121
+ `linefill-rotatedrect-approximated` diagnostic.
122
+ - 8086003: Add an optional presentation-only `z` (render-order / z-index) option to
123
+ `plot()` and every `draw.*` primitive. Default `0`; higher renders on
124
+ top, ties fall back to the existing group + declaration order. Finite
125
+ numbers only. Affects stacking only — values, alerts, and `state.*` are
126
+ unchanged.
127
+
128
+ Adapter kit: `PlotEmission` and `DrawingEmission` gain the matching
129
+ presentation-only `z?: number` wire field, validated by
130
+ `validateEmission` as a finite number (NaN / ±Infinity rejected;
131
+ fractional and negative allowed). Omitted/`0` stays byte-identical to a
132
+ pre-feature emission, so existing goldens and conformance hashes are
133
+ untouched.
134
+
135
+ Runtime: `plotImpl` reads `opts.z`, and the drawing-emit path
136
+ (`createDrawingHandle`) lifts `z` out of `state.style` — into a shallow
137
+ clone with `z` removed, where the per-kind `draw.*` impls fold the opts
138
+ bag — and threads it onto the top-level `PlotEmission.z` /
139
+ `DrawingEmission.z` with the same omit-when-`0` conditional spread used
140
+ for `xShift`. `z` is persisted **beside** the drawing slot's `state`
141
+ (never inside `DrawingState`), so an `update` retains the last value. A
142
+ no-`z` plot or drawing emits no `z` key — byte-identical to the
143
+ pre-feature baseline. `draw.table` / `draw.group` do not carry `z` in
144
+ v1.
145
+
146
+ Pine converter: `explicit_plot_zorder` is now a recognized no-op instead
147
+ of an unmapped warning. chartlang already layers marks by declaration
148
+ order within their group (the normative ordering contract), which is
149
+ exactly what Pine's `explicit_plot_zorder=true` makes authoritative — so
150
+ the flag is satisfied by default and needs no chartlang option.
151
+ `mapDeclarationArgs` no longer raises `indicator-arg-not-mapped` for it;
152
+ instead it emits a single `explicit-plot-zorder-default` info note
153
+ (covering both `explicit_plot_zorder=true` and the Pine-default
154
+ `=false`). The converter still never _emits_ a numeric `z` — Pine has no
155
+ per-element z source construct. Other unmapped `indicator(...)` args
156
+ (`timeframe`, etc.) keep warning.
157
+
158
+ Compiler: the ambient `@invinite-org/chartlang-core` `.d.ts` shim gains a
159
+ `ZOrdered { z?: number }` mixin intersected into `PlotOpts` and every
160
+ `draw.*` option type (mirroring core's `drawingStyle.ts`), so a compiled
161
+ script's `plot(value, { z })` **and** `draw.*(…, { z })` type-check (the
162
+ shim stays in lockstep with core).
163
+
164
+ Conformance: a new `z-order` scenario pins the plot `z` →
165
+ `PlotEmission.z` wire contract — a `plot(value, { z: -1 })` emits
166
+ `z: -1`, a no-`z` plot omits the field (omit-when-`0` byte-identity), and
167
+ a value-hash proves `z` never transforms the series. The `plot-field`
168
+ assertion's `field` union widens to also accept `"z"`.
169
+
170
+ - 073f41b: Add the higher-timeframe expression/callback overload to `request.security`.
171
+ Alongside the existing data form `request.security({ interval })` →
172
+ `SecurityBar`, scripts can now write `request.security({ interval }, (bar) =>
173
+ …)` → `Series<number>`, where the callback runs on the **higher-timeframe
174
+ clock** — `request.security({ interval: "1W" }, (bar) => ta.ema(bar.close, 20))`
175
+ is a true weekly EMA(20) (20 weekly bars), not 20 main bars of a weekly-stepped
176
+ series. The result is aligned no-lookahead down to the main timeline.
177
+
178
+ - **core** — the `SecurityExpr` callback type (re-exported from the package
179
+ root), the second `security` overload, and the shared `statefulPrimitives`
180
+ entry annotated as covering both arities.
181
+ - **compiler** — records one `SecurityExpressionDescriptor { slotId, interval,
182
+ paramName }` per expression callsite in `manifest.securityExpressions`
183
+ (sorted by `slotId`, omitted for the data-only form), and validates each
184
+ callback against the allowed subset — its `bar` parameter and body locals,
185
+ the ambient `ta` / `inputs`, safe `Math.*` globals, and literals — rejecting
186
+ any captured outer binding with the new
187
+ `request-security-expr-captures-local` diagnostic.
188
+ - **runtime** — mounts one `SecurityExprRunner` per manifest entry: the
189
+ callback is captured lazily on the first main compute, driven once per HTF bar
190
+ close through a dedicated fold `StreamState` so `ta.*` accumulate on the HTF
191
+ clock, and one sampled value per HTF bar feeds a per-slot output buffer that
192
+ `request.security(opts, expr)` returns aligned no-lookahead to the main
193
+ timeline. Capability / interval / stream fallbacks return an all-NaN series
194
+ with a deduped diagnostic.
195
+ - **host-worker / host-quickjs** — boot the expression form unchanged; the
196
+ `__manifest` sidecar already carries `securityExpressions`.
197
+ - **pine-converter** — Pine's `request.security(sym, "D", ta.ema(close, 9))`
198
+ now lowers to the chartlang callback form
199
+ `request.security({ interval: "1d" }, (bar) => ta.ema(bar.close, 9))` (a bare
200
+ OHLCV third arg keeps lowering to the data form).
201
+ - **conformance** — new scenarios prove the weekly expression value differs
202
+ from a same-length main-timeframe EMA, plus the `multiTimeframe: false` NaN
203
+ fallback.
204
+
205
+ - 5a9c24d: Add `state.series(init)` — a writable, indexable user series. Store an
206
+ arbitrary value each bar (`s.value = expr`) and read its history N bars
207
+ back (`s[1]`). Number-coercible (`+s`, `s.current`) and usable as a `ta.*`
208
+ source. The Pine converter lowers a history-indexed `var` to it.
209
+ - 08c536c: Add the `ta.highestbars` / `ta.lowestbars` primitives plus the cross-package
210
+ wiring that makes them usable as drawing anchors and Pine-converter targets.
211
+
212
+ - **core / runtime:** `ta.highestbars(source, length, opts?)` and
213
+ `ta.lowestbars(source, length, opts?)` return the bar OFFSET (≤ 0) to the
214
+ highest / lowest `source` value over the trailing `length` bars (window
215
+ INCLUDES the current bar). `0` → current bar is the extreme; `-k` → the
216
+ extreme occurred `k` bars ago. Ties resolve to the most recent bar; NaN
217
+ inputs are skipped; warmup is `length − 1` bars; tick-mode replays the
218
+ in-progress head as the offset-0 candidate. Registered in
219
+ `STATEFUL_PRIMITIVES` (now 174 entries) and `TA_REGISTRY` (now 96 entries).
220
+ - **compiler:** a literal-length `ta.highestbars` / `ta.lowestbars` call
221
+ contributes `length − 1` toward `maxLookback`, so the runtime sizes the time
222
+ ring buffer deep enough for a `bar.point(<that offset>, …)` anchor to resolve.
223
+ A non-literal length contributes 0.
224
+ - **pine-converter:** `ta.highestbars` / `ta.lowestbars` now map to the real
225
+ chartlang primitives (previously lossy passthroughs to `ta.highest` /
226
+ `ta.lowest`). **Behavior change:** a DYNAMIC `bar_index + <non-literal>`
227
+ drawing-x anchor no longer raises the hard `requires-bar-interval` error —
228
+ the offset is resolved by `bar.point` at runtime sign-agnostically (a
229
+ negative runtime offset, e.g. what `ta.highestbars` returns, resolves to the
230
+ historical timestamp via the time buffer). Only the literal `bar_index + N`
231
+ future case still requires a bar interval.
232
+ - **conformance:** new `TA_HIGHEST_LOWEST_BARS_SCENARIO` export pins both
233
+ primitives end-to-end through the compiler + runtime over the bundled
234
+ `goldenBars.json` fixture, and is added to `ALL_SCENARIOS`.
235
+
236
+ ### Patch Changes
237
+
238
+ - Updated dependencies [850ae21]
239
+ - Updated dependencies [ca19e20]
240
+ - Updated dependencies [6235ad7]
241
+ - Updated dependencies [3bf391a]
242
+ - Updated dependencies [8086003]
243
+ - Updated dependencies [850ae21]
244
+ - Updated dependencies [073f41b]
245
+ - Updated dependencies [5a9c24d]
246
+ - Updated dependencies [08c536c]
247
+ - @invinite-org/chartlang-core@1.2.0
248
+
3
249
  ## 1.2.1
4
250
 
5
251
  ### Patch Changes
@@ -24,7 +24,8 @@ export type ExtractMaxLookbackResult = Readonly<{
24
24
  * Walk the source file's `ElementAccessExpression` nodes and infer
25
25
  * `maxLookback` plus any `dynamicFallback` capacity from non-literal index
26
26
  * reads on Phase-1 series shapes: `bar.<ohlcv>[N]`, `ta.<name>(...)[N]`,
27
- * and identifier-bound series variables (`const e = ta.ema(...); e[N];`).
27
+ * and identifier-bound series variables (`const e = ta.ema(...); e[N];` or
28
+ * `const s = state.series(...); s[N];`).
28
29
  *
29
30
  * The optional `scope` parameter narrows both the series-variable
30
31
  * collection and the lookback walk to a single AST subtree (typically
@@ -1 +1 @@
1
- {"version":3,"file":"extractMaxLookback.d.ts","sourceRoot":"","sources":["../../src/analysis/extractMaxLookback.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAK7E;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,WAAW,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CACjD,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,OAAO,EAAE,EAAE,CAAC,WAAW,EACvB,UAAU,EAAE,MAAM,EAClB,KAAK,GAAE,EAAE,CAAC,IAAiB,GAC5B,wBAAwB,CAuC1B"}
1
+ {"version":3,"file":"extractMaxLookback.d.ts","sourceRoot":"","sources":["../../src/analysis/extractMaxLookback.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAO7E;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,WAAW,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CACjD,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,OAAO,EAAE,EAAE,CAAC,WAAW,EACvB,UAAU,EAAE,MAAM,EAClB,KAAK,GAAE,EAAE,CAAC,IAAiB,GAC5B,wBAAwB,CAmD1B"}
@@ -3,12 +3,15 @@
3
3
  import ts from "typescript";
4
4
  import { createDiagnostic } from "../diagnostics.js";
5
5
  import { resolveCalleeName } from "../transformers/resolveCallee.js";
6
+ import { unwrapParens } from "./loopBounds.js";
7
+ import { collectConstNumberEnv, resolveIndexUpperBound } from "./resolveIndexBound.js";
6
8
  const OHLCV_FIELDS = new Set(["close", "open", "high", "low", "volume", "time"]);
7
9
  /**
8
10
  * Walk the source file's `ElementAccessExpression` nodes and infer
9
11
  * `maxLookback` plus any `dynamicFallback` capacity from non-literal index
10
12
  * reads on Phase-1 series shapes: `bar.<ohlcv>[N]`, `ta.<name>(...)[N]`,
11
- * and identifier-bound series variables (`const e = ta.ema(...); e[N];`).
13
+ * and identifier-bound series variables (`const e = ta.ema(...); e[N];` or
14
+ * `const s = state.series(...); s[N];`).
12
15
  *
13
16
  * The optional `scope` parameter narrows both the series-variable
14
17
  * collection and the lookback walk to a single AST subtree (typically
@@ -28,13 +31,27 @@ export function extractMaxLookback(sourceFile, checker, sourcePath, scope = sour
28
31
  const diagnostics = [];
29
32
  const seriesVarNames = collectSeriesVarNames(scope, checker);
30
33
  const visit = (node) => {
34
+ if (ts.isCallExpression(node)) {
35
+ const calleeName = resolveCalleeName(node, checker);
36
+ if (calleeName?.startsWith("ta.")) {
37
+ const barsDepth = readHighestLowestBarsDepth(calleeName, node);
38
+ if (barsDepth > maxLookback)
39
+ maxLookback = barsDepth;
40
+ }
41
+ if (isBarPointCall(node)) {
42
+ const depth = readBarPointLookback(node);
43
+ if (depth > maxLookback)
44
+ maxLookback = depth;
45
+ }
46
+ }
31
47
  if (ts.isElementAccessExpression(node)) {
32
48
  if (isSeriesShapedAccess(node, checker, seriesVarNames)) {
33
49
  const argument = node.argumentExpression;
34
- if (ts.isNumericLiteral(argument)) {
35
- const n = Number(argument.text);
36
- if (Number.isFinite(n) && n > maxLookback)
37
- maxLookback = n;
50
+ const constEnv = collectConstNumberEnv(argument, scope);
51
+ const bound = resolveIndexUpperBound(argument, node, { constEnv, checker });
52
+ if (bound !== null) {
53
+ if (bound > maxLookback)
54
+ maxLookback = bound;
38
55
  }
39
56
  else {
40
57
  diagnostics.push(createDiagnostic({
@@ -58,6 +75,46 @@ export function extractMaxLookback(sourceFile, checker, sourcePath, scope = sour
58
75
  diagnostics: Object.freeze(diagnostics.slice()),
59
76
  });
60
77
  }
78
+ /**
79
+ * Whether a call is a `bar.point(…)` invocation. Matched textually on the
80
+ * `bar.point` property-access shape — the same OHLCV-style textual recognition
81
+ * `isSeriesShapedAccess` uses — so it fires for both the destructured
82
+ * `compute({ bar })` binding and a `declare const bar: Bar` test fixture.
83
+ */
84
+ function isBarPointCall(call) {
85
+ const expression = call.expression;
86
+ return (ts.isPropertyAccessExpression(expression) &&
87
+ expression.name.text === "point" &&
88
+ ts.isIdentifier(expression.expression) &&
89
+ expression.expression.text === "bar");
90
+ }
91
+ /**
92
+ * The historical-lookback depth a `bar.point(offset, …)` call contributes,
93
+ * or `0` when it reads the current / a future bar. A negative integer-literal
94
+ * first argument (`bar.point(-N, …)` — or the converter's parenthesised
95
+ * `bar.point(-(N), …)`) anchors `N` bars back, so the runtime's time ring
96
+ * buffer must retain `N` extra slots — exactly like a `series[N]` lookback.
97
+ * `bar.point(0, …)` (current) and positive offsets (future, extrapolated, no
98
+ * buffer depth) contribute `0`; a non-literal / dynamic offset (e.g. a bound
99
+ * `-k` or a computed `-(2 + 3)`) cannot be sized at compile time and also
100
+ * contributes `0` (reads past retention degrade to a NaN time at runtime, per
101
+ * `bar.point`'s contract).
102
+ */
103
+ function readBarPointLookback(call) {
104
+ const first = call.arguments[0];
105
+ if (first === undefined)
106
+ return 0;
107
+ const expr = unwrapParens(first);
108
+ if (ts.isPrefixUnaryExpression(expr) && expr.operator === ts.SyntaxKind.MinusToken) {
109
+ const operand = unwrapParens(expr.operand);
110
+ if (ts.isNumericLiteral(operand)) {
111
+ const n = Number(operand.text);
112
+ if (Number.isFinite(n) && n > 0)
113
+ return n;
114
+ }
115
+ }
116
+ return 0;
117
+ }
61
118
  function collectSeriesVarNames(scope, checker) {
62
119
  const names = new Set();
63
120
  const visit = (node) => {
@@ -65,7 +122,14 @@ function collectSeriesVarNames(scope, checker) {
65
122
  const initializer = node.initializer;
66
123
  if (initializer && ts.isCallExpression(initializer)) {
67
124
  const calleeName = resolveCalleeName(initializer, checker);
68
- if (calleeName?.startsWith("ta.")) {
125
+ // A `state.series(...)`-bound variable is series-shaped just
126
+ // like a `ta.*`-bound one: `s[N]` reads the slot's ring buffer,
127
+ // so its literal index must fold into `maxLookback`. Matched on
128
+ // the resolved callee name (the slot-injection path) so an
129
+ // element-access form like `state["series"](...)` is not
130
+ // recognised — that form is rejected upstream as
131
+ // `stateful-call-element-access`.
132
+ if (calleeName?.startsWith("ta.") || calleeName === "state.series") {
69
133
  names.add(node.name.text);
70
134
  }
71
135
  }
@@ -75,6 +139,26 @@ function collectSeriesVarNames(scope, checker) {
75
139
  visit(scope);
76
140
  return names;
77
141
  }
142
+ /**
143
+ * The historical-lookback depth a `ta.highestbars` / `ta.lowestbars` call
144
+ * contributes. Both primitives return the bar OFFSET (≤ 0) to the extreme
145
+ * over the trailing `length`-bar window, so the deepest offset they can
146
+ * return is `-(length − 1)`. A downstream `bar.point(<that offset>, …)`
147
+ * anchor reads `time.at(length − 1)`, so the runtime's time ring buffer
148
+ * must retain `length − 1` slots. Only a LITERAL second positional `length`
149
+ * arg can be sized at compile time; a non-literal length contributes `0`.
150
+ */
151
+ function readHighestLowestBarsDepth(calleeName, call) {
152
+ if (calleeName !== "ta.highestbars" && calleeName !== "ta.lowestbars")
153
+ return 0;
154
+ const lengthArg = call.arguments[1];
155
+ if (lengthArg === undefined || !ts.isNumericLiteral(lengthArg))
156
+ return 0;
157
+ const length = Number(lengthArg.text);
158
+ if (!Number.isFinite(length) || length <= 1)
159
+ return 0;
160
+ return length - 1;
161
+ }
78
162
  function isSeriesShapedAccess(node, checker, seriesVarNames) {
79
163
  const expression = node.expression;
80
164
  if (ts.isPropertyAccessExpression(expression)) {
@@ -1 +1 @@
1
- {"version":3,"file":"extractMaxLookback.js","sourceRoot":"","sources":["../../src/analysis/extractMaxLookback.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+DAA+D;AAE/D,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAA0B,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAErE,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;AAuBjF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAC9B,UAAyB,EACzB,OAAuB,EACvB,UAAkB,EAClB,QAAiB,UAAU;IAE3B,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,MAAM,gBAAgB,GAA2B,EAAE,CAAC;IACpD,MAAM,WAAW,GAAwB,EAAE,CAAC;IAE5C,MAAM,cAAc,GAAG,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAE7D,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;QAClC,IAAI,EAAE,CAAC,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAC;YACrC,IAAI,oBAAoB,CAAC,IAAI,EAAE,OAAO,EAAE,cAAc,CAAC,EAAE,CAAC;gBACtD,MAAM,QAAQ,GAAG,IAAI,CAAC,kBAAkB,CAAC;gBACzC,IAAI,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC;oBAChC,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;oBAChC,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,WAAW;wBAAE,WAAW,GAAG,CAAC,CAAC;gBAC/D,CAAC;qBAAM,CAAC;oBACJ,WAAW,CAAC,IAAI,CACZ,gBAAgB,CAAC;wBACb,QAAQ,EAAE,SAAS;wBACnB,IAAI,EAAE,sBAAsB;wBAC5B,OAAO,EACH,oFAAoF;wBACxF,IAAI,EAAE,UAAU;wBAChB,IAAI,EAAE,QAAQ;wBACd,UAAU;qBACb,CAAC,CACL,CAAC;oBACF,gBAAgB,CAAC,eAAe,GAAG,IAAI,CAAC;gBAC5C,CAAC;YACL,CAAC;QACL,CAAC;QACD,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACjC,CAAC,CAAC;IACF,KAAK,CAAC,KAAK,CAAC,CAAC;IAEb,OAAO,MAAM,CAAC,MAAM,CAAC;QACjB,WAAW;QACX,gBAAgB,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,gBAAgB,EAAE,CAAC;QACxD,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;KAClD,CAAC,CAAC;AACP,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAc,EAAE,OAAuB;IAClE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;QAClC,IAAI,EAAE,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/D,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;YACrC,IAAI,WAAW,IAAI,EAAE,CAAC,gBAAgB,CAAC,WAAW,CAAC,EAAE,CAAC;gBAClD,MAAM,UAAU,GAAG,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;gBAC3D,IAAI,UAAU,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;oBAChC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC9B,CAAC;YACL,CAAC;QACL,CAAC;QACD,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACjC,CAAC,CAAC;IACF,KAAK,CAAC,KAAK,CAAC,CAAC;IACb,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,SAAS,oBAAoB,CACzB,IAAgC,EAChC,OAAuB,EACvB,cAAmC;IAEnC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;IACnC,IAAI,EAAE,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5C,IAAI,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IAC5D,CAAC;IACD,IAAI,EAAE,CAAC,gBAAgB,CAAC,UAAU,CAAC,EAAE,CAAC;QAClC,MAAM,UAAU,GAAG,iBAAiB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAC1D,IAAI,UAAU,EAAE,UAAU,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IACnD,CAAC;IACD,IAAI,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,IAAI,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACzD,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nimport ts from \"typescript\";\n\nimport { type CompileDiagnostic, createDiagnostic } from \"../diagnostics.js\";\nimport { resolveCalleeName } from \"../transformers/resolveCallee.js\";\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,54 @@
1
+ import 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 plus one {@link SecurityExpressionDescriptor} per
8
+ * `request.security({ interval }, (bar) => …)` expression callsite (sorted by
9
+ * `slotId`).
10
+ *
11
+ * @since 0.7
12
+ * @stable
13
+ * @example
14
+ * const r: RequestAnalysis = { intervals: ["1W"], securityExpressions: [] };
15
+ * void r;
16
+ */
17
+ export type RequestAnalysis = Readonly<{
18
+ intervals: ReadonlyArray<string>;
19
+ securityExpressions: ReadonlyArray<SecurityExpressionDescriptor>;
20
+ }>;
21
+ /**
22
+ * Walk a script's AST and collect every static `interval` argument to
23
+ * `request.security({ interval: ... })` and `request.lowerTf(...)`, plus every
24
+ * `request.security` *expression* callsite (a second arrow/function argument).
25
+ * Dynamic intervals emit `request-security-interval-not-literal` (for
26
+ * `request.security`) or `request-lower-tf-interval-not-literal` (for
27
+ * `request.lowerTf`) and are excluded.
28
+ *
29
+ * Each expression callsite is recorded as a {@link SecurityExpressionDescriptor}
30
+ * keyed by the same `slotId` the callsite-id transformer injects (via the
31
+ * shared `callsiteIdFor` helper) so the runtime can match the manifest entry
32
+ * to the inlined callback. When `validateExpressions` is `true`, each callback
33
+ * is also run through {@link validateSecurityExpr}, pushing
34
+ * `request-security-expr-captures-local` for any out-of-subset reference.
35
+ *
36
+ * @since 0.7
37
+ * @stable
38
+ * @example
39
+ * // const { intervals, securityExpressions } =
40
+ * // extractRequestAnalysis(sf, checker, inputs, diagnostics, path, true);
41
+ * const fn: typeof extractRequestAnalysis = extractRequestAnalysis;
42
+ * void fn;
43
+ */
44
+ export declare function extractRequestAnalysis(sourceFile: ts.SourceFile, checker: ts.TypeChecker, inputs: Readonly<Record<string, ExtractedDescriptor>>, diagnostics: CompileDiagnostic[], sourcePath?: string, validateExpressions?: boolean): RequestAnalysis;
4
45
  /**
5
46
  * Walk a script's AST and collect every static `interval` argument to
6
47
  * `request.security({ interval: ... })` and `request.lowerTf(...)`. Dynamic
7
48
  * arguments emit `request-security-interval-not-literal` (for `request.security`)
8
49
  * or `request-lower-tf-interval-not-literal` (for `request.lowerTf`) and are
9
- * excluded.
50
+ * excluded. Thin delegate over {@link extractRequestAnalysis} kept for callers
51
+ * that only need the interval list.
10
52
  *
11
53
  * @since 0.4
12
54
  * @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,KAAK,EAAE,4BAA4B,EAAE,MAAM,8BAA8B,CAAC;AACjF,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAG7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAG9D;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACnC,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACjC,mBAAmB,EAAE,aAAa,CAAC,4BAA4B,CAAC,CAAC;CACpE,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;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,CAuCjB;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"}