@hoox-sh/pynets 0.1.0 → 0.2.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 +20 -0
- package/README.md +5 -3
- package/dist/browser.js +90 -34
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4441 -411
- package/package.json +1 -1
- package/src/cli/rich.ts +4 -3
- package/src/cli.ts +29 -7
- package/src/index.ts +21 -0
- package/src/runtime/compile/collections.ts +339 -0
- package/src/runtime/compile/draw.ts +181 -0
- package/src/runtime/compile/emit.ts +1164 -0
- package/src/runtime/compile/emit_call.ts +1406 -0
- package/src/runtime/compile/emit_udf.ts +326 -0
- package/src/runtime/compile/engine.ts +265 -0
- package/src/runtime/compile/helpers.ts +140 -0
- package/src/runtime/compile/index.ts +33 -0
- package/src/runtime/compile/runtime.ts +297 -0
- package/src/runtime/compile/strategy.ts +169 -0
- package/src/runtime/compile/types.ts +125 -0
- package/src/runtime/compile/udt_runtime.ts +50 -0
- package/src/runtime/interpret.ts +192 -10
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Pine AST → JS bar-loop source (Python object-mode analog, not Numba).
|
|
6
|
+
*/
|
|
7
|
+
import { parse } from "../../ast/helper.ts";
|
|
8
|
+
import type {
|
|
9
|
+
Assign,
|
|
10
|
+
Attribute,
|
|
11
|
+
BinOp,
|
|
12
|
+
BoolOp,
|
|
13
|
+
Call,
|
|
14
|
+
Case,
|
|
15
|
+
Compare,
|
|
16
|
+
Conditional,
|
|
17
|
+
Constant,
|
|
18
|
+
EnumDef,
|
|
19
|
+
expr,
|
|
20
|
+
ForIn,
|
|
21
|
+
ForTo,
|
|
22
|
+
FunctionDef,
|
|
23
|
+
If,
|
|
24
|
+
Import,
|
|
25
|
+
Name,
|
|
26
|
+
ReAssign,
|
|
27
|
+
Script,
|
|
28
|
+
stmt,
|
|
29
|
+
Subscript,
|
|
30
|
+
Switch,
|
|
31
|
+
Tuple,
|
|
32
|
+
TypeDef,
|
|
33
|
+
UnaryOp,
|
|
34
|
+
While,
|
|
35
|
+
} from "../../ast/nodes.ts";
|
|
36
|
+
import { emitCall } from "./emit_call.ts";
|
|
37
|
+
import { emitFunctionDef } from "./emit_udf.ts";
|
|
38
|
+
import type { EmitCtx, VisitFn } from "./types.ts";
|
|
39
|
+
|
|
40
|
+
const FOR_TO_CAP = 1_000_000;
|
|
41
|
+
const WHILE_CAP = 10_000;
|
|
42
|
+
|
|
43
|
+
const CHART_LOCALS = new Set([
|
|
44
|
+
"open",
|
|
45
|
+
"high",
|
|
46
|
+
"low",
|
|
47
|
+
"close",
|
|
48
|
+
"volume",
|
|
49
|
+
"time",
|
|
50
|
+
"bar_index",
|
|
51
|
+
"last_bar_index",
|
|
52
|
+
"na",
|
|
53
|
+
"hl2",
|
|
54
|
+
"hlc3",
|
|
55
|
+
"ohlc4",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const CHART_ARR = new Set([
|
|
59
|
+
"open_arr",
|
|
60
|
+
"high_arr",
|
|
61
|
+
"low_arr",
|
|
62
|
+
"close_arr",
|
|
63
|
+
"vol_arr",
|
|
64
|
+
"time_arr",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
/** User-series / loop-var names that would collide with bar locals or JS syntax. */
|
|
68
|
+
const RESERVED = new Set([
|
|
69
|
+
...CHART_LOCALS,
|
|
70
|
+
"function",
|
|
71
|
+
"return",
|
|
72
|
+
"var",
|
|
73
|
+
"let",
|
|
74
|
+
"const",
|
|
75
|
+
"default",
|
|
76
|
+
"break",
|
|
77
|
+
"case",
|
|
78
|
+
"catch",
|
|
79
|
+
"class",
|
|
80
|
+
"continue",
|
|
81
|
+
"debugger",
|
|
82
|
+
"delete",
|
|
83
|
+
"do",
|
|
84
|
+
"else",
|
|
85
|
+
"export",
|
|
86
|
+
"extends",
|
|
87
|
+
"finally",
|
|
88
|
+
"for",
|
|
89
|
+
"if",
|
|
90
|
+
"import",
|
|
91
|
+
"in",
|
|
92
|
+
"instanceof",
|
|
93
|
+
"new",
|
|
94
|
+
"super",
|
|
95
|
+
"this",
|
|
96
|
+
"throw",
|
|
97
|
+
"try",
|
|
98
|
+
"typeof",
|
|
99
|
+
"void",
|
|
100
|
+
"while",
|
|
101
|
+
"with",
|
|
102
|
+
"yield",
|
|
103
|
+
"enum",
|
|
104
|
+
"await",
|
|
105
|
+
"implements",
|
|
106
|
+
"interface",
|
|
107
|
+
"package",
|
|
108
|
+
"private",
|
|
109
|
+
"protected",
|
|
110
|
+
"public",
|
|
111
|
+
"static",
|
|
112
|
+
"arguments",
|
|
113
|
+
"eval",
|
|
114
|
+
"null",
|
|
115
|
+
"true",
|
|
116
|
+
"false",
|
|
117
|
+
"undefined",
|
|
118
|
+
"NaN",
|
|
119
|
+
"Infinity",
|
|
120
|
+
"n_bars",
|
|
121
|
+
"execute_script_compiled",
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
interface State {
|
|
125
|
+
ctx: EmitCtx;
|
|
126
|
+
/** Pine id → JS ident for in-scope for/while counters. */
|
|
127
|
+
loopVars: Map<string, string>;
|
|
128
|
+
tmp: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function newEmitCtx(): EmitCtx {
|
|
132
|
+
return {
|
|
133
|
+
arrays: new Set(),
|
|
134
|
+
plots: [],
|
|
135
|
+
varNames: new Set(),
|
|
136
|
+
functions: [],
|
|
137
|
+
userFuncs: new Set(),
|
|
138
|
+
usesStrategy: false,
|
|
139
|
+
nextSite: 0,
|
|
140
|
+
errors: [],
|
|
141
|
+
currentFunc: null,
|
|
142
|
+
currentParamNames: new Set(),
|
|
143
|
+
currentSeriesParams: new Set(),
|
|
144
|
+
funcSeriesParams: new Map(),
|
|
145
|
+
funcParamNames: new Map(),
|
|
146
|
+
stArrays: new Set(),
|
|
147
|
+
udtTypes: new Map(),
|
|
148
|
+
usesDrawings: false,
|
|
149
|
+
enumTypes: new Map(),
|
|
150
|
+
udtMethodNames: new Set(),
|
|
151
|
+
needsHeikinashi: false,
|
|
152
|
+
importAliases: new Set(),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function emitScript(tree: Script, ctx?: EmitCtx): string {
|
|
157
|
+
const c = ctx ?? newEmitCtx();
|
|
158
|
+
const state: State = { ctx: c, loopVars: new Map(), tmp: 0 };
|
|
159
|
+
prefetch(state, tree);
|
|
160
|
+
const body: string[] = [];
|
|
161
|
+
for (const s of tree.body ?? []) {
|
|
162
|
+
const line = emitStmt(state, s);
|
|
163
|
+
if (line) body.push(line);
|
|
164
|
+
}
|
|
165
|
+
collectMissedArrays(c, body);
|
|
166
|
+
return assemble(c, body);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function transpileSource(
|
|
170
|
+
source: string,
|
|
171
|
+
extras?: { getLibrary?: EmitCtx["getLibrary"] },
|
|
172
|
+
): { code: string; ctx: EmitCtx } {
|
|
173
|
+
const tree = parse(source);
|
|
174
|
+
const ctx = newEmitCtx();
|
|
175
|
+
ctx.getLibrary = extras?.getLibrary;
|
|
176
|
+
if (tree.kind !== "Script") {
|
|
177
|
+
ctx.errors.push("not a Script");
|
|
178
|
+
return { code: emitScript(tree as Script, ctx), ctx };
|
|
179
|
+
}
|
|
180
|
+
return { code: emitScript(tree as Script, ctx), ctx };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function collectMissedArrays(ctx: EmitCtx, body: string[]): void {
|
|
184
|
+
const blob = `${body.join("\n")}\n${ctx.functions.join("\n")}`;
|
|
185
|
+
for (const m of blob.matchAll(/\b([A-Za-z_][A-Za-z0-9_]*_arr)\b/g)) {
|
|
186
|
+
const name = m[1]!;
|
|
187
|
+
if (CHART_ARR.has(name) || /^plot_\d+$/.test(name)) continue;
|
|
188
|
+
ctx.arrays.add(name);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function assemble(ctx: EmitCtx, body: string[]): string {
|
|
193
|
+
if (ctx.needsHeikinashi) {
|
|
194
|
+
for (const arr of ["ha_open_arr", "ha_high_arr", "ha_low_arr", "ha_close_arr"] as const) {
|
|
195
|
+
ctx.arrays.add(arr);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const lines: string[] = [
|
|
199
|
+
"function execute_script_compiled(open_arr, high_arr, low_arr, close_arr, vol_arr, time_arr, __h) {",
|
|
200
|
+
" const n_bars = close_arr.length;",
|
|
201
|
+
];
|
|
202
|
+
for (const arr of [...ctx.arrays].sort()) {
|
|
203
|
+
if (CHART_ARR.has(arr) || /^plot_\d+$/.test(arr)) continue;
|
|
204
|
+
lines.push(` const ${arr} = new Array(n_bars).fill(null);`);
|
|
205
|
+
}
|
|
206
|
+
for (let i = 0; i < ctx.plots.length; i++) {
|
|
207
|
+
lines.push(` const plot_${i} = new Array(n_bars).fill(null);`);
|
|
208
|
+
}
|
|
209
|
+
for (const arr of [...ctx.stArrays].sort()) {
|
|
210
|
+
lines.push(` const ${arr} = new Array(n_bars).fill(null);`);
|
|
211
|
+
}
|
|
212
|
+
lines.push(" const __var_inited = Object.create(null);");
|
|
213
|
+
for (const fn of ctx.functions) {
|
|
214
|
+
if (fn) lines.push(indentBlock(fn, 2));
|
|
215
|
+
}
|
|
216
|
+
for (const [name, spec] of ctx.udtTypes) {
|
|
217
|
+
lines.push(` __h.udtRegister(${JSON.stringify(name)}, ${JSON.stringify(spec.fields)});`);
|
|
218
|
+
}
|
|
219
|
+
lines.push(" for (let __bar_idx = 0; __bar_idx < n_bars; __bar_idx++) {");
|
|
220
|
+
lines.push(" const open = open_arr[__bar_idx];");
|
|
221
|
+
lines.push(" const high = high_arr[__bar_idx];");
|
|
222
|
+
lines.push(" const low = low_arr[__bar_idx];");
|
|
223
|
+
lines.push(" const close = close_arr[__bar_idx];");
|
|
224
|
+
lines.push(" const volume = vol_arr[__bar_idx];");
|
|
225
|
+
lines.push(" const time = time_arr[__bar_idx];");
|
|
226
|
+
lines.push(" const bar_index = __bar_idx;");
|
|
227
|
+
lines.push(" const last_bar_index = n_bars - 1;");
|
|
228
|
+
lines.push(" const na = null;");
|
|
229
|
+
lines.push(" const hl2 = __h.div(__h.add(high, low), 2);");
|
|
230
|
+
lines.push(" const hlc3 = __h.div(__h.add(__h.add(high, low), close), 3);");
|
|
231
|
+
lines.push(" const ohlc4 = __h.div(__h.add(__h.add(__h.add(open, high), low), close), 4);");
|
|
232
|
+
if (ctx.usesStrategy) {
|
|
233
|
+
lines.push(" __h.strategy.beginBar(__bar_idx, open, high, low, close, time);");
|
|
234
|
+
}
|
|
235
|
+
if (ctx.needsHeikinashi) {
|
|
236
|
+
lines.push(" const _ha_c = __h.div(__h.add(__h.add(__h.add(open, high), low), close), 4);");
|
|
237
|
+
lines.push(
|
|
238
|
+
" const _ha_o = (__bar_idx === 0 || __h.isNa(ha_open_arr[__bar_idx - 1])) ? __h.div(__h.add(open, close), 2) : __h.div(__h.add(ha_open_arr[__bar_idx - 1], ha_close_arr[__bar_idx - 1]), 2);",
|
|
239
|
+
);
|
|
240
|
+
lines.push(" ha_open_arr[__bar_idx] = _ha_o;");
|
|
241
|
+
lines.push(" ha_close_arr[__bar_idx] = _ha_c;");
|
|
242
|
+
lines.push(" ha_high_arr[__bar_idx] = __h.max(high, __h.max(_ha_o, _ha_c));");
|
|
243
|
+
lines.push(" ha_low_arr[__bar_idx] = __h.min(low, __h.min(_ha_o, _ha_c));");
|
|
244
|
+
}
|
|
245
|
+
for (const line of body) {
|
|
246
|
+
lines.push(indentBlock(line, 4));
|
|
247
|
+
}
|
|
248
|
+
lines.push(" }");
|
|
249
|
+
const plotObj = ctx.plots.length
|
|
250
|
+
? `{ ${ctx.plots.map((p, i) => `${JSON.stringify(p.title)}: plot_${i}`).join(", ")} }`
|
|
251
|
+
: "{}";
|
|
252
|
+
const extras: string[] = [];
|
|
253
|
+
if (ctx.usesStrategy) extras.push("__h.strategy.extras()");
|
|
254
|
+
extras.push("((__h.draw && __h.draw.extras()) || {})");
|
|
255
|
+
extras.push("((__h.log && __h.log.extras && __h.log.extras()) || {})");
|
|
256
|
+
lines.push(` return Object.assign(${plotObj}, ${extras.join(", ")});`);
|
|
257
|
+
lines.push("}");
|
|
258
|
+
return lines.join("\n");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function visitOf(state: State): VisitFn {
|
|
262
|
+
return (node) => emitNode(state, node);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function emitNode(state: State, node: expr | stmt | null | undefined): string {
|
|
266
|
+
if (node == null) return "null";
|
|
267
|
+
switch (node.kind) {
|
|
268
|
+
case "Assign":
|
|
269
|
+
case "ReAssign":
|
|
270
|
+
case "FunctionDef":
|
|
271
|
+
case "TypeDef":
|
|
272
|
+
case "EnumDef":
|
|
273
|
+
case "Import":
|
|
274
|
+
case "Break":
|
|
275
|
+
case "Continue":
|
|
276
|
+
case "Expr":
|
|
277
|
+
return emitStmt(state, node);
|
|
278
|
+
default:
|
|
279
|
+
return emitExpr(state, node);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function emitStmt(state: State, node: stmt | expr): string {
|
|
284
|
+
switch (node.kind) {
|
|
285
|
+
case "Expr":
|
|
286
|
+
return emitExprStmt(state, node.value);
|
|
287
|
+
case "Assign":
|
|
288
|
+
return emitAssign(state, node);
|
|
289
|
+
case "ReAssign":
|
|
290
|
+
return emitReAssign(state, node);
|
|
291
|
+
case "FunctionDef":
|
|
292
|
+
emitFunctionDef(state.ctx, node as FunctionDef, (s) => emitStmt(state, s));
|
|
293
|
+
if (state.ctx.currentFunc != null) state.ctx.currentFunc = null;
|
|
294
|
+
return "";
|
|
295
|
+
case "TypeDef":
|
|
296
|
+
return emitTypeDef(state, node as TypeDef);
|
|
297
|
+
case "EnumDef":
|
|
298
|
+
return emitEnumDef(state, node as EnumDef);
|
|
299
|
+
case "Import":
|
|
300
|
+
return emitImport(state, node as Import);
|
|
301
|
+
case "Break":
|
|
302
|
+
return "break;";
|
|
303
|
+
case "Continue":
|
|
304
|
+
return "continue;";
|
|
305
|
+
case "If":
|
|
306
|
+
return emitIfStmt(state, node);
|
|
307
|
+
case "ForTo":
|
|
308
|
+
return emitForTo(state, node);
|
|
309
|
+
case "ForIn":
|
|
310
|
+
return emitForIn(state, node);
|
|
311
|
+
case "While":
|
|
312
|
+
return emitWhile(state, node);
|
|
313
|
+
case "Switch":
|
|
314
|
+
return emitSwitch(state, node, false);
|
|
315
|
+
default:
|
|
316
|
+
return emitExprStmt(state, node);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function emitExprStmt(state: State, value: expr | null | undefined): string {
|
|
321
|
+
if (value == null) return "";
|
|
322
|
+
switch (value.kind) {
|
|
323
|
+
case "If":
|
|
324
|
+
return emitIfStmt(state, value);
|
|
325
|
+
case "ForTo":
|
|
326
|
+
return emitForTo(state, value);
|
|
327
|
+
case "ForIn":
|
|
328
|
+
return emitForIn(state, value);
|
|
329
|
+
case "While":
|
|
330
|
+
return emitWhile(state, value);
|
|
331
|
+
case "Switch":
|
|
332
|
+
return emitSwitch(state, value, false);
|
|
333
|
+
default: {
|
|
334
|
+
const e = emitExpr(state, value);
|
|
335
|
+
if (!e || e === "null") return "";
|
|
336
|
+
return e.endsWith(";") || e.endsWith("}") ? e : `${e};`;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function emitExpr(state: State, node: expr | null | undefined): string {
|
|
342
|
+
if (node == null) return "null";
|
|
343
|
+
switch (node.kind) {
|
|
344
|
+
case "Name":
|
|
345
|
+
return emitName(state, node);
|
|
346
|
+
case "Constant":
|
|
347
|
+
return emitConstant(node);
|
|
348
|
+
case "BinOp":
|
|
349
|
+
return emitBinOp(state, node);
|
|
350
|
+
case "UnaryOp":
|
|
351
|
+
return emitUnaryOp(state, node);
|
|
352
|
+
case "Compare":
|
|
353
|
+
return emitCompare(state, node);
|
|
354
|
+
case "Conditional":
|
|
355
|
+
return emitConditional(state, node);
|
|
356
|
+
case "If":
|
|
357
|
+
return emitIfExpr(state, node);
|
|
358
|
+
case "BoolOp":
|
|
359
|
+
return emitBoolOp(state, node);
|
|
360
|
+
case "Tuple":
|
|
361
|
+
return emitTuple(state, node);
|
|
362
|
+
case "Subscript":
|
|
363
|
+
return emitSubscript(state, node);
|
|
364
|
+
case "Call":
|
|
365
|
+
return emitCall(state.ctx, node as Call, visitOf(state));
|
|
366
|
+
case "Attribute":
|
|
367
|
+
return emitAttribute(state, node);
|
|
368
|
+
case "Switch":
|
|
369
|
+
return emitSwitch(state, node, true);
|
|
370
|
+
case "ForTo":
|
|
371
|
+
return iife(emitForTo(state, node), "null");
|
|
372
|
+
case "ForIn":
|
|
373
|
+
return iife(emitForIn(state, node), "null");
|
|
374
|
+
case "While":
|
|
375
|
+
return iife(emitWhile(state, node), "null");
|
|
376
|
+
default:
|
|
377
|
+
return "null";
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function emitName(state: State, node: Name): string {
|
|
382
|
+
const id = node.id;
|
|
383
|
+
const loop = state.loopVars.get(id);
|
|
384
|
+
if (loop != null) return loop;
|
|
385
|
+
// Method receiver formal is `this_` (emit_udf reserved suffix).
|
|
386
|
+
if (id === "this") return "this_";
|
|
387
|
+
// barstate is a namespace (barstate.isfirst); never a bare name.
|
|
388
|
+
if (id === "barstate") return "null";
|
|
389
|
+
if (id === "true" || id === "True") return "true";
|
|
390
|
+
if (id === "false" || id === "False") return "false";
|
|
391
|
+
if (state.ctx.currentSeriesParams.has(id)) return `${safeIdent(id)}[__bar_idx]`;
|
|
392
|
+
const st = stArrayFor(state.ctx, id);
|
|
393
|
+
if (st != null) return `${st}[__bar_idx]`;
|
|
394
|
+
const arr = seriesArrName(id);
|
|
395
|
+
if (state.ctx.arrays.has(arr)) return `${arr}[__bar_idx]`;
|
|
396
|
+
if (CHART_LOCALS.has(id)) return id;
|
|
397
|
+
if (state.ctx.userFuncs.has(id)) return id;
|
|
398
|
+
if (state.ctx.enumTypes.has(id)) return JSON.stringify(id);
|
|
399
|
+
return safeIdent(id);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function emitConstant(node: Constant): string {
|
|
403
|
+
const v = node.value;
|
|
404
|
+
if (v == null) return "null";
|
|
405
|
+
if (typeof v === "number" && !Number.isFinite(v)) return "null";
|
|
406
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "string") {
|
|
407
|
+
return JSON.stringify(v);
|
|
408
|
+
}
|
|
409
|
+
return "null";
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function emitBinOp(state: State, node: BinOp): string {
|
|
413
|
+
const left = emitExpr(state, node.left);
|
|
414
|
+
const right = emitExpr(state, node.right);
|
|
415
|
+
switch (opKind(node.op)) {
|
|
416
|
+
case "Add":
|
|
417
|
+
return `__h.add(${left}, ${right})`;
|
|
418
|
+
case "Sub":
|
|
419
|
+
return `__h.sub(${left}, ${right})`;
|
|
420
|
+
case "Mult":
|
|
421
|
+
return `__h.mul(${left}, ${right})`;
|
|
422
|
+
case "Div":
|
|
423
|
+
return `__h.div(${left}, ${right})`;
|
|
424
|
+
case "Mod":
|
|
425
|
+
return `__h.mod(${left}, ${right})`;
|
|
426
|
+
default:
|
|
427
|
+
return `__h.add(${left}, ${right})`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function emitUnaryOp(state: State, node: UnaryOp): string {
|
|
432
|
+
const operand = emitExpr(state, node.operand);
|
|
433
|
+
switch (opKind(node.op)) {
|
|
434
|
+
case "USub":
|
|
435
|
+
return `__h.neg(${operand})`;
|
|
436
|
+
case "UAdd":
|
|
437
|
+
return `__h.naNum(${operand})`;
|
|
438
|
+
case "Not":
|
|
439
|
+
return `!(${operand})`;
|
|
440
|
+
case "Invert":
|
|
441
|
+
return `~(__h.nz(${operand}))`;
|
|
442
|
+
default:
|
|
443
|
+
return operand;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function emitCompare(state: State, node: Compare): string {
|
|
448
|
+
const ops = node.ops ?? [];
|
|
449
|
+
const comps = node.comparators ?? [];
|
|
450
|
+
if (ops.length === 0 || comps.length === 0) return emitExpr(state, node.left);
|
|
451
|
+
const parts: string[] = [];
|
|
452
|
+
let lhs = emitExpr(state, node.left);
|
|
453
|
+
for (let i = 0; i < ops.length && i < comps.length; i++) {
|
|
454
|
+
const rhs = emitExpr(state, comps[i]);
|
|
455
|
+
parts.push(compareCall(opKind(ops[i]!), lhs, rhs));
|
|
456
|
+
lhs = rhs;
|
|
457
|
+
}
|
|
458
|
+
return parts.length === 1 ? parts[0]! : `(${parts.join(" && ")})`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function compareCall(op: string, left: string, right: string): string {
|
|
462
|
+
switch (op) {
|
|
463
|
+
case "Eq":
|
|
464
|
+
return `__h.eq(${left}, ${right})`;
|
|
465
|
+
case "NotEq":
|
|
466
|
+
return `__h.ne(${left}, ${right})`;
|
|
467
|
+
case "Lt":
|
|
468
|
+
return `__h.lt(${left}, ${right})`;
|
|
469
|
+
case "LtE":
|
|
470
|
+
return `__h.lte(${left}, ${right})`;
|
|
471
|
+
case "Gt":
|
|
472
|
+
return `__h.gt(${left}, ${right})`;
|
|
473
|
+
case "GtE":
|
|
474
|
+
return `__h.gte(${left}, ${right})`;
|
|
475
|
+
default:
|
|
476
|
+
return `__h.eq(${left}, ${right})`;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function emitConditional(state: State, node: Conditional): string {
|
|
481
|
+
return `(${emitExpr(state, node.test)} ? ${emitExpr(state, node.body)} : ${emitExpr(state, node.orelse)})`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function emitBoolOp(state: State, node: BoolOp): string {
|
|
485
|
+
const values = (node.values ?? []).map((v) => emitExpr(state, v));
|
|
486
|
+
if (values.length === 0) return "null";
|
|
487
|
+
if (values.length === 1) return values[0]!;
|
|
488
|
+
const op = opKind(node.op) === "Or" ? " || " : " && ";
|
|
489
|
+
return `(${values.join(op)})`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function emitTuple(state: State, node: Tuple): string {
|
|
493
|
+
return `[${(node.elts ?? []).map((e) => emitExpr(state, e)).join(", ")}]`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const STRATEGY_QUERY_ATTRS = new Set([
|
|
497
|
+
"position_size",
|
|
498
|
+
"netprofit",
|
|
499
|
+
"equity",
|
|
500
|
+
"openprofit",
|
|
501
|
+
"opentrades",
|
|
502
|
+
"closedtrades",
|
|
503
|
+
]);
|
|
504
|
+
|
|
505
|
+
/** Namespaces used only as call receivers — not UDT field access. */
|
|
506
|
+
const ATTR_NAMESPACES = new Set([
|
|
507
|
+
"ta",
|
|
508
|
+
"math",
|
|
509
|
+
"str",
|
|
510
|
+
"array",
|
|
511
|
+
"map",
|
|
512
|
+
"matrix",
|
|
513
|
+
"strategy",
|
|
514
|
+
"input",
|
|
515
|
+
"color",
|
|
516
|
+
"request",
|
|
517
|
+
"label",
|
|
518
|
+
"line",
|
|
519
|
+
"box",
|
|
520
|
+
"table",
|
|
521
|
+
"session",
|
|
522
|
+
"chart",
|
|
523
|
+
"log",
|
|
524
|
+
]);
|
|
525
|
+
|
|
526
|
+
function emitBarstate(attr: string): string {
|
|
527
|
+
switch (attr) {
|
|
528
|
+
case "isfirst":
|
|
529
|
+
return "(__bar_idx === 0)";
|
|
530
|
+
case "islast":
|
|
531
|
+
return "(__bar_idx === last_bar_index)";
|
|
532
|
+
case "ishistory":
|
|
533
|
+
return "(__bar_idx < last_bar_index)";
|
|
534
|
+
// Compile has no live/historical tick split — last bar is "realtime".
|
|
535
|
+
case "isrealtime":
|
|
536
|
+
return "(__bar_idx === last_bar_index)";
|
|
537
|
+
case "isnew":
|
|
538
|
+
return "true";
|
|
539
|
+
case "isconfirmed":
|
|
540
|
+
return "true";
|
|
541
|
+
case "islastconfirmedhistory":
|
|
542
|
+
return "(__bar_idx === last_bar_index)";
|
|
543
|
+
default:
|
|
544
|
+
return "false";
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function emitSession(attr: string): string {
|
|
549
|
+
switch (attr) {
|
|
550
|
+
case "regular":
|
|
551
|
+
return `"regular"`;
|
|
552
|
+
case "extended":
|
|
553
|
+
return `"extended"`;
|
|
554
|
+
case "ismarket":
|
|
555
|
+
return "1";
|
|
556
|
+
case "ispremarket":
|
|
557
|
+
case "ispostmarket":
|
|
558
|
+
return "0";
|
|
559
|
+
case "isfirstbar":
|
|
560
|
+
case "isfirstbar_regular":
|
|
561
|
+
return "(__bar_idx === 0 ? 1 : 0)";
|
|
562
|
+
case "islastbar":
|
|
563
|
+
case "islastbar_regular":
|
|
564
|
+
return "(__bar_idx === last_bar_index ? 1 : 0)";
|
|
565
|
+
default:
|
|
566
|
+
return "null";
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function emitChart(attr: string): string {
|
|
571
|
+
switch (attr) {
|
|
572
|
+
case "is_heikinashi":
|
|
573
|
+
case "is_renko":
|
|
574
|
+
case "is_kagi":
|
|
575
|
+
case "is_linebreak":
|
|
576
|
+
case "is_pointfigure":
|
|
577
|
+
return "0";
|
|
578
|
+
case "fg_color":
|
|
579
|
+
return `"#ffffff"`;
|
|
580
|
+
case "bg_color":
|
|
581
|
+
return `"#131722"`;
|
|
582
|
+
default:
|
|
583
|
+
return "null";
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function emitTimeframe(attr: string): string {
|
|
588
|
+
switch (attr) {
|
|
589
|
+
case "period":
|
|
590
|
+
return `(__h.timeframe || "1")`;
|
|
591
|
+
case "multiplier":
|
|
592
|
+
return "1";
|
|
593
|
+
case "isintraday":
|
|
594
|
+
return "true";
|
|
595
|
+
case "isdaily":
|
|
596
|
+
case "isweekly":
|
|
597
|
+
case "ismonthly":
|
|
598
|
+
return "false";
|
|
599
|
+
default:
|
|
600
|
+
return "null";
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function emitSyminfo(attr: string): string {
|
|
605
|
+
switch (attr) {
|
|
606
|
+
case "ticker":
|
|
607
|
+
case "tickerid":
|
|
608
|
+
case "root":
|
|
609
|
+
return `"SYMBOL"`;
|
|
610
|
+
default:
|
|
611
|
+
return "null";
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function emitAttribute(state: State, node: Attribute): string {
|
|
616
|
+
// Import alias.member is not a JS object — calls go through emit_call; loads are na.
|
|
617
|
+
if (node.value.kind === "Name" && state.ctx.importAliases.has(node.value.id)) {
|
|
618
|
+
return "null";
|
|
619
|
+
}
|
|
620
|
+
if (node.value.kind === "Name" && node.value.id === "barstate") {
|
|
621
|
+
return emitBarstate(node.attr);
|
|
622
|
+
}
|
|
623
|
+
if (node.value.kind === "Name" && node.value.id === "session") {
|
|
624
|
+
return emitSession(node.attr);
|
|
625
|
+
}
|
|
626
|
+
if (node.value.kind === "Name" && node.value.id === "chart") {
|
|
627
|
+
return emitChart(node.attr);
|
|
628
|
+
}
|
|
629
|
+
if (node.value.kind === "Name" && node.value.id === "timeframe") {
|
|
630
|
+
return emitTimeframe(node.attr);
|
|
631
|
+
}
|
|
632
|
+
if (node.value.kind === "Name" && node.value.id === "syminfo") {
|
|
633
|
+
return emitSyminfo(node.attr);
|
|
634
|
+
}
|
|
635
|
+
if (node.value.kind === "Name" && node.value.id === "color") {
|
|
636
|
+
return `__h.colorByName(${JSON.stringify(node.attr)})`;
|
|
637
|
+
}
|
|
638
|
+
if (node.value.kind === "Name" && node.value.id === "strategy") {
|
|
639
|
+
const attr = node.attr;
|
|
640
|
+
if (attr === "long") return `"long"`;
|
|
641
|
+
if (attr === "short") return `"short"`;
|
|
642
|
+
if (STRATEGY_QUERY_ATTRS.has(attr)) {
|
|
643
|
+
state.ctx.usesStrategy = true;
|
|
644
|
+
return `__h.strategy.${attr}()`;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (node.value.kind === "Name") {
|
|
648
|
+
const members = state.ctx.enumTypes.get(node.value.id);
|
|
649
|
+
if (members != null && members.includes(node.attr)) {
|
|
650
|
+
return JSON.stringify(node.attr);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const obj = emitExpr(state, node.value);
|
|
654
|
+
if (node.value.kind === "Name" && state.ctx.udtTypes.has(node.value.id)) {
|
|
655
|
+
return `__h.udtGet(${obj}, ${JSON.stringify(node.attr)})`;
|
|
656
|
+
}
|
|
657
|
+
if (node.value.kind !== "Name" || !ATTR_NAMESPACES.has(node.value.id)) {
|
|
658
|
+
return `__h.udtGet(${obj}, ${JSON.stringify(node.attr)})`;
|
|
659
|
+
}
|
|
660
|
+
return `${obj}.${node.attr}`;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function emitSubscript(state: State, node: Subscript): string {
|
|
664
|
+
const slice = node.slice != null ? emitExpr(state, node.slice) : "0";
|
|
665
|
+
const arr = histArray(state, node.value);
|
|
666
|
+
return `__h.hist(${arr}, __bar_idx, ${slice})`;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function histArray(state: State, value: expr): string {
|
|
670
|
+
if (value.kind === "Name") {
|
|
671
|
+
const id = value.id;
|
|
672
|
+
if (state.ctx.currentSeriesParams.has(id)) return safeIdent(id);
|
|
673
|
+
const st = stArrayFor(state.ctx, id);
|
|
674
|
+
if (st != null) return st;
|
|
675
|
+
const user = seriesArrName(id);
|
|
676
|
+
if (state.ctx.arrays.has(user)) return user;
|
|
677
|
+
switch (id) {
|
|
678
|
+
case "open":
|
|
679
|
+
return "open_arr";
|
|
680
|
+
case "high":
|
|
681
|
+
return "high_arr";
|
|
682
|
+
case "low":
|
|
683
|
+
return "low_arr";
|
|
684
|
+
case "close":
|
|
685
|
+
return "close_arr";
|
|
686
|
+
case "volume":
|
|
687
|
+
return "vol_arr";
|
|
688
|
+
case "time":
|
|
689
|
+
return "time_arr";
|
|
690
|
+
default: {
|
|
691
|
+
state.ctx.arrays.add(user);
|
|
692
|
+
return user;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const v = emitExpr(state, value);
|
|
697
|
+
if (v.endsWith("[__bar_idx]")) return v.slice(0, -"[__bar_idx]".length);
|
|
698
|
+
return v;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function emitTypeDef(state: State, node: TypeDef): string {
|
|
702
|
+
const fields: string[] = [];
|
|
703
|
+
const defaults: Record<string, string> = {};
|
|
704
|
+
for (const s of node.body ?? []) {
|
|
705
|
+
if (s.kind === "FunctionDef") {
|
|
706
|
+
state.ctx.udtMethodNames.add(s.name);
|
|
707
|
+
emitFunctionDef(state.ctx, s, (st) => emitStmt(state, st));
|
|
708
|
+
if (state.ctx.currentFunc != null) state.ctx.currentFunc = null;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (s.kind !== "Assign") continue;
|
|
712
|
+
if (s.target.kind !== "Name") continue;
|
|
713
|
+
const fname = s.target.id;
|
|
714
|
+
fields.push(fname);
|
|
715
|
+
defaults[fname] = s.value != null ? emitExpr(state, s.value) : "null";
|
|
716
|
+
}
|
|
717
|
+
state.ctx.udtTypes.set(node.name, { fields, defaults });
|
|
718
|
+
return "";
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function emitEnumDef(state: State, node: EnumDef): string {
|
|
722
|
+
const members: string[] = [];
|
|
723
|
+
for (const s of node.body ?? []) {
|
|
724
|
+
if (s.kind === "Assign" && s.target.kind === "Name") {
|
|
725
|
+
members.push(s.target.id);
|
|
726
|
+
} else if (s.kind === "Expr" && s.value?.kind === "Name") {
|
|
727
|
+
members.push(s.value.id);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
state.ctx.enumTypes.set(node.name, members);
|
|
731
|
+
return "";
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** Record the alias; optionally inline library FunctionDef / TypeDef / EnumDef / Assign. No errors. */
|
|
735
|
+
function emitImport(state: State, node: Import): string {
|
|
736
|
+
const alias = node.alias || node.name;
|
|
737
|
+
state.ctx.importAliases.add(alias);
|
|
738
|
+
const getLibrary = state.ctx.getLibrary;
|
|
739
|
+
if (getLibrary) {
|
|
740
|
+
const src = getLibrary(node.namespace, node.name, node.version);
|
|
741
|
+
if (src) {
|
|
742
|
+
let tree: ReturnType<typeof parse>;
|
|
743
|
+
try {
|
|
744
|
+
tree = parse(src);
|
|
745
|
+
} catch {
|
|
746
|
+
return "";
|
|
747
|
+
}
|
|
748
|
+
if (tree.kind === "Script") {
|
|
749
|
+
for (const s of (tree as Script).body ?? []) {
|
|
750
|
+
if (
|
|
751
|
+
s.kind === "FunctionDef" ||
|
|
752
|
+
s.kind === "TypeDef" ||
|
|
753
|
+
s.kind === "EnumDef" ||
|
|
754
|
+
s.kind === "Assign"
|
|
755
|
+
) {
|
|
756
|
+
emitStmt(state, s);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return "";
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function emitAssign(state: State, node: Assign): string {
|
|
766
|
+
const rhs = node.value != null ? emitExpr(state, node.value) : "null";
|
|
767
|
+
const isVar = isVarMode(node.mode);
|
|
768
|
+
return emitStore(state, node.target, rhs, isVar);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function emitReAssign(state: State, node: ReAssign): string {
|
|
772
|
+
return emitStore(state, node.target, emitExpr(state, node.value), false);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function emitStore(state: State, target: expr, rhs: string, isVar: boolean): string {
|
|
776
|
+
if (target.kind === "Name") {
|
|
777
|
+
return storeName(state, target.id, rhs, isVar);
|
|
778
|
+
}
|
|
779
|
+
if (target.kind === "Tuple") {
|
|
780
|
+
const tmp = `__t${state.tmp++}`;
|
|
781
|
+
const lines = [`const ${tmp} = ${rhs};`];
|
|
782
|
+
const elts = target.elts ?? [];
|
|
783
|
+
for (let i = 0; i < elts.length; i++) {
|
|
784
|
+
const el = elts[i]!;
|
|
785
|
+
if (el.kind === "Name") {
|
|
786
|
+
lines.push(storeName(state, el.id, `${tmp}[${i}]`, isVar));
|
|
787
|
+
} else {
|
|
788
|
+
lines.push(emitStore(state, el, `${tmp}[${i}]`, isVar));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return lines.join("\n");
|
|
792
|
+
}
|
|
793
|
+
if (target.kind === "Attribute") {
|
|
794
|
+
return `__h.udtSet(${emitExpr(state, target.value)}, ${JSON.stringify(target.attr)}, ${rhs});`;
|
|
795
|
+
}
|
|
796
|
+
return `${emitExpr(state, target)} = __h.naNum(${rhs});`;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function storeName(state: State, id: string, rhs: string, isVar: boolean): string {
|
|
800
|
+
const arr = trackSeries(state, id);
|
|
801
|
+
const wrapped = `__h.hold(${rhs})`;
|
|
802
|
+
if (isVar) {
|
|
803
|
+
const key = seriesBase(id);
|
|
804
|
+
state.ctx.varNames.add(id);
|
|
805
|
+
return [
|
|
806
|
+
`if (!__var_inited.${key}) {`,
|
|
807
|
+
` ${arr}[__bar_idx] = ${wrapped};`,
|
|
808
|
+
` __var_inited.${key} = true;`,
|
|
809
|
+
`} else {`,
|
|
810
|
+
` ${arr}[__bar_idx] = ${arr}[__bar_idx - 1];`,
|
|
811
|
+
`}`,
|
|
812
|
+
].join("\n");
|
|
813
|
+
}
|
|
814
|
+
return `${arr}[__bar_idx] = ${wrapped};`;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function emitIfExpr(state: State, node: If): string {
|
|
818
|
+
const thenE = singlePureExpr(state, node.body);
|
|
819
|
+
const elseE =
|
|
820
|
+
node.orelse && node.orelse.length ? singlePureExpr(state, node.orelse) : "null";
|
|
821
|
+
if (thenE != null && elseE != null) {
|
|
822
|
+
return `(__h.nz(${emitExpr(state, node.test)}) ? ${thenE} : ${elseE})`;
|
|
823
|
+
}
|
|
824
|
+
return iife(emitIfStmt(state, node, true), "null");
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function emitIfStmt(state: State, node: If, asExpr = false): string {
|
|
828
|
+
const test = `__h.nz(${emitExpr(state, node.test)})`;
|
|
829
|
+
const thenBlock = emitBranch(state, node.body, asExpr);
|
|
830
|
+
const orelse = node.orelse ?? [];
|
|
831
|
+
if (orelse.length === 0) {
|
|
832
|
+
return `if (${test}) {\n${indentBlock(thenBlock, 2)}\n}`;
|
|
833
|
+
}
|
|
834
|
+
if (orelse.length === 1) {
|
|
835
|
+
const only = unwrapExpr(orelse[0]!);
|
|
836
|
+
if (only && only.kind === "If") {
|
|
837
|
+
const rest = emitIfStmt(state, only, asExpr);
|
|
838
|
+
return `if (${test}) {\n${indentBlock(thenBlock, 2)}\n} else ${rest}`;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const elseBlock = emitBranch(state, orelse, asExpr);
|
|
842
|
+
return `if (${test}) {\n${indentBlock(thenBlock, 2)}\n} else {\n${indentBlock(elseBlock, 2)}\n}`;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function emitBranch(state: State, stmts: stmt[], asExpr: boolean): string {
|
|
846
|
+
const list = stmts ?? [];
|
|
847
|
+
if (list.length === 0) return asExpr ? "return null;" : "";
|
|
848
|
+
const lines: string[] = [];
|
|
849
|
+
for (let i = 0; i < list.length; i++) {
|
|
850
|
+
const s = list[i]!;
|
|
851
|
+
const tail = asExpr && i === list.length - 1;
|
|
852
|
+
if (tail) {
|
|
853
|
+
const inner = unwrapExpr(s);
|
|
854
|
+
if (inner && isValueExpr(inner)) {
|
|
855
|
+
lines.push(`return ${emitExpr(state, inner)};`);
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (s.kind === "Assign" || s.kind === "ReAssign") {
|
|
859
|
+
lines.push(emitStmt(state, s));
|
|
860
|
+
if (s.target.kind === "Name") {
|
|
861
|
+
lines.push(`return ${emitName(state, s.target)};`);
|
|
862
|
+
} else {
|
|
863
|
+
lines.push("return null;");
|
|
864
|
+
}
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (inner && inner.kind === "If") {
|
|
868
|
+
lines.push(emitIfStmt(state, inner, true));
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const stmt = emitStmt(state, s);
|
|
872
|
+
if (stmt) lines.push(stmt);
|
|
873
|
+
lines.push("return null;");
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
const stmt = emitStmt(state, s);
|
|
877
|
+
if (stmt) lines.push(stmt);
|
|
878
|
+
}
|
|
879
|
+
return lines.join("\n");
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function singlePureExpr(state: State, stmts: stmt[]): string | null {
|
|
883
|
+
if (!stmts || stmts.length !== 1) return null;
|
|
884
|
+
const inner = unwrapExpr(stmts[0]!);
|
|
885
|
+
if (inner == null) return null;
|
|
886
|
+
if (inner.kind === "If") {
|
|
887
|
+
const thenE = singlePureExpr(state, inner.body);
|
|
888
|
+
const elseE =
|
|
889
|
+
inner.orelse && inner.orelse.length ? singlePureExpr(state, inner.orelse) : "null";
|
|
890
|
+
if (thenE == null || elseE == null) return null;
|
|
891
|
+
return `(__h.nz(${emitExpr(state, inner.test)}) ? ${thenE} : ${elseE})`;
|
|
892
|
+
}
|
|
893
|
+
if (!isValueExpr(inner)) return null;
|
|
894
|
+
return emitExpr(state, inner);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function isValueExpr(node: expr): boolean {
|
|
898
|
+
switch (node.kind) {
|
|
899
|
+
case "ForTo":
|
|
900
|
+
case "ForIn":
|
|
901
|
+
case "While":
|
|
902
|
+
case "Switch":
|
|
903
|
+
return false;
|
|
904
|
+
default:
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function emitForTo(state: State, node: ForTo): string {
|
|
910
|
+
const id = node.target.kind === "Name" ? node.target.id : `__i${state.tmp++}`;
|
|
911
|
+
const js = node.target.kind === "Name" ? safeIdent(id) : id;
|
|
912
|
+
const startN = `__s${state.tmp++}`;
|
|
913
|
+
const endN = `__e${state.tmp++}`;
|
|
914
|
+
const stepN = `__p${state.tmp++}`;
|
|
915
|
+
const guardN = `__g${state.tmp++}`;
|
|
916
|
+
const start = emitExpr(state, node.start);
|
|
917
|
+
const end = emitExpr(state, node.end);
|
|
918
|
+
const step =
|
|
919
|
+
node.step != null
|
|
920
|
+
? `__h.naNum(${emitExpr(state, node.step)})`
|
|
921
|
+
: `(__h.lte(${startN}, ${endN}) ? 1 : -1)`;
|
|
922
|
+
const prev = state.loopVars.get(id);
|
|
923
|
+
state.loopVars.set(id, js);
|
|
924
|
+
let body: string;
|
|
925
|
+
try {
|
|
926
|
+
body = emitBlock(state, node.body);
|
|
927
|
+
} finally {
|
|
928
|
+
restoreLoop(state, id, prev);
|
|
929
|
+
}
|
|
930
|
+
return [
|
|
931
|
+
`{`,
|
|
932
|
+
` const ${startN} = __h.naNum(${start});`,
|
|
933
|
+
` const ${endN} = __h.naNum(${end});`,
|
|
934
|
+
` const ${stepN} = ${step};`,
|
|
935
|
+
` if (${startN} != null && ${endN} != null && ${stepN} != null && ${stepN} !== 0) {`,
|
|
936
|
+
` let ${guardN} = 0;`,
|
|
937
|
+
` for (let ${js} = ${startN}; (${stepN} > 0 ? ${js} <= ${endN} : ${js} >= ${endN}) && ${guardN}++ < ${FOR_TO_CAP}; ${js} += ${stepN}) {`,
|
|
938
|
+
indentBlock(body, 6),
|
|
939
|
+
` }`,
|
|
940
|
+
` }`,
|
|
941
|
+
`}`,
|
|
942
|
+
]
|
|
943
|
+
.filter((l) => l !== "")
|
|
944
|
+
.join("\n");
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function emitForIn(state: State, node: ForIn): string {
|
|
948
|
+
const iterN = `__it${state.tmp++}`;
|
|
949
|
+
const itemsN = `__xs${state.tmp++}`;
|
|
950
|
+
const idxN = `__k${state.tmp++}`;
|
|
951
|
+
const iter = emitExpr(state, node.iter);
|
|
952
|
+
const added: Array<{ id: string; prev: string | undefined }> = [];
|
|
953
|
+
let header: string;
|
|
954
|
+
let bind: string;
|
|
955
|
+
if (node.target.kind === "Tuple" && (node.target.elts?.length ?? 0) > 0) {
|
|
956
|
+
const names = node.target.elts.map((el, i) => {
|
|
957
|
+
const pid = el.kind === "Name" ? el.id : `__v${state.tmp++}_${i}`;
|
|
958
|
+
const js = el.kind === "Name" ? safeIdent(pid) : pid;
|
|
959
|
+
added.push({ id: pid, prev: state.loopVars.get(pid) });
|
|
960
|
+
state.loopVars.set(pid, js);
|
|
961
|
+
return js;
|
|
962
|
+
});
|
|
963
|
+
if (names.length === 2) {
|
|
964
|
+
header = `const ${names[0]} = ${idxN};`;
|
|
965
|
+
bind = `const ${names[1]} = ${itemsN}[${idxN}];`;
|
|
966
|
+
} else {
|
|
967
|
+
header = "";
|
|
968
|
+
bind = names.map((n, i) => `const ${n} = ${itemsN}[${idxN}][${i}];`).join("\n");
|
|
969
|
+
}
|
|
970
|
+
} else {
|
|
971
|
+
const pid = node.target.kind === "Name" ? node.target.id : `__v${state.tmp++}`;
|
|
972
|
+
const js = node.target.kind === "Name" ? safeIdent(pid) : pid;
|
|
973
|
+
added.push({ id: pid, prev: state.loopVars.get(pid) });
|
|
974
|
+
state.loopVars.set(pid, js);
|
|
975
|
+
header = "";
|
|
976
|
+
bind = `const ${js} = ${itemsN}[${idxN}];`;
|
|
977
|
+
}
|
|
978
|
+
let body: string;
|
|
979
|
+
try {
|
|
980
|
+
body = emitBlock(state, node.body);
|
|
981
|
+
} finally {
|
|
982
|
+
for (const a of added) restoreLoop(state, a.id, a.prev);
|
|
983
|
+
}
|
|
984
|
+
const inner = [header, bind, body].filter(Boolean).join("\n");
|
|
985
|
+
return [
|
|
986
|
+
`{`,
|
|
987
|
+
` const ${iterN} = ${iter};`,
|
|
988
|
+
` const ${itemsN} = ${iterN} != null && typeof ${iterN}.length === "number" ? ${iterN} : [];`,
|
|
989
|
+
` for (let ${idxN} = 0; ${idxN} < ${itemsN}.length && ${idxN} < ${FOR_TO_CAP}; ${idxN}++) {`,
|
|
990
|
+
indentBlock(inner, 4),
|
|
991
|
+
` }`,
|
|
992
|
+
`}`,
|
|
993
|
+
].join("\n");
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function emitWhile(state: State, node: While): string {
|
|
997
|
+
const guardN = `__w${state.tmp++}`;
|
|
998
|
+
const test = `__h.nz(${emitExpr(state, node.test)})`;
|
|
999
|
+
const body = emitBlock(state, node.body);
|
|
1000
|
+
return [
|
|
1001
|
+
`{`,
|
|
1002
|
+
` let ${guardN} = 0;`,
|
|
1003
|
+
` while (${test} && ${guardN}++ < ${WHILE_CAP}) {`,
|
|
1004
|
+
indentBlock(body, 4),
|
|
1005
|
+
` }`,
|
|
1006
|
+
`}`,
|
|
1007
|
+
].join("\n");
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function emitSwitch(state: State, node: Switch, asExpr: boolean): string {
|
|
1011
|
+
const cases = node.cases ?? [];
|
|
1012
|
+
const subjN = node.subject != null ? `__sw${state.tmp++}` : null;
|
|
1013
|
+
const parts: string[] = [];
|
|
1014
|
+
if (subjN != null) {
|
|
1015
|
+
parts.push(`const ${subjN} = ${emitExpr(state, node.subject)};`);
|
|
1016
|
+
}
|
|
1017
|
+
let first = true;
|
|
1018
|
+
let defaultCase: Case | null = null;
|
|
1019
|
+
for (const c of cases) {
|
|
1020
|
+
if (c.pattern == null) {
|
|
1021
|
+
defaultCase = c;
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
const cond =
|
|
1025
|
+
subjN != null
|
|
1026
|
+
? `__h.eq(${subjN}, ${emitExpr(state, c.pattern)})`
|
|
1027
|
+
: `__h.nz(${emitExpr(state, c.pattern)})`;
|
|
1028
|
+
const kw = first ? "if" : "else if";
|
|
1029
|
+
first = false;
|
|
1030
|
+
const body = emitBranch(state, c.body ?? [], asExpr);
|
|
1031
|
+
parts.push(`${kw} (${cond}) {\n${indentBlock(body, 2)}\n}`);
|
|
1032
|
+
}
|
|
1033
|
+
if (defaultCase != null) {
|
|
1034
|
+
const body = emitBranch(state, defaultCase.body ?? [], asExpr);
|
|
1035
|
+
if (first) {
|
|
1036
|
+
parts.push(body);
|
|
1037
|
+
} else {
|
|
1038
|
+
parts.push(`else {\n${indentBlock(body, 2)}\n}`);
|
|
1039
|
+
}
|
|
1040
|
+
} else if (asExpr && !first) {
|
|
1041
|
+
parts.push("else {\n return null;\n}");
|
|
1042
|
+
}
|
|
1043
|
+
const block = parts.filter(Boolean).join("\n");
|
|
1044
|
+
if (asExpr) return iife(block, "null");
|
|
1045
|
+
return `{\n${indentBlock(block, 2)}\n}`;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function emitBlock(state: State, stmts: stmt[] | null | undefined): string {
|
|
1049
|
+
const lines: string[] = [];
|
|
1050
|
+
for (const s of stmts ?? []) {
|
|
1051
|
+
const line = emitStmt(state, s);
|
|
1052
|
+
if (line) lines.push(line);
|
|
1053
|
+
}
|
|
1054
|
+
return lines.join("\n");
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function unwrapExpr(node: stmt | expr): expr | null {
|
|
1058
|
+
if (node.kind === "Expr") return node.value;
|
|
1059
|
+
switch (node.kind) {
|
|
1060
|
+
case "If":
|
|
1061
|
+
case "ForTo":
|
|
1062
|
+
case "ForIn":
|
|
1063
|
+
case "While":
|
|
1064
|
+
case "Switch":
|
|
1065
|
+
case "Call":
|
|
1066
|
+
case "Name":
|
|
1067
|
+
case "Constant":
|
|
1068
|
+
case "Attribute":
|
|
1069
|
+
case "Subscript":
|
|
1070
|
+
case "BinOp":
|
|
1071
|
+
case "UnaryOp":
|
|
1072
|
+
case "Compare":
|
|
1073
|
+
case "Tuple":
|
|
1074
|
+
case "Conditional":
|
|
1075
|
+
case "BoolOp":
|
|
1076
|
+
return node;
|
|
1077
|
+
default:
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function isVarMode(mode: Assign["mode"]): boolean {
|
|
1083
|
+
if (mode == null) return false;
|
|
1084
|
+
const k = opKind(mode);
|
|
1085
|
+
return k === "Var" || k === "VarIp";
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function prefetch(state: State, node: unknown): void {
|
|
1089
|
+
if (node == null || typeof node !== "object") return;
|
|
1090
|
+
if (Array.isArray(node)) {
|
|
1091
|
+
for (const item of node) prefetch(state, item);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
const n = node as { kind?: string; name?: string; target?: expr };
|
|
1095
|
+
if (n.kind === "TypeDef" || n.kind === "EnumDef" || n.kind === "Import") return;
|
|
1096
|
+
if (n.kind === "FunctionDef" && typeof n.name === "string") {
|
|
1097
|
+
state.ctx.userFuncs.add(n.name);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if ((n.kind === "Assign" || n.kind === "ReAssign") && n.target != null) {
|
|
1101
|
+
collectStoreNames(state, n.target);
|
|
1102
|
+
}
|
|
1103
|
+
for (const v of Object.values(n)) prefetch(state, v);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function collectStoreNames(state: State, target: expr): void {
|
|
1107
|
+
if (target.kind === "Name") {
|
|
1108
|
+
trackSeries(state, target.id);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (target.kind === "Tuple") {
|
|
1112
|
+
for (const el of target.elts ?? []) collectStoreNames(state, el);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function trackSeries(state: State, id: string): string {
|
|
1117
|
+
const arr = seriesArrName(id);
|
|
1118
|
+
state.ctx.arrays.add(arr);
|
|
1119
|
+
return arr;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function seriesArrName(id: string): string {
|
|
1123
|
+
return `${seriesBase(id)}_arr`;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function seriesBase(id: string): string {
|
|
1127
|
+
return RESERVED.has(id) ? `__u_${id}` : id;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function safeIdent(id: string): string {
|
|
1131
|
+
return RESERVED.has(id) ? `__u_${id}` : id;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/** Persistent UDF series local: `__st_${safeFunc}_${safeLocal}` (same as emit_udf). */
|
|
1135
|
+
function stArrayFor(ctx: EmitCtx, id: string): string | null {
|
|
1136
|
+
if (ctx.currentFunc == null) return null;
|
|
1137
|
+
const name = `__st_${safeIdent(ctx.currentFunc)}_${safeIdent(id)}`;
|
|
1138
|
+
return ctx.stArrays.has(name) ? name : null;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function restoreLoop(state: State, id: string, prev: string | undefined): void {
|
|
1142
|
+
if (prev === undefined) state.loopVars.delete(id);
|
|
1143
|
+
else state.loopVars.set(id, prev);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function opKind(op: { kind?: string } | string | null | undefined): string {
|
|
1147
|
+
if (op == null) return "";
|
|
1148
|
+
if (typeof op === "string") return op;
|
|
1149
|
+
return op.kind ?? "";
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function indentBlock(s: string, n: number): string {
|
|
1153
|
+
if (!s) return s;
|
|
1154
|
+
const pad = " ".repeat(n);
|
|
1155
|
+
return s
|
|
1156
|
+
.split("\n")
|
|
1157
|
+
.map((line) => (line.length ? pad + line : line))
|
|
1158
|
+
.join("\n");
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function iife(body: string, fallback: string): string {
|
|
1162
|
+
const inner = body.trim() ? `${body}\nreturn ${fallback};` : `return ${fallback};`;
|
|
1163
|
+
return `(() => {\n${indentBlock(inner, 2)}\n})()`;
|
|
1164
|
+
}
|