@pineforge/codegen-pyodide 0.8.0 → 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 +509 -44
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +286 -27
- package/pineforge_codegen/codegen/input.py +14 -0
- package/pineforge_codegen/codegen/security.py +294 -13
- package/pineforge_codegen/codegen/tables.py +59 -3
- package/pineforge_codegen/codegen/types.py +369 -19
- package/pineforge_codegen/codegen/visit_call.py +147 -22
- package/pineforge_codegen/codegen/visit_expr.py +54 -0
- package/pineforge_codegen/codegen/visit_stmt.py +161 -14
- package/pineforge_codegen/parser.py +56 -23
- package/pineforge_codegen/support_checker.py +281 -17
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +2 -8
- package/pineforge_codegen-0.8.0.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"))
|
|
@@ -446,6 +488,16 @@ class CallVisitor:
|
|
|
446
488
|
if namespace is None and func_name == "color" and func_name not in self._func_names:
|
|
447
489
|
return "0"
|
|
448
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
|
+
|
|
449
501
|
# Skip visual/unsupported namespace calls
|
|
450
502
|
if namespace in SKIP_NAMESPACES or namespace in SKIP_VAR_TYPES:
|
|
451
503
|
return "0"
|
|
@@ -468,19 +520,34 @@ class CallVisitor:
|
|
|
468
520
|
all_args.append(None)
|
|
469
521
|
all_args[i] = node.kwargs[pname]
|
|
470
522
|
|
|
471
|
-
# Find matching security call ID
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
|
484
551
|
|
|
485
552
|
if sec_id is not None and expr_node is not None:
|
|
486
553
|
if isinstance(expr_node, TupleLiteral):
|
|
@@ -522,9 +589,12 @@ class CallVisitor:
|
|
|
522
589
|
|
|
523
590
|
# ticker.* calls
|
|
524
591
|
if namespace == "ticker":
|
|
525
|
-
# ticker.inherit(symbol, ...)
|
|
526
|
-
#
|
|
527
|
-
|
|
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"):
|
|
528
598
|
if node.args:
|
|
529
599
|
return self._visit_expr(node.args[0])
|
|
530
600
|
if "symbol" in node.kwargs:
|
|
@@ -915,10 +985,19 @@ class CallVisitor:
|
|
|
915
985
|
elif f.default:
|
|
916
986
|
val = self._visit_expr(f.default)
|
|
917
987
|
if val is not None:
|
|
918
|
-
# 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.
|
|
919
993
|
f_cpp_type = self._type_spec_to_cpp(field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name))
|
|
920
|
-
if f_cpp_type == "int"
|
|
921
|
-
|
|
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})"
|
|
922
1001
|
field_inits.append(f".{f.name} = {val}")
|
|
923
1002
|
# Mark the constructed object non-na (the struct's ``__pf_na`` is the
|
|
924
1003
|
# last declared field, so this designator stays in declaration order).
|
|
@@ -1012,6 +1091,17 @@ class CallVisitor:
|
|
|
1012
1091
|
all_args.extend(self._visit_expr(v) for v in node.kwargs.values())
|
|
1013
1092
|
else:
|
|
1014
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)
|
|
1015
1105
|
# Default args (parser does not store defaults): isInSession(sess, res = timeframe.period)
|
|
1016
1106
|
if namespace is None and func_name in self._func_names:
|
|
1017
1107
|
fi = self._func_info_map.get(func_name)
|
|
@@ -1023,7 +1113,16 @@ class CallVisitor:
|
|
|
1023
1113
|
emit_name = self._func_safe_name(func_name) if func_name in self._func_names else func_name
|
|
1024
1114
|
# Per-call-site variant: if this function has TA/series calls, call the correct variant
|
|
1025
1115
|
cs_info = self.ctx.func_call_cs_map.get(id(node))
|
|
1026
|
-
|
|
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:
|
|
1027
1126
|
# Inside a per-call-site variant: override the cs_map index with
|
|
1028
1127
|
# the parent's active call-site index. This ensures sub-functions
|
|
1029
1128
|
# called from ma_cs6() use their _cs6 variant, not _cs0.
|
|
@@ -1040,6 +1139,32 @@ class CallVisitor:
|
|
|
1040
1139
|
emit_name = f"{self._func_safe_name(func_name)}_cs{self._active_call_site_idx}"
|
|
1041
1140
|
return f"{prefix}{emit_name}({', '.join(all_args)})"
|
|
1042
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
|
+
|
|
1043
1168
|
def _visit_fixnan(self, node: FuncCall) -> str:
|
|
1044
1169
|
"""Emit fixnan with persistent state member."""
|
|
1045
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,
|
|
@@ -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>()
|
|
@@ -589,6 +629,14 @@ class ExprVisitor:
|
|
|
589
629
|
if node.member in COLOR_CONST_MAP:
|
|
590
630
|
return COLOR_CONST_MAP[node.member]
|
|
591
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"
|
|
592
640
|
if ns in SKIP_NAMESPACES:
|
|
593
641
|
return "0"
|
|
594
642
|
if ns == "currency":
|
|
@@ -660,10 +708,16 @@ class ExprVisitor:
|
|
|
660
708
|
or name in self._var_names
|
|
661
709
|
or name in self._current_loop_vars
|
|
662
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
|
|
663
713
|
):
|
|
664
714
|
safe = self._safe_name(name)
|
|
665
715
|
if self._active_var_remap and safe in self._active_var_remap:
|
|
666
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}"
|
|
667
721
|
return f"{safe}.{node.member}"
|
|
668
722
|
if name not in self.ctx.series_vars:
|
|
669
723
|
# Unknown identifier — likely an enum value
|
|
@@ -100,11 +100,16 @@ from ..ast_nodes import (
|
|
|
100
100
|
)
|
|
101
101
|
from ..symbols import TypeSpec
|
|
102
102
|
from .tables import (
|
|
103
|
+
ARRAY_NEW_CTORS,
|
|
103
104
|
TA_RETURNS_BOOL,
|
|
104
105
|
TA_TUPLE_FIELDS,
|
|
105
106
|
MATRIX_RETURNING_METHODS,
|
|
106
107
|
)
|
|
107
108
|
|
|
109
|
+
# Sentinel for "no block-scoped var remap was activated" so an empty dict
|
|
110
|
+
# saved-remap is still distinguishable from the no-op case.
|
|
111
|
+
_NO_BLOCK_REMAP = object()
|
|
112
|
+
|
|
108
113
|
|
|
109
114
|
class StmtVisitor:
|
|
110
115
|
"""Statement-level visitor methods shared across the codegen.
|
|
@@ -291,7 +296,7 @@ class StmtVisitor:
|
|
|
291
296
|
# array.new_float() etc., plus array-returning copy/slice.
|
|
292
297
|
if isinstance(node.value, FuncCall):
|
|
293
298
|
func_name, namespace = self._resolve_callee(node.value.callee)
|
|
294
|
-
if namespace == "array" and func_name in
|
|
299
|
+
if namespace == "array" and func_name in ARRAY_NEW_CTORS | {"new", "from", "copy", "slice"}:
|
|
295
300
|
self._array_vars.add(node.name)
|
|
296
301
|
spec = self._type_spec_from_expr(node.value) or self._array_spec_for_name(node.name)
|
|
297
302
|
self._collection_types.setdefault(node.name, spec)
|
|
@@ -401,12 +406,57 @@ class StmtVisitor:
|
|
|
401
406
|
self._visit_if_switch_expr(node.value, safe, lines, indent)
|
|
402
407
|
return
|
|
403
408
|
|
|
409
|
+
# UDT lvalue alias (BUG C): a local initialised from a user-defined-UDT
|
|
410
|
+
# var/global lvalue (or a ternary/switch of such lvalues) and then
|
|
411
|
+
# mutated through must ALIAS the global, not value-copy — Pine UDTs are
|
|
412
|
+
# reference types. Emit a C++ reference (non-rebinding) or pointer
|
|
413
|
+
# (rebinding) alias instead of the default copy.
|
|
414
|
+
if not is_global_member:
|
|
415
|
+
alias = self._udt_local_alias_kind(node)
|
|
416
|
+
if alias is not None:
|
|
417
|
+
kind, udt_t = alias
|
|
418
|
+
if kind == "ref":
|
|
419
|
+
cpp_val = self._visit_rhs_value(node.value, node.name, target_cpp_type=udt_t)
|
|
420
|
+
lines.append(f"{pad}{udt_t}& {safe} = {cpp_val};")
|
|
421
|
+
return
|
|
422
|
+
# Pointer alias: take address of each selected lvalue; subsequent
|
|
423
|
+
# field access lowers to ``->`` and rebinds to ``&(other)``.
|
|
424
|
+
self._udt_ptr_alias_locals.add(node.name)
|
|
425
|
+
cpp_val = self._addr_of_udt_selection(node.value, node.name)
|
|
426
|
+
lines.append(f"{pad}{udt_t}* {safe} = {cpp_val};")
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
# Collection lvalue alias (BUG 2): a local bound to an existing array /
|
|
430
|
+
# map / matrix lvalue (or a ternary/switch selecting same-typed ones)
|
|
431
|
+
# and later MUTATED through must ALIAS the member, not value-copy — Pine
|
|
432
|
+
# collections are reference types. Proven: jevondijefferson-big-breakout
|
|
433
|
+
# does ``array<orderBlock> orderBlocks = internal ? internalOrderBlocks
|
|
434
|
+
# : swingOrderBlocks`` then ``orderBlocks.unshift(ob)`` in three helpers;
|
|
435
|
+
# the value-copy left the member arrays empty. Emit a non-rebinding C++
|
|
436
|
+
# reference instead.
|
|
437
|
+
if not is_global_member:
|
|
438
|
+
coll_spec = self._collection_lvalue_selection_spec(node.value)
|
|
439
|
+
if coll_spec is not None and self._collection_local_must_alias(node):
|
|
440
|
+
# Register the local's collection kind so subsequent
|
|
441
|
+
# ``.size()/.get()/.unshift()`` dispatch resolves correctly.
|
|
442
|
+
self._collection_types[node.name] = coll_spec
|
|
443
|
+
if coll_spec.kind == "array":
|
|
444
|
+
self._array_vars.add(node.name)
|
|
445
|
+
elif coll_spec.kind == "map":
|
|
446
|
+
self._map_vars.add(node.name)
|
|
447
|
+
elif coll_spec.kind == "matrix":
|
|
448
|
+
self._matrix_specs[node.name] = coll_spec
|
|
449
|
+
cpp_type = self._type_spec_to_cpp(coll_spec)
|
|
450
|
+
cpp_val = self._visit_rhs_value(node.value, node.name, target_cpp_type=cpp_type)
|
|
451
|
+
lines.append(f"{pad}{cpp_type}& {safe} = {cpp_val};")
|
|
452
|
+
return
|
|
453
|
+
|
|
404
454
|
# General declaration
|
|
405
|
-
|
|
455
|
+
cpp_type = self._type_for_decl(node) if not is_global_member else None
|
|
456
|
+
cpp_val = self._visit_rhs_value(node.value, node.name, target_cpp_type=cpp_type)
|
|
406
457
|
if is_global_member:
|
|
407
458
|
lines.append(f"{pad}{safe} = {cpp_val};")
|
|
408
459
|
else:
|
|
409
|
-
cpp_type = self._type_for_decl(node)
|
|
410
460
|
lines.append(f"{pad}{cpp_type} {safe} = {cpp_val};")
|
|
411
461
|
|
|
412
462
|
@staticmethod
|
|
@@ -476,6 +526,12 @@ class StmtVisitor:
|
|
|
476
526
|
if self._active_var_remap and safe in self._active_var_remap:
|
|
477
527
|
safe = self._active_var_remap[safe]
|
|
478
528
|
|
|
529
|
+
# Pointer-aliased UDT local (BUG C, rebinding case): ``p := other``
|
|
530
|
+
# rebinds the pointer to the address of the newly selected UDT lvalue.
|
|
531
|
+
if target_name in self._udt_ptr_alias_locals and node.op == ":=":
|
|
532
|
+
lines.append(f"{pad}{safe} = {self._addr_of_udt_selection(node.value, target_name)};")
|
|
533
|
+
return
|
|
534
|
+
|
|
479
535
|
if target_name in self.ctx.series_vars:
|
|
480
536
|
val_cpp = self._visit_expr(node.value)
|
|
481
537
|
if node.op == ":=":
|
|
@@ -509,7 +565,7 @@ class StmtVisitor:
|
|
|
509
565
|
f"expected {self._type_spec_to_cpp(lhs_spec)}, "
|
|
510
566
|
f"got {self._type_spec_to_cpp(rhs_spec)}",
|
|
511
567
|
)
|
|
512
|
-
val_cpp = self.
|
|
568
|
+
val_cpp = self._visit_rhs_value(node.value, target_name)
|
|
513
569
|
if node.op == ":=":
|
|
514
570
|
lines.append(f"{pad}{safe} = {val_cpp};")
|
|
515
571
|
else:
|
|
@@ -519,7 +575,7 @@ class StmtVisitor:
|
|
|
519
575
|
else:
|
|
520
576
|
lines.append(f"{pad}{safe} {node.op} {val_cpp};")
|
|
521
577
|
else:
|
|
522
|
-
val_cpp = self.
|
|
578
|
+
val_cpp = self._visit_rhs_value(node.value, target_name)
|
|
523
579
|
if node.op == ":=":
|
|
524
580
|
lines.append(f"{pad}{safe} = {val_cpp};")
|
|
525
581
|
else:
|
|
@@ -600,7 +656,30 @@ class StmtVisitor:
|
|
|
600
656
|
|
|
601
657
|
lines.append(f"{pad}/* unsupported tuple assignment */")
|
|
602
658
|
|
|
659
|
+
def _push_block_var_remap(self, node):
|
|
660
|
+
"""Activate block-scoped var renames for ``node`` (BUG 1). Returns the
|
|
661
|
+
previous ``_active_var_remap`` to restore (or ``_NO_BLOCK_REMAP`` if this
|
|
662
|
+
block owns no renames). Renames are MERGED over the inherited remap so
|
|
663
|
+
nested blocks keep any enclosing func-clone / outer-block mapping."""
|
|
664
|
+
renames = self._block_var_renames.get(id(node))
|
|
665
|
+
if not renames:
|
|
666
|
+
return _NO_BLOCK_REMAP
|
|
667
|
+
saved = self._active_var_remap
|
|
668
|
+
self._active_var_remap = {**saved, **renames}
|
|
669
|
+
return saved
|
|
670
|
+
|
|
671
|
+
def _pop_block_var_remap(self, saved) -> None:
|
|
672
|
+
if saved is not _NO_BLOCK_REMAP:
|
|
673
|
+
self._active_var_remap = saved
|
|
674
|
+
|
|
603
675
|
def _visit_if(self, node: IfStmt, lines: list[str], indent: int) -> None:
|
|
676
|
+
_blk_saved = self._push_block_var_remap(node)
|
|
677
|
+
try:
|
|
678
|
+
self._visit_if_body(node, lines, indent)
|
|
679
|
+
finally:
|
|
680
|
+
self._pop_block_var_remap(_blk_saved)
|
|
681
|
+
|
|
682
|
+
def _visit_if_body(self, node: IfStmt, lines: list[str], indent: int) -> None:
|
|
604
683
|
pad = " " * indent
|
|
605
684
|
|
|
606
685
|
# TA hoisting: inside per-call-site function variants, execute ALL
|
|
@@ -637,20 +716,73 @@ class StmtVisitor:
|
|
|
637
716
|
pad = " " * indent
|
|
638
717
|
start = self._visit_expr(node.start)
|
|
639
718
|
end = self._visit_expr(node.end)
|
|
640
|
-
step = self._visit_expr(node.step) if node.step else "1"
|
|
641
719
|
var = node.var # new AST uses .var instead of .var_name
|
|
642
|
-
|
|
720
|
+
if node.step is not None:
|
|
721
|
+
# Explicit `by` step: unchanged from before — ascending compare
|
|
722
|
+
# (matches every existing corpus use, all positive literal steps).
|
|
723
|
+
step = self._visit_expr(node.step)
|
|
724
|
+
lines.append(f"{pad}for (int {var} = {start}; {var} <= {end}; {var} += {step}) {{")
|
|
725
|
+
else:
|
|
726
|
+
# No `by` clause: Pine v6 auto-infers the loop direction from
|
|
727
|
+
# start/end — descending (step -1) when start > end, else
|
|
728
|
+
# ascending (step +1); see the Pine v6 `for` reference. start/end
|
|
729
|
+
# are arbitrary runtime expressions (``for i = array.size(arr)-1
|
|
730
|
+
# to 0`` — a common "iterate backward to safely remove an element
|
|
731
|
+
# while iterating" idiom), so the direction can't always be
|
|
732
|
+
# resolved at codegen time. Compute start/end into locals ONCE
|
|
733
|
+
# (avoids re-evaluating a side-effecting expression, same class
|
|
734
|
+
# of bug as nz()'s double-eval) and pick the comparison direction
|
|
735
|
+
# at runtime from their relative order — this previously always
|
|
736
|
+
# emitted an ascending `<=` loop, which never executes when
|
|
737
|
+
# start > end (silently dropping the whole loop body).
|
|
738
|
+
fid = self._for_counter
|
|
739
|
+
self._for_counter += 1
|
|
740
|
+
s_var, e_var = f"_for_start_{fid}", f"_for_end_{fid}"
|
|
741
|
+
lines.append(f"{pad}int {s_var} = ({start}), {e_var} = ({end});")
|
|
742
|
+
lines.append(
|
|
743
|
+
f"{pad}for (int {var} = {s_var}; "
|
|
744
|
+
f"({s_var} <= {e_var}) ? ({var} <= {e_var}) : ({var} >= {e_var}); "
|
|
745
|
+
f"{var} += ({s_var} <= {e_var}) ? 1 : -1) {{"
|
|
746
|
+
)
|
|
643
747
|
# Register the loop counter so reads of it inside the body resolve (the
|
|
644
748
|
# unknown-identifier guard in _visit_ident would otherwise flag it).
|
|
645
749
|
saved_loop = self._current_loop_vars
|
|
646
750
|
self._current_loop_vars = set(self._current_loop_vars)
|
|
647
751
|
if var:
|
|
648
752
|
self._current_loop_vars.add(var)
|
|
649
|
-
|
|
650
|
-
|
|
753
|
+
_blk_saved = self._push_block_var_remap(node)
|
|
754
|
+
try:
|
|
755
|
+
for s in node.body:
|
|
756
|
+
self._visit_stmt(s, lines, indent + 1)
|
|
757
|
+
finally:
|
|
758
|
+
self._pop_block_var_remap(_blk_saved)
|
|
651
759
|
self._current_loop_vars = saved_loop
|
|
652
760
|
lines.append(f"{pad}}}")
|
|
653
761
|
|
|
762
|
+
def _loop_elem_is_writeback_udt(self, iterable) -> bool:
|
|
763
|
+
"""Whether a ``for x in coll`` loop variable must bind by reference.
|
|
764
|
+
|
|
765
|
+
In Pine a ``for x in arr`` loop variable over an array of *user-defined
|
|
766
|
+
objects* is a reference to the element — field writes (``x.f := v``)
|
|
767
|
+
mutate the array in place — whereas over a primitive array it is a
|
|
768
|
+
copy. So emit C++ ``auto&`` only for arrays whose element is a
|
|
769
|
+
user-defined UDT struct. Primitive elements keep ``auto`` (Pine copy
|
|
770
|
+
semantics: writing the loop var must NOT write back). Drawing handles
|
|
771
|
+
(line/box/label/linefill/...) also keep ``auto``: their element type
|
|
772
|
+
name is a builtin, not in ``_udt_defs``, and a handle copy already
|
|
773
|
+
mutates the shared engine object. (Reassigning the loop var itself —
|
|
774
|
+
``x := ...`` — is not modelled by either form, but Pine forbids it for
|
|
775
|
+
objects in practice and it does not occur in the corpus.)
|
|
776
|
+
"""
|
|
777
|
+
spec = self._type_spec_from_expr(iterable)
|
|
778
|
+
return (
|
|
779
|
+
spec is not None
|
|
780
|
+
and spec.kind == "array"
|
|
781
|
+
and spec.element is not None
|
|
782
|
+
and spec.element.kind == "udt"
|
|
783
|
+
and spec.element.name in self._udt_defs
|
|
784
|
+
)
|
|
785
|
+
|
|
654
786
|
def _visit_for_in(self, node, lines: list[str], indent: int) -> None:
|
|
655
787
|
pad = " " * indent
|
|
656
788
|
iterable = self._visit_expr(node.iterable)
|
|
@@ -664,12 +796,17 @@ class StmtVisitor:
|
|
|
664
796
|
self._current_loop_vars.add(v)
|
|
665
797
|
if node.var:
|
|
666
798
|
v_cpp = self._safe_name(node.var)
|
|
667
|
-
|
|
799
|
+
ref = "&" if self._loop_elem_is_writeback_udt(node.iterable) else ""
|
|
800
|
+
lines.append(f"{pad}for (auto{ref} {v_cpp} : {iterable}) {{")
|
|
668
801
|
elif node.vars:
|
|
669
802
|
bindings = ", ".join(node.vars)
|
|
670
803
|
lines.append(f"{pad}for (auto [{bindings}] : {iterable}) {{")
|
|
671
|
-
|
|
672
|
-
|
|
804
|
+
_blk_saved = self._push_block_var_remap(node)
|
|
805
|
+
try:
|
|
806
|
+
for s in node.body:
|
|
807
|
+
self._visit_stmt(s, lines, indent + 1)
|
|
808
|
+
finally:
|
|
809
|
+
self._pop_block_var_remap(_blk_saved)
|
|
673
810
|
lines.append(f"{pad}}}")
|
|
674
811
|
self._current_loop_vars = saved_loop
|
|
675
812
|
|
|
@@ -677,8 +814,12 @@ class StmtVisitor:
|
|
|
677
814
|
pad = " " * indent
|
|
678
815
|
cond = self._visit_expr(node.condition)
|
|
679
816
|
lines.append(f"{pad}while ({cond}) {{")
|
|
680
|
-
|
|
681
|
-
|
|
817
|
+
_blk_saved = self._push_block_var_remap(node)
|
|
818
|
+
try:
|
|
819
|
+
for s in node.body:
|
|
820
|
+
self._visit_stmt(s, lines, indent + 1)
|
|
821
|
+
finally:
|
|
822
|
+
self._pop_block_var_remap(_blk_saved)
|
|
682
823
|
lines.append(f"{pad}}}")
|
|
683
824
|
|
|
684
825
|
def _visit_switch(self, node: SwitchStmt, lines: list[str], indent: int) -> None:
|
|
@@ -727,6 +868,12 @@ class StmtVisitor:
|
|
|
727
868
|
# Check if it's a skip expr
|
|
728
869
|
if self._is_skip_expr(stmt.expr):
|
|
729
870
|
return
|
|
871
|
+
# A void drawing setter / delete / visual-noop cannot be the
|
|
872
|
+
# branch's value (it lowers to a void C++ call) — emit it as
|
|
873
|
+
# a statement and leave ``target`` at its default.
|
|
874
|
+
if self._call_is_void(stmt.expr):
|
|
875
|
+
self._visit_stmt(stmt, lines, indent)
|
|
876
|
+
return
|
|
730
877
|
cpp = self._visit_expr(stmt.expr)
|
|
731
878
|
pad = " " * indent
|
|
732
879
|
lines.append(f"{pad}{target} = {cpp};")
|