@pineforge/codegen-pyodide 0.8.0 → 0.9.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/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +797 -45
- package/pineforge_codegen/analyzer/call_handlers.py +368 -68
- package/pineforge_codegen/analyzer/contracts.py +91 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +1613 -111
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +536 -45
- package/pineforge_codegen/codegen/input.py +31 -0
- package/pineforge_codegen/codegen/security.py +791 -39
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +115 -10
- package/pineforge_codegen/codegen/types.py +478 -46
- package/pineforge_codegen/codegen/visit_call.py +331 -75
- package/pineforge_codegen/codegen/visit_expr.py +116 -14
- package/pineforge_codegen/codegen/visit_stmt.py +208 -23
- package/pineforge_codegen/parser.py +129 -29
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +320 -19
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -10
- package/pineforge_codegen-0.8.0.tar.gz +0 -0
|
@@ -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>()
|
|
@@ -235,6 +275,16 @@ class ExprVisitor:
|
|
|
235
275
|
return str(val)
|
|
236
276
|
if isinstance(val, str):
|
|
237
277
|
return f'std::string("{val}")'
|
|
278
|
+
# TA runtime-reset lowering: an input-backed var renders as its
|
|
279
|
+
# override-aware getter (not the member name), because the reset may
|
|
280
|
+
# run before the input members are initialised (evaluate_security path).
|
|
281
|
+
if (self._reset_input_getter_mode
|
|
282
|
+
and name in self._input_backed_vars
|
|
283
|
+
and name in self._input_var_to_call):
|
|
284
|
+
call_node = self._input_var_to_call[name]
|
|
285
|
+
func_name_i, namespace_i = self._resolve_callee(call_node.callee)
|
|
286
|
+
title = self._get_input_title(call_node, var_name=name)
|
|
287
|
+
return self._render_input_value(call_node, func_name_i, namespace_i, title)
|
|
238
288
|
# Pine type name `color` used as a value (no variable) → int64 color constant.
|
|
239
289
|
# Params handled above; symbol table does not retain function locals after analysis.
|
|
240
290
|
if name == "color":
|
|
@@ -333,7 +383,12 @@ class ExprVisitor:
|
|
|
333
383
|
if node.member == "position_size":
|
|
334
384
|
return "signed_position_size()"
|
|
335
385
|
if node.member == "position_avg_price":
|
|
336
|
-
|
|
386
|
+
# Pine returns `na` when the position is flat
|
|
387
|
+
# (position_size == 0); the engine field is 0.0 when flat.
|
|
388
|
+
# Guard so the common `na(strategy.position_avg_price)`
|
|
389
|
+
# flat/in-position idiom is not silently defeated.
|
|
390
|
+
return ("(signed_position_size() == 0.0 ? na<double>() "
|
|
391
|
+
": position_entry_price_)")
|
|
337
392
|
if node.member == "position_entry_name":
|
|
338
393
|
return "position_entry_name()"
|
|
339
394
|
# Trade counts
|
|
@@ -438,13 +493,34 @@ class ExprVisitor:
|
|
|
438
493
|
(node.member in TA_IMPLICIT_COMPUTE_FULL and node.member in TA_COMPUTE_ARGS and TA_COMPUTE_ARGS[node.member] == [])
|
|
439
494
|
or node.member == "vwap"
|
|
440
495
|
):
|
|
441
|
-
# Find the matching call site
|
|
442
|
-
|
|
496
|
+
# Find the matching call site. Skip sites pruned as dead
|
|
497
|
+
# code (their owner function is never called): a dead site's
|
|
498
|
+
# member declaration is never emitted (see base.py
|
|
499
|
+
# ``_dead_ta_indices`` / emit_top.py member-decl guard), so
|
|
500
|
+
# binding a LIVE bare property read to one references an
|
|
501
|
+
# undeclared member. Regression: nightowlxtrader-azt — a live
|
|
502
|
+
# ``ta.vwap`` (top-level + inside request.security) resolved
|
|
503
|
+
# to dead ``f5``'s first-in-order ``_ta_vwap_10`` and emitted
|
|
504
|
+
# ``use of undeclared identifier '_ta_vwap_10'``. A live read
|
|
505
|
+
# must bind to a live site.
|
|
506
|
+
for _i, site in enumerate(self.ctx.ta_call_sites):
|
|
507
|
+
if _i in self._dead_ta_indices:
|
|
508
|
+
continue
|
|
443
509
|
ta_short = site.class_name.split("::")[-1].lower()
|
|
444
510
|
if site.member_name.startswith(f"_ta_{node.member}_"):
|
|
445
511
|
if node.member == "vwap":
|
|
446
|
-
return
|
|
447
|
-
|
|
512
|
+
return (
|
|
513
|
+
f"(history_advances_new_bar() ? {site.member_name}.compute("
|
|
514
|
+
"current_bar_.close, current_bar_.volume, current_bar_.timestamp) "
|
|
515
|
+
f": {site.member_name}.recompute(current_bar_.close, "
|
|
516
|
+
"current_bar_.volume, current_bar_.timestamp))"
|
|
517
|
+
)
|
|
518
|
+
return (
|
|
519
|
+
f"(history_advances_new_bar() ? {site.member_name}.compute("
|
|
520
|
+
f"{TA_IMPLICIT_COMPUTE_FULL[node.member]}) : "
|
|
521
|
+
f"{site.member_name}.recompute("
|
|
522
|
+
f"{TA_IMPLICIT_COMPUTE_FULL[node.member]}))"
|
|
523
|
+
)
|
|
448
524
|
# No registered call site for this TA property read —
|
|
449
525
|
# the old fallback emitted std::string("<name>"), a
|
|
450
526
|
# silent type mismatch. Reject loudly instead.
|
|
@@ -589,6 +665,14 @@ class ExprVisitor:
|
|
|
589
665
|
if node.member in COLOR_CONST_MAP:
|
|
590
666
|
return COLOR_CONST_MAP[node.member]
|
|
591
667
|
return "0"
|
|
668
|
+
# Drawing style/visual CONSTANT member reads (line.style_solid,
|
|
669
|
+
# box.style_dashed, label.style_label_left, ...). These namespaces
|
|
670
|
+
# left SKIP_NAMESPACES (their FuncCall forms now route to the arena),
|
|
671
|
+
# but their constant members only ever feed dropped visual kwargs, so
|
|
672
|
+
# they still lower to "0". The function names arrive as FuncCalls
|
|
673
|
+
# (visit_call), never here. See spec §4.4.
|
|
674
|
+
if ns in DRAWING_STYLE_NS:
|
|
675
|
+
return "0"
|
|
592
676
|
if ns in SKIP_NAMESPACES:
|
|
593
677
|
return "0"
|
|
594
678
|
if ns == "currency":
|
|
@@ -660,10 +744,16 @@ class ExprVisitor:
|
|
|
660
744
|
or name in self._var_names
|
|
661
745
|
or name in self._current_loop_vars
|
|
662
746
|
or name in self._current_func_param_types
|
|
747
|
+
or name in self._current_func_locals
|
|
748
|
+
or name in self._udt_var_types
|
|
663
749
|
):
|
|
664
750
|
safe = self._safe_name(name)
|
|
665
751
|
if self._active_var_remap and safe in self._active_var_remap:
|
|
666
752
|
safe = self._active_var_remap[safe]
|
|
753
|
+
# Pointer-aliased UDT local (BUG C, rebinding case): field access
|
|
754
|
+
# goes through ``->`` since the local holds ``UDT*``.
|
|
755
|
+
if name in self._udt_ptr_alias_locals:
|
|
756
|
+
return f"{safe}->{node.member}"
|
|
667
757
|
return f"{safe}.{node.member}"
|
|
668
758
|
if name not in self.ctx.series_vars:
|
|
669
759
|
# Unknown identifier — likely an enum value
|
|
@@ -675,6 +765,18 @@ class ExprVisitor:
|
|
|
675
765
|
right = self._visit_expr(node.right)
|
|
676
766
|
cpp_ops = {"and": "&&", "or": "||"}
|
|
677
767
|
op = cpp_ops.get(node.op, node.op)
|
|
768
|
+
if node.op == "+":
|
|
769
|
+
lt = self._infer_type(node.left)
|
|
770
|
+
rt = self._infer_type(node.right)
|
|
771
|
+
if lt == "std::string" or rt == "std::string":
|
|
772
|
+
def _as_string(rendered, inferred):
|
|
773
|
+
if inferred == "std::string":
|
|
774
|
+
return rendered
|
|
775
|
+
if inferred == "bool":
|
|
776
|
+
return f'(({rendered}) ? std::string("true") : std::string("false"))'
|
|
777
|
+
return f"std::to_string({rendered})"
|
|
778
|
+
|
|
779
|
+
return f"({_as_string(left, lt)} + {_as_string(right, rt)})"
|
|
678
780
|
# PineScript % works on floats — use std::fmod in C++
|
|
679
781
|
if node.op == "%":
|
|
680
782
|
return f"std::fmod((double)({left}), (double)({right}))"
|
|
@@ -730,26 +832,26 @@ class ExprVisitor:
|
|
|
730
832
|
# ``ta.highest(high, 10)[1]`` or ``f()[2]``. In Pine the call yields a
|
|
731
833
|
# series, so ``[k]`` reads its value k bars ago — but the call lowers to
|
|
732
834
|
# a freshly-computed C++ scalar, and ``scalar[k]`` is not subscriptable.
|
|
733
|
-
# Materialize the result into a
|
|
835
|
+
# Materialize the result into a checkpoint-owned class-member
|
|
734
836
|
# ``Series<T>`` that pushes (new bar) / updates (intrabar) the value
|
|
735
837
|
# exactly once per evaluation — same semantics as every other series in
|
|
736
838
|
# the strategy — and read ``[k]`` off it. The inner call is emitted once
|
|
737
|
-
# so its own stateful indicator is not double-stepped
|
|
738
|
-
#
|
|
739
|
-
#
|
|
839
|
+
# so its own stateful indicator is not double-stepped. The deterministic
|
|
840
|
+
# member identity includes the emitted UDF variant; separate Pine call
|
|
841
|
+
# sites never share history, and on_bar clears every synthetic member at
|
|
842
|
+
# run-start even when this particular expression is conditional.
|
|
740
843
|
if isinstance(node.object, FuncCall):
|
|
741
844
|
inner = self._visit_expr(node.object)
|
|
742
845
|
cpp_t = self._infer_type(node.object)
|
|
743
846
|
if cpp_t not in ("double", "int", "bool"):
|
|
744
847
|
cpp_t = "double"
|
|
848
|
+
member = self._inline_history_member("hist_call", node)
|
|
745
849
|
return (
|
|
746
850
|
f"([&]() -> {cpp_t} {{ "
|
|
747
|
-
f"static thread_local Series<{cpp_t}> _hist_call; "
|
|
748
|
-
f"if (is_first_tick_ && bar_index_ == 0) _hist_call.clear(); "
|
|
749
851
|
f"{cpp_t} _hv = ({inner}); "
|
|
750
|
-
f"if (
|
|
751
|
-
f"else
|
|
752
|
-
f"return
|
|
852
|
+
f"if (history_advances_new_bar()) {member}.push(_hv); "
|
|
853
|
+
f"else {member}.update(_hv); "
|
|
854
|
+
f"return {member}[(int)({idx})]; }}())"
|
|
753
855
|
)
|
|
754
856
|
obj = self._visit_expr(node.object)
|
|
755
857
|
# If subscripting a non-series variable (e.g., function parameter),
|
|
@@ -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.
|
|
@@ -253,6 +258,10 @@ class StmtVisitor:
|
|
|
253
258
|
# Global-scope non-var vars are class members — emit assignment, not declaration
|
|
254
259
|
is_global_member = node.name in self._global_member_vars
|
|
255
260
|
|
|
261
|
+
def remember_local_type(cpp_type: str | None) -> None:
|
|
262
|
+
if cpp_type and not is_global_member:
|
|
263
|
+
self._current_func_local_types[node.name] = cpp_type
|
|
264
|
+
|
|
256
265
|
# Check if it is a static (non-series) global member variable already evaluated inside _inputs_initialized_ block
|
|
257
266
|
is_static_global_input = False
|
|
258
267
|
if is_global_member and isinstance(node.value, FuncCall) and self._is_input_call(node.value):
|
|
@@ -279,7 +288,7 @@ class StmtVisitor:
|
|
|
279
288
|
title = self._get_input_title(node.value, var_name=node.name)
|
|
280
289
|
cpp_val = self._render_input_value(node.value, func_name_i, namespace_i, title)
|
|
281
290
|
if node.name in self.ctx.series_vars:
|
|
282
|
-
|
|
291
|
+
self._emit_history_series_write(lines, pad, safe, cpp_val)
|
|
283
292
|
elif is_global_member:
|
|
284
293
|
lines.append(f"{pad}{safe} = {cpp_val};")
|
|
285
294
|
else:
|
|
@@ -291,7 +300,7 @@ class StmtVisitor:
|
|
|
291
300
|
# array.new_float() etc., plus array-returning copy/slice.
|
|
292
301
|
if isinstance(node.value, FuncCall):
|
|
293
302
|
func_name, namespace = self._resolve_callee(node.value.callee)
|
|
294
|
-
if namespace == "array" and func_name in
|
|
303
|
+
if namespace == "array" and func_name in ARRAY_NEW_CTORS | {"new", "from", "copy", "slice"}:
|
|
295
304
|
self._array_vars.add(node.name)
|
|
296
305
|
spec = self._type_spec_from_expr(node.value) or self._array_spec_for_name(node.name)
|
|
297
306
|
self._collection_types.setdefault(node.name, spec)
|
|
@@ -376,9 +385,12 @@ class StmtVisitor:
|
|
|
376
385
|
compute_args = self._ta_compute_args_for_site(site)
|
|
377
386
|
ret_type = "bool" if self._ta_name_from_site(site) in TA_RETURNS_BOOL else "double"
|
|
378
387
|
ta_name = self._ta_member_name(site)
|
|
379
|
-
ta_expr =
|
|
388
|
+
ta_expr = (
|
|
389
|
+
f"(history_advances_new_bar() ? {ta_name}.compute({compute_args}) "
|
|
390
|
+
f": {ta_name}.recompute({compute_args}))"
|
|
391
|
+
)
|
|
380
392
|
if node.name in self.ctx.series_vars:
|
|
381
|
-
|
|
393
|
+
self._emit_history_series_write(lines, pad, safe, ta_expr)
|
|
382
394
|
elif is_global_member:
|
|
383
395
|
lines.append(f"{pad}{safe} = {ta_expr};")
|
|
384
396
|
else:
|
|
@@ -388,7 +400,7 @@ class StmtVisitor:
|
|
|
388
400
|
# Non-var series variable — push instead of declare
|
|
389
401
|
if node.name in self.ctx.series_vars:
|
|
390
402
|
cpp_val = self._visit_expr(node.value)
|
|
391
|
-
|
|
403
|
+
self._emit_history_series_write(lines, pad, safe, cpp_val)
|
|
392
404
|
return
|
|
393
405
|
|
|
394
406
|
# If/switch expression: x = if cond ... else ...
|
|
@@ -401,12 +413,74 @@ class StmtVisitor:
|
|
|
401
413
|
self._visit_if_switch_expr(node.value, safe, lines, indent)
|
|
402
414
|
return
|
|
403
415
|
|
|
416
|
+
# UDT lvalue alias (BUG C): a local initialised from a user-defined-UDT
|
|
417
|
+
# var/global lvalue (or a ternary/switch of such lvalues) and then
|
|
418
|
+
# mutated through must ALIAS the global, not value-copy — Pine UDTs are
|
|
419
|
+
# reference types. Emit a C++ reference (non-rebinding) or pointer
|
|
420
|
+
# (rebinding) alias instead of the default copy.
|
|
421
|
+
if not is_global_member:
|
|
422
|
+
alias = self._udt_local_alias_kind(node)
|
|
423
|
+
if alias is not None:
|
|
424
|
+
kind, udt_t = alias
|
|
425
|
+
if kind == "ref":
|
|
426
|
+
cpp_val = self._visit_rhs_value(node.value, node.name, target_cpp_type=udt_t)
|
|
427
|
+
lines.append(f"{pad}{udt_t}& {safe} = {cpp_val};")
|
|
428
|
+
return
|
|
429
|
+
# Pointer alias: take address of each selected lvalue; subsequent
|
|
430
|
+
# field access lowers to ``->`` and rebinds to ``&(other)``.
|
|
431
|
+
self._udt_ptr_alias_locals.add(node.name)
|
|
432
|
+
cpp_val = self._addr_of_udt_selection(node.value, node.name)
|
|
433
|
+
lines.append(f"{pad}{udt_t}* {safe} = {cpp_val};")
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
# Collection lvalue alias (BUG 2): a local bound to an existing array /
|
|
437
|
+
# map / matrix lvalue (or a ternary/switch selecting same-typed ones)
|
|
438
|
+
# and later MUTATED through must ALIAS the member, not value-copy — Pine
|
|
439
|
+
# collections are reference types. Proven: jevondijefferson-big-breakout
|
|
440
|
+
# does ``array<orderBlock> orderBlocks = internal ? internalOrderBlocks
|
|
441
|
+
# : swingOrderBlocks`` then ``orderBlocks.unshift(ob)`` in three helpers;
|
|
442
|
+
# the value-copy left the member arrays empty. Emit a non-rebinding C++
|
|
443
|
+
# reference instead.
|
|
444
|
+
if not is_global_member:
|
|
445
|
+
coll_spec = self._collection_lvalue_selection_spec(node.value)
|
|
446
|
+
if coll_spec is not None and self._collection_local_must_alias(node):
|
|
447
|
+
# Register the local's collection kind so subsequent
|
|
448
|
+
# ``.size()/.get()/.unshift()`` dispatch resolves correctly.
|
|
449
|
+
self._collection_types[node.name] = coll_spec
|
|
450
|
+
if coll_spec.kind == "array":
|
|
451
|
+
self._array_vars.add(node.name)
|
|
452
|
+
elif coll_spec.kind == "map":
|
|
453
|
+
self._map_vars.add(node.name)
|
|
454
|
+
elif coll_spec.kind == "matrix":
|
|
455
|
+
self._matrix_specs[node.name] = coll_spec
|
|
456
|
+
cpp_type = self._type_spec_to_cpp(coll_spec)
|
|
457
|
+
cpp_val = self._visit_rhs_value(node.value, node.name, target_cpp_type=cpp_type)
|
|
458
|
+
lines.append(f"{pad}{cpp_type}& {safe} = {cpp_val};")
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
# Global-scope UDT array-element alias (BUG: Pine array elements of a
|
|
462
|
+
# user-defined type are references). A global loop-local bound from
|
|
463
|
+
# ``arr.get(i)`` and later field-mutated must ALIAS the element so the
|
|
464
|
+
# mutation writes back into the array. Emit a fresh per-iteration
|
|
465
|
+
# ``UDT& z = arr[i];`` reference (any hoisted class member was
|
|
466
|
+
# suppressed) — identical to the non-hoisted function-local alias path.
|
|
467
|
+
# Gated by _register_udt_array_get_ref_locals (which only records global-
|
|
468
|
+
# scope names); the ``_current_func_body is None`` guard confines this to
|
|
469
|
+
# global scope, so a function-local sharing the name keeps its own path.
|
|
470
|
+
if (node.name in self._udt_array_get_ref_locals
|
|
471
|
+
and getattr(self, "_current_func_body", None) is None):
|
|
472
|
+
udt_t = self._udt_var_types.get(node.name)
|
|
473
|
+
cpp_val = self._visit_rhs_value(node.value, node.name, target_cpp_type=udt_t)
|
|
474
|
+
lines.append(f"{pad}{udt_t}& {safe} = {cpp_val};")
|
|
475
|
+
return
|
|
476
|
+
|
|
404
477
|
# General declaration
|
|
405
|
-
|
|
478
|
+
cpp_type = self._type_for_decl(node) if not is_global_member else None
|
|
479
|
+
cpp_val = self._visit_rhs_value(node.value, node.name, target_cpp_type=cpp_type)
|
|
406
480
|
if is_global_member:
|
|
407
481
|
lines.append(f"{pad}{safe} = {cpp_val};")
|
|
408
482
|
else:
|
|
409
|
-
cpp_type
|
|
483
|
+
remember_local_type(cpp_type)
|
|
410
484
|
lines.append(f"{pad}{cpp_type} {safe} = {cpp_val};")
|
|
411
485
|
|
|
412
486
|
@staticmethod
|
|
@@ -476,6 +550,12 @@ class StmtVisitor:
|
|
|
476
550
|
if self._active_var_remap and safe in self._active_var_remap:
|
|
477
551
|
safe = self._active_var_remap[safe]
|
|
478
552
|
|
|
553
|
+
# Pointer-aliased UDT local (BUG C, rebinding case): ``p := other``
|
|
554
|
+
# rebinds the pointer to the address of the newly selected UDT lvalue.
|
|
555
|
+
if target_name in self._udt_ptr_alias_locals and node.op == ":=":
|
|
556
|
+
lines.append(f"{pad}{safe} = {self._addr_of_udt_selection(node.value, target_name)};")
|
|
557
|
+
return
|
|
558
|
+
|
|
479
559
|
if target_name in self.ctx.series_vars:
|
|
480
560
|
val_cpp = self._visit_expr(node.value)
|
|
481
561
|
if node.op == ":=":
|
|
@@ -509,7 +589,7 @@ class StmtVisitor:
|
|
|
509
589
|
f"expected {self._type_spec_to_cpp(lhs_spec)}, "
|
|
510
590
|
f"got {self._type_spec_to_cpp(rhs_spec)}",
|
|
511
591
|
)
|
|
512
|
-
val_cpp = self.
|
|
592
|
+
val_cpp = self._visit_rhs_value(node.value, target_name)
|
|
513
593
|
if node.op == ":=":
|
|
514
594
|
lines.append(f"{pad}{safe} = {val_cpp};")
|
|
515
595
|
else:
|
|
@@ -519,7 +599,7 @@ class StmtVisitor:
|
|
|
519
599
|
else:
|
|
520
600
|
lines.append(f"{pad}{safe} {node.op} {val_cpp};")
|
|
521
601
|
else:
|
|
522
|
-
val_cpp = self.
|
|
602
|
+
val_cpp = self._visit_rhs_value(node.value, target_name)
|
|
523
603
|
if node.op == ":=":
|
|
524
604
|
lines.append(f"{pad}{safe} = {val_cpp};")
|
|
525
605
|
else:
|
|
@@ -535,7 +615,11 @@ class StmtVisitor:
|
|
|
535
615
|
compute_args = self._ta_compute_args_for_site(site)
|
|
536
616
|
ta_mem = self._ta_member_name(site)
|
|
537
617
|
result_var = f"_result_{ta_mem}"
|
|
538
|
-
lines.append(
|
|
618
|
+
lines.append(
|
|
619
|
+
f"{pad}auto {result_var} = (history_advances_new_bar() ? "
|
|
620
|
+
f"{ta_mem}.compute({compute_args}) : "
|
|
621
|
+
f"{ta_mem}.recompute({compute_args}));"
|
|
622
|
+
)
|
|
539
623
|
|
|
540
624
|
ta_name = self._ta_name_from_site(site)
|
|
541
625
|
fields = TA_TUPLE_FIELDS.get(ta_name, [f"field{i}" for i in range(len(node.names))])
|
|
@@ -555,7 +639,9 @@ class StmtVisitor:
|
|
|
555
639
|
# destructured names keep the plain scalar declaration.
|
|
556
640
|
if name in self.ctx.series_vars:
|
|
557
641
|
safe = self._safe_name(name)
|
|
558
|
-
|
|
642
|
+
self._emit_history_series_write(
|
|
643
|
+
lines, pad, safe, field_expr
|
|
644
|
+
)
|
|
559
645
|
else:
|
|
560
646
|
lines.append(f"{pad}double {name} = {field_expr};")
|
|
561
647
|
return
|
|
@@ -600,7 +686,30 @@ class StmtVisitor:
|
|
|
600
686
|
|
|
601
687
|
lines.append(f"{pad}/* unsupported tuple assignment */")
|
|
602
688
|
|
|
689
|
+
def _push_block_var_remap(self, node):
|
|
690
|
+
"""Activate block-scoped var renames for ``node`` (BUG 1). Returns the
|
|
691
|
+
previous ``_active_var_remap`` to restore (or ``_NO_BLOCK_REMAP`` if this
|
|
692
|
+
block owns no renames). Renames are MERGED over the inherited remap so
|
|
693
|
+
nested blocks keep any enclosing func-clone / outer-block mapping."""
|
|
694
|
+
renames = self._block_var_renames.get(id(node))
|
|
695
|
+
if not renames:
|
|
696
|
+
return _NO_BLOCK_REMAP
|
|
697
|
+
saved = self._active_var_remap
|
|
698
|
+
self._active_var_remap = {**saved, **renames}
|
|
699
|
+
return saved
|
|
700
|
+
|
|
701
|
+
def _pop_block_var_remap(self, saved) -> None:
|
|
702
|
+
if saved is not _NO_BLOCK_REMAP:
|
|
703
|
+
self._active_var_remap = saved
|
|
704
|
+
|
|
603
705
|
def _visit_if(self, node: IfStmt, lines: list[str], indent: int) -> None:
|
|
706
|
+
_blk_saved = self._push_block_var_remap(node)
|
|
707
|
+
try:
|
|
708
|
+
self._visit_if_body(node, lines, indent)
|
|
709
|
+
finally:
|
|
710
|
+
self._pop_block_var_remap(_blk_saved)
|
|
711
|
+
|
|
712
|
+
def _visit_if_body(self, node: IfStmt, lines: list[str], indent: int) -> None:
|
|
604
713
|
pad = " " * indent
|
|
605
714
|
|
|
606
715
|
# TA hoisting: inside per-call-site function variants, execute ALL
|
|
@@ -637,48 +746,118 @@ class StmtVisitor:
|
|
|
637
746
|
pad = " " * indent
|
|
638
747
|
start = self._visit_expr(node.start)
|
|
639
748
|
end = self._visit_expr(node.end)
|
|
640
|
-
step = self._visit_expr(node.step) if node.step else "1"
|
|
641
|
-
var = node.var # new AST uses .var instead of .var_name
|
|
642
|
-
|
|
749
|
+
step = self._visit_expr(node.step) if node.step is not None else "1"
|
|
750
|
+
var = self._safe_name(node.var) # new AST uses .var instead of .var_name
|
|
751
|
+
|
|
752
|
+
# Pine infers loop direction from the initial ``from``/``to`` values for
|
|
753
|
+
# both implicit and explicit ``by`` loops. The ``by`` value is a positive
|
|
754
|
+
# magnitude; descending loops subtract it. ``to`` can change during the
|
|
755
|
+
# loop, so refresh the cached end expression after each iteration while
|
|
756
|
+
# keeping the initial direction and step fixed.
|
|
757
|
+
fid = self._for_counter
|
|
758
|
+
self._for_counter += 1
|
|
759
|
+
s_var = f"_for_start_{fid}"
|
|
760
|
+
e_var = f"_for_end_{fid}"
|
|
761
|
+
step_var = f"_for_step_{fid}"
|
|
762
|
+
down_var = f"_for_down_{fid}"
|
|
763
|
+
lines.append(f"{pad}int {s_var} = ({start});")
|
|
764
|
+
lines.append(f"{pad}int {e_var} = ({end});")
|
|
765
|
+
lines.append(f"{pad}int {step_var} = ({step});")
|
|
766
|
+
lines.append(f"{pad}if ({step_var} < 0) {step_var} = -{step_var};")
|
|
767
|
+
lines.append(f"{pad}if ({step_var} == 0) {step_var} = 1;")
|
|
768
|
+
lines.append(f"{pad}const bool {down_var} = ({s_var} > {e_var});")
|
|
769
|
+
lines.append(
|
|
770
|
+
f"{pad}for (int {var} = {s_var}; "
|
|
771
|
+
f"({down_var} ? ({var} >= {e_var}) : ({var} <= {e_var})); "
|
|
772
|
+
f"{var} += ({down_var} ? -{step_var} : {step_var}), {e_var} = ({end})) {{"
|
|
773
|
+
)
|
|
643
774
|
# Register the loop counter so reads of it inside the body resolve (the
|
|
644
775
|
# unknown-identifier guard in _visit_ident would otherwise flag it).
|
|
645
776
|
saved_loop = self._current_loop_vars
|
|
646
777
|
self._current_loop_vars = set(self._current_loop_vars)
|
|
647
|
-
if var:
|
|
648
|
-
self._current_loop_vars.add(var)
|
|
649
|
-
|
|
650
|
-
|
|
778
|
+
if node.var:
|
|
779
|
+
self._current_loop_vars.add(node.var)
|
|
780
|
+
_blk_saved = self._push_block_var_remap(node)
|
|
781
|
+
try:
|
|
782
|
+
for s in node.body:
|
|
783
|
+
self._visit_stmt(s, lines, indent + 1)
|
|
784
|
+
finally:
|
|
785
|
+
self._pop_block_var_remap(_blk_saved)
|
|
651
786
|
self._current_loop_vars = saved_loop
|
|
652
787
|
lines.append(f"{pad}}}")
|
|
653
788
|
|
|
789
|
+
def _loop_elem_is_writeback_udt(self, iterable) -> bool:
|
|
790
|
+
"""Whether a ``for x in coll`` loop variable must bind by reference.
|
|
791
|
+
|
|
792
|
+
In Pine a ``for x in arr`` loop variable over an array of *user-defined
|
|
793
|
+
objects* is a reference to the element — field writes (``x.f := v``)
|
|
794
|
+
mutate the array in place — whereas over a primitive array it is a
|
|
795
|
+
copy. So emit C++ ``auto&`` only for arrays whose element is a
|
|
796
|
+
user-defined UDT struct. Primitive elements keep ``auto`` (Pine copy
|
|
797
|
+
semantics: writing the loop var must NOT write back). Drawing handles
|
|
798
|
+
(line/box/label/linefill/...) also keep ``auto``: their element type
|
|
799
|
+
name is a builtin, not in ``_udt_defs``, and a handle copy already
|
|
800
|
+
mutates the shared engine object. (Reassigning the loop var itself —
|
|
801
|
+
``x := ...`` — is not modelled by either form, but Pine forbids it for
|
|
802
|
+
objects in practice and it does not occur in the corpus.)
|
|
803
|
+
"""
|
|
804
|
+
spec = self._type_spec_from_expr(iterable)
|
|
805
|
+
return (
|
|
806
|
+
spec is not None
|
|
807
|
+
and spec.kind == "array"
|
|
808
|
+
and spec.element is not None
|
|
809
|
+
and spec.element.kind == "udt"
|
|
810
|
+
and spec.element.name in self._udt_defs
|
|
811
|
+
)
|
|
812
|
+
|
|
654
813
|
def _visit_for_in(self, node, lines: list[str], indent: int) -> None:
|
|
655
814
|
pad = " " * indent
|
|
656
815
|
iterable = self._visit_expr(node.iterable)
|
|
657
816
|
saved_loop = self._current_loop_vars
|
|
817
|
+
saved_loop_specs = self._current_loop_var_specs
|
|
658
818
|
self._current_loop_vars = set(self._current_loop_vars)
|
|
819
|
+
self._current_loop_var_specs = dict(self._current_loop_var_specs)
|
|
820
|
+
iterable_spec = self._type_spec_from_expr(node.iterable)
|
|
821
|
+
elem_spec = (
|
|
822
|
+
iterable_spec.element
|
|
823
|
+
if iterable_spec is not None and iterable_spec.kind == "array"
|
|
824
|
+
else None
|
|
825
|
+
)
|
|
659
826
|
if node.var:
|
|
660
827
|
self._current_loop_vars.add(node.var)
|
|
828
|
+
if elem_spec is not None:
|
|
829
|
+
self._current_loop_var_specs[node.var] = elem_spec
|
|
661
830
|
if node.vars:
|
|
662
831
|
for v in node.vars:
|
|
663
832
|
if v != "_":
|
|
664
833
|
self._current_loop_vars.add(v)
|
|
665
834
|
if node.var:
|
|
666
835
|
v_cpp = self._safe_name(node.var)
|
|
667
|
-
|
|
836
|
+
ref = "&" if self._loop_elem_is_writeback_udt(node.iterable) else ""
|
|
837
|
+
lines.append(f"{pad}for (auto{ref} {v_cpp} : {iterable}) {{")
|
|
668
838
|
elif node.vars:
|
|
669
839
|
bindings = ", ".join(node.vars)
|
|
670
840
|
lines.append(f"{pad}for (auto [{bindings}] : {iterable}) {{")
|
|
671
|
-
|
|
672
|
-
|
|
841
|
+
_blk_saved = self._push_block_var_remap(node)
|
|
842
|
+
try:
|
|
843
|
+
for s in node.body:
|
|
844
|
+
self._visit_stmt(s, lines, indent + 1)
|
|
845
|
+
finally:
|
|
846
|
+
self._pop_block_var_remap(_blk_saved)
|
|
673
847
|
lines.append(f"{pad}}}")
|
|
674
848
|
self._current_loop_vars = saved_loop
|
|
849
|
+
self._current_loop_var_specs = saved_loop_specs
|
|
675
850
|
|
|
676
851
|
def _visit_while(self, node: WhileStmt, lines: list[str], indent: int) -> None:
|
|
677
852
|
pad = " " * indent
|
|
678
853
|
cond = self._visit_expr(node.condition)
|
|
679
854
|
lines.append(f"{pad}while ({cond}) {{")
|
|
680
|
-
|
|
681
|
-
|
|
855
|
+
_blk_saved = self._push_block_var_remap(node)
|
|
856
|
+
try:
|
|
857
|
+
for s in node.body:
|
|
858
|
+
self._visit_stmt(s, lines, indent + 1)
|
|
859
|
+
finally:
|
|
860
|
+
self._pop_block_var_remap(_blk_saved)
|
|
682
861
|
lines.append(f"{pad}}}")
|
|
683
862
|
|
|
684
863
|
def _visit_switch(self, node: SwitchStmt, lines: list[str], indent: int) -> None:
|
|
@@ -727,6 +906,12 @@ class StmtVisitor:
|
|
|
727
906
|
# Check if it's a skip expr
|
|
728
907
|
if self._is_skip_expr(stmt.expr):
|
|
729
908
|
return
|
|
909
|
+
# A void drawing setter / delete / visual-noop cannot be the
|
|
910
|
+
# branch's value (it lowers to a void C++ call) — emit it as
|
|
911
|
+
# a statement and leave ``target`` at its default.
|
|
912
|
+
if self._call_is_void(stmt.expr):
|
|
913
|
+
self._visit_stmt(stmt, lines, indent)
|
|
914
|
+
return
|
|
730
915
|
cpp = self._visit_expr(stmt.expr)
|
|
731
916
|
pad = " " * indent
|
|
732
917
|
lines.append(f"{pad}{target} = {cpp};")
|