diff --git a/pineforge_codegen/codegen/base.py b/pineforge_codegen/codegen/base.py index e3eed5f..272aca0 100644 --- a/pineforge_codegen/codegen/base.py +++ b/pineforge_codegen/codegen/base.py @@ -1914,7 +1914,10 @@ def _register_global_aggregate_member_types(self) -> None: self._collection_types[name] = spec elif ns == "array" and fn in ({"new", "from"} | set(ARRAY_NEW_CTORS)): self._array_vars.add(name) - spec = self._type_spec_from_expr(expr) or self._array_spec_for_name(name) + spec = self._widen_array_spec_for_name( + name, + self._type_spec_from_expr(expr) or self._array_spec_for_name(name), + ) self._collection_types[name] = spec elif ns == "map" and fn == "new": self._map_vars.add(name) @@ -1934,7 +1937,10 @@ def _register_global_aggregate_member_types(self) -> None: fn2, ns2 = self._resolve_callee(expr.callee) if ns2 == "array" and fn2 in ({"new", "from"} | set(ARRAY_NEW_CTORS)): self._array_vars.add(name) - spec2 = self._type_spec_from_expr(expr) or self._array_spec_for_name(name) + spec2 = self._widen_array_spec_for_name( + name, + self._type_spec_from_expr(expr) or self._array_spec_for_name(name), + ) self._collection_types[name] = spec2 continue if name in self._matrix_specs: @@ -4275,7 +4281,9 @@ def generate(self) -> str: ) ): self._array_vars.add(name) - array_spec = exact_member_spec or self._array_spec_for_name(name) + array_spec = self._widen_array_spec_for_name( + name, exact_member_spec or self._array_spec_for_name(name) + ) lines.append( f" {self._type_spec_to_cpp(array_spec)} {safe};" ) diff --git a/pineforge_codegen/codegen/emit_top.py b/pineforge_codegen/codegen/emit_top.py index 61e03fd..58be635 100644 --- a/pineforge_codegen/codegen/emit_top.py +++ b/pineforge_codegen/codegen/emit_top.py @@ -1035,6 +1035,11 @@ def _emit_on_bar(self, lines: list[str]) -> None: # declaration statements, not in the global on_bar preamble. if name in getattr(self, "_func_local_var_names", ()): continue + # A wide int array member (``std::vector``, see + # ``_wide_int_array_names``) needs its first-bar constructor + # spelled with the same element type: name the target for + # visit_call's ``array.new_*`` / ``array.from`` lowering. + self._array_ctor_target_name = name safe = self._safe_name(name) runtime_info = self._runtime_scalar_var_init_by_member.get(name) if (runtime_info is not None @@ -1131,6 +1136,7 @@ def _emit_on_bar(self, lines: list[str]) -> None: if cloned not in init_emitted: init_emitted.add(cloned) lines.append(f" {cloned}.push({cpp_val});") + self._array_ctor_target_name = None lines.append(" _var_initialized = true;") lines.append(" } else {") for name, _, _ in self.ctx.var_members: diff --git a/pineforge_codegen/codegen/types.py b/pineforge_codegen/codegen/types.py index ed42a52..89bc53a 100644 --- a/pineforge_codegen/codegen/types.py +++ b/pineforge_codegen/codegen/types.py @@ -135,8 +135,9 @@ def _type_spec_to_cpp(self, spec: TypeSpec | None) -> str: if spec is None: return "double" if spec.kind == "primitive": - return {"float": "double", "int": "int", "bool": "bool", - "string": "std::string", "color": "int"}.get(spec.name or "float", "double") + return {"float": "double", "int": "int", "int64": "int64_t", + "bool": "bool", "string": "std::string", + "color": "int"}.get(spec.name or "float", "double") if spec.kind == "udt" and spec.name: # Drawing handle structs (P1): map BEFORE the _udt_defs check so # array -> std::vector and scalar line -> Line instead @@ -269,6 +270,175 @@ def _default_for_spec(self, spec: TypeSpec | None) -> str: return self._default_for_type(cpp_type) def _collection_spec_for_name(self, name: str) -> TypeSpec | None: + """``_collection_spec_for_name_raw`` with the wide-int element applied + (``_widen_array_spec_for_name``): an ``array`` that stores epoch + milliseconds resolves as ``std::vector`` everywhere it is + read, declared or passed.""" + return self._widen_array_spec_for_name( + name, self._collection_spec_for_name_raw(name) + ) + + # Element-returning array reads whose result carries the receiver's wide + # provenance (``_expr_returns_wide_int``). + _WIDE_ARRAY_ELEMENT_READS = frozenset({ + "get", "first", "last", "pop", "shift", "remove", "max", "min", + }) + # Writers whose VALUE argument decides whether an int array is wide: + # name -> (functional-form value index, method-form value index). + _ARRAY_ELEMENT_WRITERS = { + "push": (1, 0), "unshift": (1, 0), "set": (2, 1), "insert": (2, 1), + "fill": (1, 0), + } + + @staticmethod + def _int_fits_int32(value: int) -> bool: + return -(1 << 31) <= value < (1 << 31) + + def _pure_int_literal_value(self, node) -> int | None: + """Exact value of an expression built only from int literals and + ``+ - *`` / unary minus, else ``None``. + + Pine ``int`` is 64-bit: ``90 * 24 * 60 * 60 * 1000`` (three months in + milliseconds) is 7 776 000 000 on TradingView. Emitted as C++ ``int`` + literal arithmetic the same product overflows (wraps to -813 934 592), + so a ``(time - t0) > threeMonths`` expiry fires on every bar (round 8 + family U: latibonit15 execution-signals-confluence, six lanes; lab tv + u-lati-levels-nq15 vs the engine, 2026-09-05). Folding the literal + subtree in Python keeps the exact value; the caller emits it as a + 64-bit literal only when it does not fit ``int32`` so every in-range + expression is byte-identical to before. + """ + if isinstance(node, NumberLiteral): + return node.value if isinstance(node.value, int) and not isinstance(node.value, bool) else None + if isinstance(node, UnaryOp) and node.op == "-": + inner = self._pure_int_literal_value(node.operand) + return -inner if inner is not None else None + if isinstance(node, BinOp) and node.op in ("+", "-", "*"): + left = self._pure_int_literal_value(node.left) + if left is None: + return None + right = self._pure_int_literal_value(node.right) + if right is None: + return None + if node.op == "+": + return left + right + if node.op == "-": + return left - right + return left * right + return None + + def _literal_overflows_int32(self, node) -> bool: + value = self._pure_int_literal_value(node) + return value is not None and not self._int_fits_int32(value) + + def _array_receiver_and_value(self, call): + """``(receiver_name, value_node)`` of an element-writing array call in + either form (``array.push(a, v)`` / ``a.push(v)``), else ``None``.""" + if not isinstance(call, FuncCall): + return None + func_name, namespace = self._resolve_callee(call.callee) + if func_name not in self._ARRAY_ELEMENT_WRITERS: + return None + functional_idx, method_idx = self._ARRAY_ELEMENT_WRITERS[func_name] + if namespace == "array": + receiver = call.args[0] if call.args else call.kwargs.get("id") + value = (call.args[functional_idx] + if len(call.args) > functional_idx + else call.kwargs.get("value")) + elif (isinstance(call.callee, MemberAccess) + and isinstance(call.callee.object, Identifier)): + receiver = call.callee.object + value = (call.args[method_idx] + if len(call.args) > method_idx + else call.kwargs.get("value")) + else: + return None + if not isinstance(receiver, Identifier) or value is None: + return None + return receiver.name, value + + def _wide_int_array_names(self) -> set[str]: + """Names of int arrays that hold a wide integer: one that receives an + epoch-millisecond value (``time``, ``time_close``, ``timestamp(...)``, + a wide callable result, ...) through ``array.push`` / ``unshift`` / + ``set`` / ``insert`` / ``fill``, or is built by ``array.new_int`` / + ``array.new`` / ``array.from`` from one. Their element type is + ``int64_t`` (TypeSpec primitive ``int64``): Pine's ``int`` holds the + epoch, ``std::vector`` truncates it. Cached on the instance.""" + cached = getattr(self, "_wide_int_array_cache", None) + if cached is not None: + return cached + # The scan below asks ``_expr_returns_wide_int``, whose receiver typing + # resolves collection specs through ``_collection_spec_for_name`` and so + # back here: publish the (growing) set first so the recursion reads the + # partial answer instead of re-entering, then iterate to the fixpoint so + # an array filled from another wide array's elements widens too. + names: set[str] = set() + self._wide_int_array_cache = names + + def scan(nodes, owner_info): + for child in nodes: + if isinstance(child, FuncCall): + pair = self._array_receiver_and_value(child) + if pair is not None and self._expr_returns_wide_int( + pair[1], owner_info, set(), None + ): + names.add(pair[0]) + continue + target = None + value = None + if isinstance(child, VarDecl): + target, value = child.name, child.value + elif (isinstance(child, Assignment) + and isinstance(child.target, Identifier)): + target, value = child.target.name, child.value + if target is None or not isinstance(value, FuncCall): + continue + fn, ns = self._resolve_callee(value.callee) + if ns != "array": + continue + candidates = [] + if fn in ("new_int", "new"): + if fn == "new": + targs = (self._template_args_from_call(value) + if hasattr(value, "annotations") else []) + if not targs or targs[0] != "int": + continue + if len(value.args) > 1: + candidates.append(value.args[1]) + if "initial_value" in value.kwargs: + candidates.append(value.kwargs["initial_value"]) + elif fn == "from": + candidates = list(value.args) + if any(self._expr_returns_wide_int(c, owner_info, set(), None) + for c in candidates): + names.add(target) + + ast = getattr(self.ctx, "ast", None) + for _round in range(8): + before = len(names) + if ast is not None: + scan(self._walk_ast(ast), None) + for info in getattr(self.ctx, "func_infos", ()): + node = getattr(info, "node", None) + if node is not None: + scan(self._walk_ast_list(node.body), info) + if len(names) == before: + break + return names + + def _widen_array_spec_for_name(self, name, spec): + """``array`` -> ``array`` when ``name`` is a wide int + array; every other spec (and ``None``) passes through unchanged.""" + if (spec is None or spec.kind != "array" or spec.element is None + or spec.element.kind != "primitive" + or spec.element.name != "int"): + return spec + if name in self._wide_int_array_names(): + return TypeSpec.array(TypeSpec.primitive("int64")) + return spec + + def _collection_spec_for_name_raw(self, name: str) -> TypeSpec | None: """Resolve collection metadata with lexical precedence. Source-ordered callable locals shadow loop bindings and parameters once @@ -417,7 +587,9 @@ def _array_from_element_spec(self, node) -> TypeSpec | None: return TypeSpec.primitive("string") if node.op == "/" or left.name == "float" or right.name == "float": return TypeSpec.primitive("float") - if left.name == "int" and right.name == "int": + if left.name in ("int", "int64") and right.name in ("int", "int64"): + if "int64" in (left.name, right.name): + return TypeSpec.primitive("int64") return TypeSpec.primitive("int") return None spec = self._type_spec_from_expr(node) @@ -976,7 +1148,7 @@ def _array_method_expr( "percentrank", "abs", "standardize", "covariance", "binary_search", "binary_search_leftmost", "binary_search_rightmost", "sort_indices", } - if method in numeric_only and elem_cpp not in ("double", "int"): + if method in numeric_only and elem_cpp not in ("double", "int", "int64_t"): self._codegen_error( None, f"array.{method} requires a numeric array", @@ -1187,7 +1359,7 @@ def _series_param_element_cpp_type( # truly untyped slot may consult the per-callsite specialization map. spec = declared_spec if spec is not None and spec.kind == "primitive": - if spec.name == "int": + if spec.name in ("int", "int64"): return "int64_t" if spec.name == "bool": return "bool" @@ -1217,7 +1389,7 @@ def _series_param_element_cpp_type( specs = list(getattr(func_info, "param_type_specs", ()) or ()) spec = specs[index] if index < len(specs) else None if spec is not None and spec.kind == "primitive": - if spec.name == "int": + if spec.name in ("int", "int64"): return "int64_t" if spec.name == "bool": return "bool" @@ -1352,8 +1524,25 @@ def _expr_returns_wide_int( return self._expr_is_int64_builtin(expr) if self._expr_is_int64_builtin(expr): return True + if isinstance(expr, (BinOp, UnaryOp)) and self._literal_overflows_int32(expr): + # ``90 * 24 * 60 * 60 * 1000``: an int-literal product beyond + # int32 is a 64-bit value in Pine (``_pure_int_literal_value``). + return True if isinstance(expr, FuncCall): func_name, namespace = self._resolve_callee(expr.callee) + if func_name in self._WIDE_ARRAY_ELEMENT_READS: + # An element read off a wide int array (``array.get(times, i)`` + # / ``times.get(i)``) carries the epoch: the destination must + # not narrow it (``_wide_int_array_names``). + receiver = None + if namespace == "array": + receiver = expr.args[0] if expr.args else expr.kwargs.get("id") + elif (isinstance(expr.callee, MemberAccess) + and isinstance(expr.callee.object, Identifier)): + receiver = expr.callee.object + if (isinstance(receiver, Identifier) + and receiver.name in self._wide_int_array_names()): + return True if namespace is None and func_name in { "int", "float", diff --git a/pineforge_codegen/codegen/visit_call.py b/pineforge_codegen/codegen/visit_call.py index 8358994..34e0063 100644 --- a/pineforge_codegen/codegen/visit_call.py +++ b/pineforge_codegen/codegen/visit_call.py @@ -1555,6 +1555,12 @@ def _visit_func_call(self, node: FuncCall) -> str: if namespace == "array": if func_name in ("new", "new_float", "new_int", "new_bool", "new_string") or func_name in ARRAY_DRAWING_NEW_CTORS: spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float")) + # The constructor of a wide int array (``_wide_int_array_names``) + # must spell the declared ``std::vector``: the + # declaration site names its target here. + target = getattr(self, "_array_ctor_target_name", None) + if target is not None: + spec = self._widen_array_spec_for_name(target, spec) cpp_type = self._type_spec_to_cpp(spec) elem_spec = spec.element if spec.element is not None else TypeSpec.primitive("float") init_default = self._default_for_spec(elem_spec) @@ -1568,6 +1574,9 @@ def _visit_func_call(self, node: FuncCall) -> str: return f"{cpp_type}()" if func_name == "from": spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float")) + target = getattr(self, "_array_ctor_target_name", None) + if target is not None: + spec = self._widen_array_spec_for_name(target, spec) elems = ", ".join(self._visit_expr(a) for a in node.args) return f"{self._type_spec_to_cpp(spec)}{{{elems}}}" # Method calls: array.method(arr, args...) diff --git a/pineforge_codegen/codegen/visit_expr.py b/pineforge_codegen/codegen/visit_expr.py index c1a2ccb..ea8eff7 100644 --- a/pineforge_codegen/codegen/visit_expr.py +++ b/pineforge_codegen/codegen/visit_expr.py @@ -1051,6 +1051,13 @@ def _lower_relational(self, op: str, left_node, right_node, return f"({left_cpp} {op} {right_cpp})" def _visit_binop(self, node: BinOp) -> str: + # An int-literal-only ``+ - *`` tree whose exact value leaves int32 is + # a 64-bit Pine int (``90 * 24 * 60 * 60 * 1000`` = 7 776 000 000); + # C++ ``int`` literal arithmetic would wrap it. Fold it here and spell + # the value as a 64-bit literal. In-range trees are emitted as before. + folded = self._pure_int_literal_value(node) + if folded is not None and not self._int_fits_int32(folded): + return f"static_cast({folded}LL)" left = self._visit_expr(node.left) right = self._visit_expr(node.right) cpp_ops = {"and": "&&", "or": "||"} diff --git a/pineforge_codegen/codegen/visit_stmt.py b/pineforge_codegen/codegen/visit_stmt.py index aa6b78b..c006d9f 100644 --- a/pineforge_codegen/codegen/visit_stmt.py +++ b/pineforge_codegen/codegen/visit_stmt.py @@ -495,6 +495,11 @@ def _visit_var_decl(self, node: VarDecl, lines: list[str], pad: str) -> None: ) previous_input_name = self._current_input_var_name self._current_input_var_name = node.name + # A ``var`` int array that stores epoch milliseconds is declared + # ``std::vector``; its one-shot constructor here must + # spell the same type (visit_call array.new_* / array.from). + previous_ctor_target = getattr(self, "_array_ctor_target_name", None) + self._array_ctor_target_name = node.name try: type_spec = info.get("type_spec") target_cpp_type = info.get("drawing_cpp") @@ -535,6 +540,7 @@ def _visit_var_decl(self, node: VarDecl, lines: list[str], pad: str) -> None: init_cpp = self._visit_expr(node.value) finally: self._current_input_var_name = previous_input_name + self._array_ctor_target_name = previous_ctor_target if info.get("drawing_cpp") is None: init_cpp = self._typed_na_init( init_cpp, member_name, info["ptype"] @@ -618,13 +624,19 @@ def remember_local_type(cpp_type: str | None) -> None: func_name, namespace = self._resolve_callee(node.value.callee) if namespace == "array" and func_name in ARRAY_NEW_CTORS | {"new", "from", "copy", "slice"}: captured = self._callable_collection_bindings.get(id(node)) - spec = ( + spec = self._widen_array_spec_for_name( + node.name, captured if captured is not None and captured.kind == "array" else self._type_spec_from_expr(node.value) - or self._array_spec_for_name(node.name) + or self._array_spec_for_name(node.name), ) - init = self._visit_expr(node.value) + previous_target = getattr(self, "_array_ctor_target_name", None) + self._array_ctor_target_name = node.name + try: + init = self._visit_expr(node.value) + finally: + self._array_ctor_target_name = previous_target self._array_vars.add(node.name) self._collection_types.setdefault(node.name, spec) cpp_type = self._type_spec_to_cpp(spec) diff --git a/pineforge_codegen/symbols.py b/pineforge_codegen/symbols.py index a6b0cba..798da9c 100644 --- a/pineforge_codegen/symbols.py +++ b/pineforge_codegen/symbols.py @@ -75,7 +75,9 @@ def method_receiver_type_name(spec: TypeSpec | None) -> str | None: if spec is None: return None if spec.kind in {"primitive", "udt"}: - return spec.name + # A wide (epoch-millisecond) element is still Pine ``int`` to the + # source: a user method declared on ``array`` binds to it. + return "int" if spec.name == "int64" else spec.name if spec.kind == "array" and spec.element is not None: element = method_receiver_type_name(spec.element) return f"array<{element}>" if element is not None else None @@ -106,7 +108,7 @@ def method_receiver_cpp_token( if spec is not None: if spec.kind in {"primitive", "udt"} and spec.name: - return spec.name + return "int" if spec.name == "int64" else spec.name if spec.kind == "array" and spec.element is not None: return f"array_{method_receiver_cpp_token(spec.element)}" if spec.kind == "map" and spec.key is not None and spec.value is not None: diff --git a/tests/test_wide_int_arrays_and_literals.py b/tests/test_wide_int_arrays_and_literals.py new file mode 100644 index 0000000..533d9fd --- /dev/null +++ b/tests/test_wide_int_arrays_and_literals.py @@ -0,0 +1,84 @@ +"""Pine ``int`` is 64-bit: epoch-millisecond arrays and int-literal products. + +Round 8 family U (latibonit15 execution-signals-confluence, six lanes; lab tv +u-lati-levels-nq15 vs the engine, 2026-09-05): the script keeps its level +creation times in ``array.new_int()`` and expires a level when +``(time - existingTime) > 90 * 24 * 60 * 60 * 1000``. Emitted as +``std::vector`` the stamp truncated and the literal product wrapped to +-813 934 592 in C++ ``int`` arithmetic, so every nearby level "expired" one +day after creation where TradingView blocks the duplicate for three months. +""" +from pineforge_codegen import transpile + + +def _gen(body: str) -> str: + return transpile(f'//@version=6\nstrategy("T")\n{body}\n') + + +LEVEL_MACHINE = ''' +var float[] levelPrices = array.new_float() +var int[] levelTimes = array.new_int() +if close > open + threeMonths = 90 * 24 * 60 * 60 * 1000 + if array.size(levelPrices) > 0 + for i = array.size(levelPrices) - 1 to 0 + existingTime = array.get(levelTimes, i) + if (time - existingTime) > threeMonths + array.remove(levelPrices, i) + array.remove(levelTimes, i) + array.push(levelPrices, open) + array.push(levelTimes, time) +if array.size(levelPrices) > 3 + strategy.entry("L", strategy.long) +''' + + +def test_int_array_holding_time_is_int64(): + cpp = _gen(LEVEL_MACHINE) + assert "std::vector levelTimes;" in cpp + # Every constructor of the member spells the wide element type too. + assert "std::vector levelTimes" not in cpp + assert "levelTimes = std::vector()" not in cpp + assert "levelTimes = std::vector()" in cpp + # The element read keeps the epoch: the local is not narrowed. + assert "int64_t existingTime = " in cpp + assert " int existingTime = " not in cpp + # A float array is untouched. + assert "std::vector levelPrices;" in cpp + + +def test_int_literal_product_beyond_int32_is_folded_to_a_64_bit_literal(): + cpp = _gen(LEVEL_MACHINE) + assert "static_cast(7776000000LL)" in cpp + assert "((((90 * 24) * 60) * 60) * 1000)" not in cpp + + +def test_in_range_int_literal_arithmetic_is_unchanged(): + cpp = _gen("oneDay = 24 * 60 * 60 * 1000\nif close > oneDay\n strategy.entry(\"L\", strategy.long)\n") + assert "((24 * 60) * 60) * 1000" in cpp + assert "static_cast(" not in cpp.split("oneDay = ")[1].split("\n")[0] + + +def test_method_form_push_of_time_widens_the_array(): + cpp = _gen(''' +var times = array.new() +times.push(time) +if times.size() > 0 and time - times.get(0) > 30 * 24 * 60 * 60 * 1000 + strategy.entry("L", strategy.long) +''') + assert "std::vector times;" in cpp + assert "times = std::vector()" in cpp + assert "static_cast(2592000000LL)" in cpp + + +def test_int_array_without_epoch_values_stays_narrow(): + cpp = _gen(''' +var int[] counts = array.new_int() +array.push(counts, 3) +c = array.get(counts, 0) +if c > 1 + strategy.entry("L", strategy.long) +''') + assert "std::vector counts;" in cpp + assert "counts = std::vector()" in cpp + assert "int64_t" not in cpp.split("std::vector counts;")[1].split("counts = std::vector()")[0]