@pineforge/codegen-pyodide 0.7.6 → 0.8.1
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/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +453 -15
- package/pineforge_codegen/analyzer/call_handlers.py +200 -11
- package/pineforge_codegen/analyzer/contracts.py +62 -0
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +532 -51
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +331 -30
- package/pineforge_codegen/codegen/helpers.py +26 -1
- package/pineforge_codegen/codegen/input.py +39 -2
- package/pineforge_codegen/codegen/security.py +321 -9
- package/pineforge_codegen/codegen/ta.py +4 -1
- package/pineforge_codegen/codegen/tables.py +63 -7
- package/pineforge_codegen/codegen/types.py +383 -21
- package/pineforge_codegen/codegen/visit_call.py +159 -22
- package/pineforge_codegen/codegen/visit_expr.py +91 -3
- package/pineforge_codegen/codegen/visit_stmt.py +188 -26
- package/pineforge_codegen/lexer.py +68 -3
- package/pineforge_codegen/parser.py +106 -49
- package/pineforge_codegen/support_checker.py +361 -37
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +4 -18
- package/pineforge_codegen-0.7.6.tar.gz +0 -0
|
@@ -141,10 +141,14 @@ from ..ast_nodes import (
|
|
|
141
141
|
)
|
|
142
142
|
from ..symbols import TypeSpec
|
|
143
143
|
from .. import signatures as sigs
|
|
144
|
+
from .drawing import ALL_DRAWING_METHODS
|
|
144
145
|
from .tables import (
|
|
146
|
+
ARRAY_DRAWING_NEW_CTORS,
|
|
145
147
|
ARRAY_METHODS,
|
|
146
148
|
BAR_FIELDS,
|
|
147
149
|
BAR_SERIES_PUSH,
|
|
150
|
+
DRAWING_NS,
|
|
151
|
+
DRAWING_TYPE_TO_CPP,
|
|
148
152
|
MAP_METHODS,
|
|
149
153
|
MATH_FUNC_MAP,
|
|
150
154
|
MATRIX_METHODS,
|
|
@@ -226,6 +230,32 @@ class CallVisitor:
|
|
|
226
230
|
)
|
|
227
231
|
rest = [self._visit_expr(a) for a in rest_nodes]
|
|
228
232
|
return f"{fn_cpp}({', '.join([recv_e] + rest)})"
|
|
233
|
+
|
|
234
|
+
# Drawing method dispatch (spec §4.3 / L.1). A KNOWN drawing method on a
|
|
235
|
+
# receiver that resolves to a drawing udt — gated on the METHOD NAME
|
|
236
|
+
# FIRST so a user method (egoigor's ``ln.slope()``, already routed by the
|
|
237
|
+
# block above) is never captured here. This single check covers all
|
|
238
|
+
# receiver shapes (identifier ``ln.set_x2(v)``, obj.field
|
|
239
|
+
# ``d.fld.set_y2(v)``, and arbitrary-expr ``d.upln.get(0).delete()``), so
|
|
240
|
+
# it precedes the obj.field.method / identifier branches below AND the
|
|
241
|
+
# generic ``delete`` -> ``_delete_`` rewrites + _resolve_callee.
|
|
242
|
+
if isinstance(callee, MemberAccess) and callee.member in ALL_DRAWING_METHODS:
|
|
243
|
+
recv_spec = self._type_spec_from_expr(callee.object)
|
|
244
|
+
if (recv_spec is not None and recv_spec.kind == "udt"
|
|
245
|
+
and recv_spec.name in DRAWING_TYPE_TO_CPP):
|
|
246
|
+
return self._emit_drawing_method(
|
|
247
|
+
recv_spec.name, callee.member, callee.object,
|
|
248
|
+
list(node.args), node,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# chart.point.now/new/from_index/from_time/copy — REAL data (a ChartPoint
|
|
252
|
+
# aggregate). Routed here BEFORE the obj.field.method receiver logic,
|
|
253
|
+
# which would otherwise mis-treat ``chart.point`` as a receiver object
|
|
254
|
+
# and raise on the ``chart.point`` member read (chart ∈ SKIP_NAMESPACES).
|
|
255
|
+
if self._is_chart_point_callee(callee):
|
|
256
|
+
cp_func, _cp_ns = self._resolve_callee(callee)
|
|
257
|
+
return self._emit_chart_point(cp_func, node)
|
|
258
|
+
|
|
229
259
|
# obj.field.method(args) — must not lower to namespace::method (loses receiver chain).
|
|
230
260
|
if isinstance(callee, MemberAccess):
|
|
231
261
|
obj = callee.object
|
|
@@ -321,10 +351,22 @@ class CallVisitor:
|
|
|
321
351
|
return f"is_na({args})"
|
|
322
352
|
|
|
323
353
|
# nz(x) / nz(x, y)
|
|
354
|
+
#
|
|
355
|
+
# x's emitted C++ source is substituted into the surrounding
|
|
356
|
+
# expression, so it must be evaluated EXACTLY ONCE: when x is a
|
|
357
|
+
# stateful call (e.g. a ta.* site lowered to `.compute()`/
|
|
358
|
+
# `.recompute()`), naively embedding {x} twice — once for the
|
|
359
|
+
# is_na() check, once for the non-na branch — invokes that call
|
|
360
|
+
# twice per bar, silently corrupting the indicator's internal state
|
|
361
|
+
# (e.g. nz(ta.sma(v, 50), v) becomes an effective 25-bar SMA: every
|
|
362
|
+
# bar is pushed into the ring buffer twice). An immediately-invoked
|
|
363
|
+
# lambda hoists x into a local `auto` so it is computed once and
|
|
364
|
+
# both branches read the same value; `[&]` is safe here since the
|
|
365
|
+
# lambda is called synchronously and discarded, never escaping.
|
|
324
366
|
if func_name == "nz" and namespace is None:
|
|
325
367
|
x = self._visit_expr(node.args[0])
|
|
326
368
|
y = self._visit_expr(node.args[1]) if len(node.args) > 1 else "0.0"
|
|
327
|
-
return f"(
|
|
369
|
+
return f"([&]{{ auto _nz_v = ({x}); return is_na(_nz_v) ? ({y}) : _nz_v; }}())"
|
|
328
370
|
|
|
329
371
|
# fixnan(x) -> persistent state
|
|
330
372
|
if func_name == "fixnan" and namespace is None:
|
|
@@ -414,7 +456,7 @@ class CallVisitor:
|
|
|
414
456
|
|
|
415
457
|
# Array operations — emit proper C++ vector operations
|
|
416
458
|
if namespace == "array":
|
|
417
|
-
if func_name in ("new", "new_float", "new_int", "new_bool", "new_string"):
|
|
459
|
+
if func_name in ("new", "new_float", "new_int", "new_bool", "new_string") or func_name in ARRAY_DRAWING_NEW_CTORS:
|
|
418
460
|
spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
|
|
419
461
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
420
462
|
init_default = self._default_for_spec(spec.element if spec.element is not None else TypeSpec.primitive("float"))
|
|
@@ -439,6 +481,23 @@ class CallVisitor:
|
|
|
439
481
|
if namespace == "color":
|
|
440
482
|
return self._visit_color_call(func_name, node)
|
|
441
483
|
|
|
484
|
+
# Bare color(...) cast (cosmetic). The engine has no color-cast helper
|
|
485
|
+
# and colors have no backtest-logic effect, so emit a benign default
|
|
486
|
+
# color (0 = na color, matching the color.new / from_gradient
|
|
487
|
+
# fallbacks). The support checker warns on this construct.
|
|
488
|
+
if namespace is None and func_name == "color" and func_name not in self._func_names:
|
|
489
|
+
return "0"
|
|
490
|
+
|
|
491
|
+
# Drawing-objects-as-data namespace-functional form (spec §4.3 form 1):
|
|
492
|
+
# line.new(...) / line.get_y2(ln) / box.set_top(b, v) / linefill.new(...)
|
|
493
|
+
# MUST precede the SKIP_NAMESPACES early-return (these namespaces were
|
|
494
|
+
# removed from SKIP_NAMESPACES). chart.point.* resolves to namespace
|
|
495
|
+
# "chart", so it is matched by callee shape instead.
|
|
496
|
+
if namespace in DRAWING_NS:
|
|
497
|
+
return self._emit_drawing_namespace_call(namespace, func_name, node)
|
|
498
|
+
if self._is_chart_point_callee(callee):
|
|
499
|
+
return self._emit_chart_point(func_name, node)
|
|
500
|
+
|
|
442
501
|
# Skip visual/unsupported namespace calls
|
|
443
502
|
if namespace in SKIP_NAMESPACES or namespace in SKIP_VAR_TYPES:
|
|
444
503
|
return "0"
|
|
@@ -461,19 +520,34 @@ class CallVisitor:
|
|
|
461
520
|
all_args.append(None)
|
|
462
521
|
all_args[i] = node.kwargs[pname]
|
|
463
522
|
|
|
464
|
-
# Find matching security call ID
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
523
|
+
# Find matching security call ID. A request.security whose
|
|
524
|
+
# timeframe is a UDF parameter called from multiple sites with
|
|
525
|
+
# multiple distinct literal timeframes is registered as N
|
|
526
|
+
# CLONES (one SecurityCallInfo per call site, same source
|
|
527
|
+
# expr_node identity, distinct sec_id/callsite_idx — see
|
|
528
|
+
# Analyzer._check_mixed_callsite_security_tf). All clones
|
|
529
|
+
# match the identity check below identically, so when more
|
|
530
|
+
# than one matches, disambiguate by which call-site clone's
|
|
531
|
+
# function body is currently being emitted
|
|
532
|
+
# (self._active_call_site_idx, set by _emit_func_def while
|
|
533
|
+
# walking that exact clone's body).
|
|
534
|
+
candidates = [
|
|
535
|
+
item for item in self._security_calls
|
|
536
|
+
if not item.get("is_lower_tf_array")
|
|
537
|
+
and (exprn := item["expr_node"]) is not None
|
|
538
|
+
and (len(all_args) > 2 and exprn is all_args[2])
|
|
539
|
+
]
|
|
540
|
+
chosen = None
|
|
541
|
+
if len(candidates) == 1:
|
|
542
|
+
chosen = candidates[0]
|
|
543
|
+
elif len(candidates) > 1:
|
|
544
|
+
chosen = next(
|
|
545
|
+
(c for c in candidates
|
|
546
|
+
if c.get("callsite_idx") == self._active_call_site_idx),
|
|
547
|
+
candidates[0],
|
|
548
|
+
)
|
|
549
|
+
sec_id = chosen["sec_id"] if chosen else None
|
|
550
|
+
expr_node = chosen["expr_node"] if chosen else None
|
|
477
551
|
|
|
478
552
|
if sec_id is not None and expr_node is not None:
|
|
479
553
|
if isinstance(expr_node, TupleLiteral):
|
|
@@ -515,9 +589,12 @@ class CallVisitor:
|
|
|
515
589
|
|
|
516
590
|
# ticker.* calls
|
|
517
591
|
if namespace == "ticker":
|
|
518
|
-
# ticker.inherit(symbol, ...)
|
|
519
|
-
#
|
|
520
|
-
|
|
592
|
+
# ticker.inherit(symbol, ...) / ticker.standard(symbol) — passthrough,
|
|
593
|
+
# and ticker.heikinashi(symbol) — same-symbol HA: emit the symbol
|
|
594
|
+
# argument unchanged. The runtime HA candle transform is applied by
|
|
595
|
+
# the engine via register_security_eval's heikinashi flag, so the
|
|
596
|
+
# ticker value itself just needs to be the (string) chart symbol.
|
|
597
|
+
if func_name in ("inherit", "standard", "heikinashi"):
|
|
521
598
|
if node.args:
|
|
522
599
|
return self._visit_expr(node.args[0])
|
|
523
600
|
if "symbol" in node.kwargs:
|
|
@@ -908,11 +985,25 @@ class CallVisitor:
|
|
|
908
985
|
elif f.default:
|
|
909
986
|
val = self._visit_expr(f.default)
|
|
910
987
|
if val is not None:
|
|
911
|
-
# Fix narrowing:
|
|
988
|
+
# Fix narrowing: brace-init (``T{.field = v}``) disallows
|
|
989
|
+
# narrowing. Pine ``int`` UDT fields are emitted as
|
|
990
|
+
# ``int64_t`` (see base.py) but are initialised from
|
|
991
|
+
# ``na<double>()`` / doubles in places, so cast to the
|
|
992
|
+
# field's type. ``na<double>()`` for an int field → 0.
|
|
912
993
|
f_cpp_type = self._type_spec_to_cpp(field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name))
|
|
913
|
-
if f_cpp_type == "int"
|
|
914
|
-
|
|
994
|
+
if f_cpp_type == "int":
|
|
995
|
+
f_cpp_type = "int64_t"
|
|
996
|
+
if f_cpp_type == "int64_t":
|
|
997
|
+
if "na<double>" in val:
|
|
998
|
+
val = val.replace("na<double>()", "na<int64_t>()")
|
|
999
|
+
else:
|
|
1000
|
+
val = f"(int64_t)({val})"
|
|
915
1001
|
field_inits.append(f".{f.name} = {val}")
|
|
1002
|
+
# Mark the constructed object non-na (the struct's ``__pf_na`` is the
|
|
1003
|
+
# last declared field, so this designator stays in declaration order).
|
|
1004
|
+
# A bare default-constructed UDT keeps ``__pf_na = true`` (na); only a
|
|
1005
|
+
# real ``.new(...)`` flips it false so ``na(obj)`` reports correctly.
|
|
1006
|
+
field_inits.append(".__pf_na = false")
|
|
916
1007
|
return f"{namespace}{{{', '.join(field_inits)}}}"
|
|
917
1008
|
|
|
918
1009
|
# UDT copy: TypeName.copy(obj)
|
|
@@ -1000,6 +1091,17 @@ class CallVisitor:
|
|
|
1000
1091
|
all_args.extend(self._visit_expr(v) for v in node.kwargs.values())
|
|
1001
1092
|
else:
|
|
1002
1093
|
all_args = [_visit_arg_for_series(a, i) for i, a in enumerate(node.args)]
|
|
1094
|
+
# Drawing-style/visual CONSTANT passed positionally into a user function's
|
|
1095
|
+
# ``string`` parameter: ``label.style_*`` / ``size.*`` / other
|
|
1096
|
+
# DRAWING_STYLE_NS members lower to the bare token ``"0"`` (they only ever
|
|
1097
|
+
# feed dropped visual kwargs). Bound to a ``std::string`` parameter, that
|
|
1098
|
+
# ``0`` constructs ``std::string((char const*)0)`` at the call site -> a
|
|
1099
|
+
# null-pointer ``strlen`` crash at runtime. Coerce such args to
|
|
1100
|
+
# ``std::string("")`` so the (inert, visual-only) value is a valid empty
|
|
1101
|
+
# string. Only touches user functions with a known string param and an
|
|
1102
|
+
# arg that is exactly such a drawing-style constant read.
|
|
1103
|
+
if namespace is None and func_name in self._func_names:
|
|
1104
|
+
self._coerce_drawing_style_string_args(func_name, node.args, all_args)
|
|
1003
1105
|
# Default args (parser does not store defaults): isInSession(sess, res = timeframe.period)
|
|
1004
1106
|
if namespace is None and func_name in self._func_names:
|
|
1005
1107
|
fi = self._func_info_map.get(func_name)
|
|
@@ -1011,7 +1113,16 @@ class CallVisitor:
|
|
|
1011
1113
|
emit_name = self._func_safe_name(func_name) if func_name in self._func_names else func_name
|
|
1012
1114
|
# Per-call-site variant: if this function has TA/series calls, call the correct variant
|
|
1013
1115
|
cs_info = self.ctx.func_call_cs_map.get(id(node))
|
|
1014
|
-
|
|
1116
|
+
dispatch_key = (self._current_instance_name, id(node))
|
|
1117
|
+
if dispatch_key in self._instance_dispatch:
|
|
1118
|
+
# Context-sensitive (call-path) dispatch: the instance pre-pass
|
|
1119
|
+
# resolved this nested stateful-helper call to the clone bound to
|
|
1120
|
+
# THIS enclosing path's members (see _build_func_instances). This
|
|
1121
|
+
# is authoritative — it supersedes the textual-cs threading below,
|
|
1122
|
+
# which conflates a callee's own call sites with the enclosing
|
|
1123
|
+
# function's call sites for helpers reached through >1 path.
|
|
1124
|
+
emit_name = self._instance_dispatch[dispatch_key]
|
|
1125
|
+
elif self._active_call_site_idx is not None and cs_info is not None:
|
|
1015
1126
|
# Inside a per-call-site variant: override the cs_map index with
|
|
1016
1127
|
# the parent's active call-site index. This ensures sub-functions
|
|
1017
1128
|
# called from ma_cs6() use their _cs6 variant, not _cs0.
|
|
@@ -1028,6 +1139,32 @@ class CallVisitor:
|
|
|
1028
1139
|
emit_name = f"{self._func_safe_name(func_name)}_cs{self._active_call_site_idx}"
|
|
1029
1140
|
return f"{prefix}{emit_name}({', '.join(all_args)})"
|
|
1030
1141
|
|
|
1142
|
+
def _coerce_drawing_style_string_args(self, func_name, arg_nodes, all_args) -> None:
|
|
1143
|
+
"""In-place coerce positional args bound to a ``std::string`` user-function
|
|
1144
|
+
parameter that lowered to the bare token ``"0"`` from a drawing-style /
|
|
1145
|
+
visual constant (``label.style_*`` etc.). Such a literal ``0`` binds as
|
|
1146
|
+
``std::string((char const*)0)`` and segfaults on first use. Replace with
|
|
1147
|
+
``std::string("")`` (the value is visual-only and inert in a backtest)."""
|
|
1148
|
+
from .tables import DRAWING_STYLE_NS
|
|
1149
|
+
fi = self._func_info_map.get(func_name)
|
|
1150
|
+
if not fi or not getattr(fi, "node", None) or not fi.node.params:
|
|
1151
|
+
return
|
|
1152
|
+
specs = getattr(fi, "param_type_specs", []) or []
|
|
1153
|
+
for i, arg in enumerate(arg_nodes):
|
|
1154
|
+
if i >= len(all_args) or all_args[i] != "0":
|
|
1155
|
+
continue
|
|
1156
|
+
# Only when the destination parameter is a string.
|
|
1157
|
+
spec = specs[i] if i < len(specs) else None
|
|
1158
|
+
is_string_param = spec is not None and getattr(spec, "kind", None) == "primitive" \
|
|
1159
|
+
and getattr(spec, "name", None) == "string"
|
|
1160
|
+
if not is_string_param:
|
|
1161
|
+
continue
|
|
1162
|
+
# Only when the source really is a drawing-style/visual constant read
|
|
1163
|
+
# (so we never silently turn a numeric ``0`` into an empty string).
|
|
1164
|
+
if (isinstance(arg, MemberAccess) and isinstance(arg.object, Identifier)
|
|
1165
|
+
and arg.object.name in DRAWING_STYLE_NS):
|
|
1166
|
+
all_args[i] = 'std::string("")'
|
|
1167
|
+
|
|
1031
1168
|
def _visit_fixnan(self, node: FuncCall) -> str:
|
|
1032
1169
|
"""Emit fixnan with persistent state member."""
|
|
1033
1170
|
self._fixnan_counter += 1
|
|
@@ -111,6 +111,8 @@ from .tables import (
|
|
|
111
111
|
COLOR_CONST_MAP,
|
|
112
112
|
DAYOFWEEK_MAP,
|
|
113
113
|
DISPLAY_MAP,
|
|
114
|
+
DRAWING_STYLE_NS,
|
|
115
|
+
DRAWING_TYPE_TO_CPP,
|
|
114
116
|
ON_OFF_INHERIT_MAP,
|
|
115
117
|
ORDER_DIRECTION_MAP,
|
|
116
118
|
SKIP_NAMESPACES,
|
|
@@ -180,7 +182,7 @@ class ExprVisitor:
|
|
|
180
182
|
if isinstance(node, NumberLiteral):
|
|
181
183
|
return str(node.value)
|
|
182
184
|
if isinstance(node, StringLiteral):
|
|
183
|
-
return f'std::string("{node.value}")'
|
|
185
|
+
return f'std::string("{self._cpp_string_escape(node.value)}")'
|
|
184
186
|
if isinstance(node, BoolLiteral):
|
|
185
187
|
return "true" if node.value else "false"
|
|
186
188
|
if isinstance(node, NaLiteral):
|
|
@@ -209,6 +211,44 @@ class ExprVisitor:
|
|
|
209
211
|
return f"std::make_tuple({elems})"
|
|
210
212
|
return "/* unknown */"
|
|
211
213
|
|
|
214
|
+
# ------------------------------------------------------------------
|
|
215
|
+
# Target-typed RHS lowering (drawing handles are C++ structs, not doubles)
|
|
216
|
+
# ------------------------------------------------------------------
|
|
217
|
+
def _is_na_expr(self, node) -> bool:
|
|
218
|
+
"""True for a bare ``na`` (keyword NaLiteral or ``na`` identifier)."""
|
|
219
|
+
return (isinstance(node, NaLiteral)
|
|
220
|
+
or (isinstance(node, Identifier) and node.name == "na"))
|
|
221
|
+
|
|
222
|
+
def _drawing_na_default(self, target_name: str | None) -> str | None:
|
|
223
|
+
"""If ``target_name`` is a drawing-handle variable (line/box/label/
|
|
224
|
+
linefill/chart.point), return its na default literal (e.g. ``Box{}`` —
|
|
225
|
+
a default-constructed handle whose id == -1 == na); otherwise None."""
|
|
226
|
+
if not target_name:
|
|
227
|
+
return None
|
|
228
|
+
udt = self._udt_var_types.get(target_name)
|
|
229
|
+
if udt in DRAWING_TYPE_TO_CPP:
|
|
230
|
+
return f"{DRAWING_TYPE_TO_CPP[udt]}{{}}"
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
def _visit_rhs_value(self, value_node, target_name: str | None = None,
|
|
234
|
+
target_cpp_type: str | None = None) -> str:
|
|
235
|
+
"""Visit an assignment / declaration RHS.
|
|
236
|
+
|
|
237
|
+
A bare ``na`` lowers to a type-appropriate initializer for the target
|
|
238
|
+
instead of ``na<double>()``: drawing handles brace-init to their na
|
|
239
|
+
handle (``Box{}`` / ``Line{}`` / …); ``std::string``/``int``/``int64_t``/
|
|
240
|
+
``bool`` use ``na<T>()``. Without this, ``Box b = na;`` and
|
|
241
|
+
``string s = na;`` would both emit ``na<double>()`` and fail to compile
|
|
242
|
+
(no viable ``operator=`` / conversion). Every other RHS lowers unchanged.
|
|
243
|
+
"""
|
|
244
|
+
if target_name and self._is_na_expr(value_node):
|
|
245
|
+
draw_default = self._drawing_na_default(target_name)
|
|
246
|
+
if draw_default is not None:
|
|
247
|
+
return draw_default
|
|
248
|
+
if target_cpp_type in ("std::string", "int", "int64_t", "bool"):
|
|
249
|
+
return f"na<{target_cpp_type}>()"
|
|
250
|
+
return self._visit_expr(value_node)
|
|
251
|
+
|
|
212
252
|
def _visit_ident(self, node: Identifier) -> str:
|
|
213
253
|
name = node.name
|
|
214
254
|
# Bare 'na' identifier → na<double>()
|
|
@@ -475,8 +515,17 @@ class ExprVisitor:
|
|
|
475
515
|
if node.member in ("is_heikinashi", "is_kagi", "is_linebreak",
|
|
476
516
|
"is_pnf", "is_range", "is_renko"):
|
|
477
517
|
return "false"
|
|
478
|
-
#
|
|
479
|
-
#
|
|
518
|
+
# Cosmetic / chart-only reads with no batch-mode value (theme
|
|
519
|
+
# colors, viewport bar times). The support checker warns on
|
|
520
|
+
# these (COSMETIC_MEMBERS); emit a benign default (0 = na color /
|
|
521
|
+
# epoch 0). They have no backtest-logic effect.
|
|
522
|
+
if node.member in ("fg_color", "bg_color",
|
|
523
|
+
"left_visible_bar_time",
|
|
524
|
+
"right_visible_bar_time"):
|
|
525
|
+
return "0"
|
|
526
|
+
# Defensive: support_checker (COSMETIC_MEMBERS / UNSUPPORTED_MEMBERS)
|
|
527
|
+
# should already have warned/rejected any unhandled chart.* member.
|
|
528
|
+
# Reaching here is a bug.
|
|
480
529
|
raise ValueError(
|
|
481
530
|
f"codegen: unhandled chart.{node.member} — analyzer should have rejected. "
|
|
482
531
|
f"Add a handler above or extend UNSUPPORTED_MEMBERS."
|
|
@@ -580,6 +629,14 @@ class ExprVisitor:
|
|
|
580
629
|
if node.member in COLOR_CONST_MAP:
|
|
581
630
|
return COLOR_CONST_MAP[node.member]
|
|
582
631
|
return "0"
|
|
632
|
+
# Drawing style/visual CONSTANT member reads (line.style_solid,
|
|
633
|
+
# box.style_dashed, label.style_label_left, ...). These namespaces
|
|
634
|
+
# left SKIP_NAMESPACES (their FuncCall forms now route to the arena),
|
|
635
|
+
# but their constant members only ever feed dropped visual kwargs, so
|
|
636
|
+
# they still lower to "0". The function names arrive as FuncCalls
|
|
637
|
+
# (visit_call), never here. See spec §4.4.
|
|
638
|
+
if ns in DRAWING_STYLE_NS:
|
|
639
|
+
return "0"
|
|
583
640
|
if ns in SKIP_NAMESPACES:
|
|
584
641
|
return "0"
|
|
585
642
|
if ns == "currency":
|
|
@@ -651,10 +708,16 @@ class ExprVisitor:
|
|
|
651
708
|
or name in self._var_names
|
|
652
709
|
or name in self._current_loop_vars
|
|
653
710
|
or name in self._current_func_param_types
|
|
711
|
+
or name in self._current_func_locals
|
|
712
|
+
or name in self._udt_var_types
|
|
654
713
|
):
|
|
655
714
|
safe = self._safe_name(name)
|
|
656
715
|
if self._active_var_remap and safe in self._active_var_remap:
|
|
657
716
|
safe = self._active_var_remap[safe]
|
|
717
|
+
# Pointer-aliased UDT local (BUG C, rebinding case): field access
|
|
718
|
+
# goes through ``->`` since the local holds ``UDT*``.
|
|
719
|
+
if name in self._udt_ptr_alias_locals:
|
|
720
|
+
return f"{safe}->{node.member}"
|
|
658
721
|
return f"{safe}.{node.member}"
|
|
659
722
|
if name not in self.ctx.series_vars:
|
|
660
723
|
# Unknown identifier — likely an enum value
|
|
@@ -717,6 +780,31 @@ class ExprVisitor:
|
|
|
717
780
|
if series_name not in self._strategy_series_vars:
|
|
718
781
|
self._strategy_series_vars.add(series_name)
|
|
719
782
|
return f"{series_name}[{idx}]"
|
|
783
|
+
# History reference applied directly to an inline call result, e.g.
|
|
784
|
+
# ``ta.highest(high, 10)[1]`` or ``f()[2]``. In Pine the call yields a
|
|
785
|
+
# series, so ``[k]`` reads its value k bars ago — but the call lowers to
|
|
786
|
+
# a freshly-computed C++ scalar, and ``scalar[k]`` is not subscriptable.
|
|
787
|
+
# Materialize the result into a self-contained history buffer: a static
|
|
788
|
+
# ``Series<T>`` that pushes (new bar) / updates (intrabar) the value
|
|
789
|
+
# exactly once per evaluation — same semantics as every other series in
|
|
790
|
+
# the strategy — and read ``[k]`` off it. The inner call is emitted once
|
|
791
|
+
# so its own stateful indicator is not double-stepped, and the buffer
|
|
792
|
+
# clears itself on run-start (``is_first_tick_ && bar_index_ == 0``) so a
|
|
793
|
+
# reused strategy handle (parameter sweep) does not leak prior-run history.
|
|
794
|
+
if isinstance(node.object, FuncCall):
|
|
795
|
+
inner = self._visit_expr(node.object)
|
|
796
|
+
cpp_t = self._infer_type(node.object)
|
|
797
|
+
if cpp_t not in ("double", "int", "bool"):
|
|
798
|
+
cpp_t = "double"
|
|
799
|
+
return (
|
|
800
|
+
f"([&]() -> {cpp_t} {{ "
|
|
801
|
+
f"static thread_local Series<{cpp_t}> _hist_call; "
|
|
802
|
+
f"if (is_first_tick_ && bar_index_ == 0) _hist_call.clear(); "
|
|
803
|
+
f"{cpp_t} _hv = ({inner}); "
|
|
804
|
+
f"if (is_first_tick_) _hist_call.push(_hv); "
|
|
805
|
+
f"else _hist_call.update(_hv); "
|
|
806
|
+
f"return _hist_call[(int)({idx})]; }}())"
|
|
807
|
+
)
|
|
720
808
|
obj = self._visit_expr(node.object)
|
|
721
809
|
# If subscripting a non-series variable (e.g., function parameter),
|
|
722
810
|
# src[0] → src (current value), src[N>0] → src (can't access history)
|