@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.
- package/CHANGELOG.md +246 -0
- package/dist/analysis/extractMaxLookback.d.ts +2 -1
- package/dist/analysis/extractMaxLookback.d.ts.map +1 -1
- package/dist/analysis/extractMaxLookback.js +90 -6
- package/dist/analysis/extractMaxLookback.js.map +1 -1
- package/dist/analysis/extractRequestedIntervals.d.ts +43 -1
- package/dist/analysis/extractRequestedIntervals.d.ts.map +1 -1
- package/dist/analysis/extractRequestedIntervals.js +95 -10
- package/dist/analysis/extractRequestedIntervals.js.map +1 -1
- package/dist/analysis/forbiddenConstructs.d.ts.map +1 -1
- package/dist/analysis/forbiddenConstructs.js +2 -41
- package/dist/analysis/forbiddenConstructs.js.map +1 -1
- package/dist/analysis/index.d.ts +3 -1
- package/dist/analysis/index.d.ts.map +1 -1
- package/dist/analysis/index.js +2 -1
- package/dist/analysis/index.js.map +1 -1
- package/dist/analysis/loopBounds.d.ts +91 -0
- package/dist/analysis/loopBounds.d.ts.map +1 -0
- package/dist/analysis/loopBounds.js +132 -0
- package/dist/analysis/loopBounds.js.map +1 -0
- package/dist/analysis/resolveIndexBound.d.ts +73 -0
- package/dist/analysis/resolveIndexBound.d.ts.map +1 -0
- package/dist/analysis/resolveIndexBound.js +336 -0
- package/dist/analysis/resolveIndexBound.js.map +1 -0
- package/dist/analysis/validateSecurityExpr.d.ts +25 -0
- package/dist/analysis/validateSecurityExpr.d.ts.map +1 -0
- package/dist/analysis/validateSecurityExpr.js +154 -0
- package/dist/analysis/validateSecurityExpr.js.map +1 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +13 -3
- package/dist/api.js.map +1 -1
- package/dist/diagnostics.d.ts +4 -2
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/manifest.d.ts +2 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +7 -0
- package/dist/manifest.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +91 -14
- package/dist/program.js.map +1 -1
- package/dist/transformers/callsiteIdInjection.d.ts +21 -0
- package/dist/transformers/callsiteIdInjection.d.ts.map +1 -1
- package/dist/transformers/callsiteIdInjection.js +26 -3
- package/dist/transformers/callsiteIdInjection.js.map +1 -1
- package/dist/transformers/resolveCallee.d.ts +21 -0
- package/dist/transformers/resolveCallee.d.ts.map +1 -1
- package/dist/transformers/resolveCallee.js +14 -1
- package/dist/transformers/resolveCallee.js.map +1 -1
- 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;
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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":"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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,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;
|
|
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"}
|