@pineforge/codegen-pyodide 0.9.0 → 0.10.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/__init__.py +3 -0
- package/pineforge_codegen/analyzer/base.py +3999 -140
- package/pineforge_codegen/analyzer/call_handlers.py +662 -48
- package/pineforge_codegen/analyzer/contracts.py +104 -7
- package/pineforge_codegen/analyzer/types.py +951 -14
- package/pineforge_codegen/codegen/base.py +2359 -249
- package/pineforge_codegen/codegen/drawing.py +94 -14
- package/pineforge_codegen/codegen/emit_top.py +732 -172
- package/pineforge_codegen/codegen/security.py +1456 -150
- package/pineforge_codegen/codegen/ta.py +435 -99
- package/pineforge_codegen/codegen/tables.py +224 -77
- package/pineforge_codegen/codegen/types.py +1196 -215
- package/pineforge_codegen/codegen/visit_call.py +1168 -99
- package/pineforge_codegen/codegen/visit_expr.py +354 -33
- package/pineforge_codegen/codegen/visit_stmt.py +1144 -200
- package/pineforge_codegen/finite_ta_length.py +798 -0
- package/pineforge_codegen/method_binding.py +163 -0
- package/pineforge_codegen/parser.py +48 -9
- package/pineforge_codegen/signatures.py +4 -4
- package/pineforge_codegen/support_checker.py +194 -19
- package/pineforge_codegen/symbols.py +65 -0
- package/pineforge_codegen-0.10.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +9 -11
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
|
@@ -166,6 +166,21 @@ _BUILTIN_NAMESPACE_NAMES: frozenset[str] = frozenset({
|
|
|
166
166
|
})
|
|
167
167
|
|
|
168
168
|
|
|
169
|
+
# KI-71/KI-73: Pine relational comparisons (``==`` ``!=`` ``<`` ``>``
|
|
170
|
+
# ``<=`` ``>=``) with an ``na`` operand evaluate *falsy*. Pine also treats
|
|
171
|
+
# finite float operands at most 1e-10 apart as equal, independent of their
|
|
172
|
+
# magnitude. Naive C++ relationals honour neither rule completely, so numeric
|
|
173
|
+
# comparisons route through the shared lowering below.
|
|
174
|
+
_RELATIONAL_OPS: frozenset[str] = frozenset({"==", "!=", "<", ">", "<=", ">="})
|
|
175
|
+
|
|
176
|
+
# C++ scalar types carrying a detectable ``na`` sentinel via ``is_na``:
|
|
177
|
+
# ``double`` -> NaN (IEEE), ``int``/``int64_t`` -> ``numeric_limits<T>::min()``.
|
|
178
|
+
# Only these route through the na-aware relational lowering — ``is_na`` has no
|
|
179
|
+
# overload for ``bool``/``std::string``/vector/UDT-value operands, and Pine's
|
|
180
|
+
# na-bool is engine-indistinguishable from ``false`` (na<bool>() == false).
|
|
181
|
+
_NA_SCALAR_CPP: frozenset[str] = frozenset({"int", "int64_t", "double"})
|
|
182
|
+
|
|
183
|
+
|
|
169
184
|
class ExprVisitor:
|
|
170
185
|
"""Expression-level visitor methods shared across the codegen.
|
|
171
186
|
|
|
@@ -207,7 +222,21 @@ class ExprVisitor:
|
|
|
207
222
|
if isinstance(node, Subscript):
|
|
208
223
|
return self._visit_subscript(node)
|
|
209
224
|
if isinstance(node, TupleLiteral):
|
|
210
|
-
elems =
|
|
225
|
+
elems = []
|
|
226
|
+
for element in node.elements:
|
|
227
|
+
element_spec = self._type_spec_from_expr(element)
|
|
228
|
+
element_target = None
|
|
229
|
+
if (element_spec is not None
|
|
230
|
+
and element_spec.kind == "udt"
|
|
231
|
+
and element_spec.name in DRAWING_TYPE_TO_CPP):
|
|
232
|
+
element_target = self._type_spec_to_cpp(element_spec)
|
|
233
|
+
elems.append(
|
|
234
|
+
self._visit_rhs_value(
|
|
235
|
+
element,
|
|
236
|
+
target_cpp_type=element_target,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
elems = ", ".join(elems)
|
|
211
240
|
return f"std::make_tuple({elems})"
|
|
212
241
|
return "/* unknown */"
|
|
213
242
|
|
|
@@ -219,34 +248,132 @@ class ExprVisitor:
|
|
|
219
248
|
return (isinstance(node, NaLiteral)
|
|
220
249
|
or (isinstance(node, Identifier) and node.name == "na"))
|
|
221
250
|
|
|
222
|
-
def
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
251
|
+
def _drawing_target_cpp_type(
|
|
252
|
+
self,
|
|
253
|
+
target_name: str | None,
|
|
254
|
+
target_cpp_type: str | None,
|
|
255
|
+
) -> str | None:
|
|
256
|
+
"""Resolve a drawing target without leaking a shadowed global type.
|
|
257
|
+
|
|
258
|
+
An explicit contextual type always wins. For reassignments, where the
|
|
259
|
+
caller may not have one, consult function-local and parameter types
|
|
260
|
+
before the analyzer's flat global drawing registry. A same-named
|
|
261
|
+
scalar local/parameter must therefore block the global fallback.
|
|
262
|
+
"""
|
|
263
|
+
if target_cpp_type is not None:
|
|
264
|
+
return (
|
|
265
|
+
target_cpp_type
|
|
266
|
+
if target_cpp_type in DRAWING_TYPE_TO_CPP.values()
|
|
267
|
+
else None
|
|
268
|
+
)
|
|
269
|
+
if not target_name:
|
|
270
|
+
return None
|
|
271
|
+
if target_name in self._lexical_drawing_types:
|
|
272
|
+
return self._lexical_drawing_types[target_name]
|
|
273
|
+
param_spec = getattr(self, "_current_func_param_specs", {}).get(
|
|
274
|
+
target_name
|
|
275
|
+
)
|
|
276
|
+
if (param_spec is not None
|
|
277
|
+
and param_spec.kind == "udt"
|
|
278
|
+
and param_spec.name in DRAWING_TYPE_TO_CPP):
|
|
279
|
+
return DRAWING_TYPE_TO_CPP[param_spec.name]
|
|
280
|
+
for scoped_types in (
|
|
281
|
+
getattr(self, "_current_func_local_types", {}),
|
|
282
|
+
getattr(self, "_current_func_param_types", {}),
|
|
283
|
+
):
|
|
284
|
+
if target_name in scoped_types:
|
|
285
|
+
scoped_type = scoped_types[target_name].removesuffix("&")
|
|
286
|
+
return (
|
|
287
|
+
scoped_type
|
|
288
|
+
if scoped_type in DRAWING_TYPE_TO_CPP.values()
|
|
289
|
+
else None
|
|
290
|
+
)
|
|
291
|
+
# Only declarations already emitted into the current lexical path may
|
|
292
|
+
# shadow a global. Scanning the whole future function body makes a
|
|
293
|
+
# later local declaration pre-shadow earlier statements. Exact
|
|
294
|
+
# top-level types are captured separately because the analyzer's flat
|
|
295
|
+
# raw-name registry can be overwritten by a same-named local.
|
|
296
|
+
return self._global_drawing_cpp_types.get(target_name)
|
|
297
|
+
|
|
298
|
+
def _udt_target_cpp_type(
|
|
299
|
+
self,
|
|
300
|
+
*,
|
|
301
|
+
target_name: str | None = None,
|
|
302
|
+
target_node=None,
|
|
303
|
+
type_hint: str | None = None,
|
|
304
|
+
) -> str | None:
|
|
305
|
+
"""Return the exact authored UDT type for a contextual RHS target.
|
|
306
|
+
|
|
307
|
+
Pine's bare ``na`` is target typed. Arbitrary UDTs represent their na
|
|
308
|
+
ID with a default-constructed ``T{}``, just as generated drawing
|
|
309
|
+
handles do, but the drawing-only registry cannot safely answer for
|
|
310
|
+
same-spelled ordinary UDT declarations in sibling blocks/functions.
|
|
311
|
+
Prefer explicit/node context, then the source-ordered lexical map, and
|
|
312
|
+
consult legacy raw-name metadata only when no lexical binding exists.
|
|
313
|
+
"""
|
|
314
|
+
spec = self._type_spec_from_hint_name(type_hint) if type_hint else None
|
|
315
|
+
if spec is None and target_node is not None:
|
|
316
|
+
spec = self._type_spec_from_expr(target_node)
|
|
317
|
+
if spec is not None and spec.kind == "udt" and spec.name in self._udt_defs:
|
|
318
|
+
return spec.name
|
|
226
319
|
if not target_name:
|
|
227
320
|
return None
|
|
228
|
-
|
|
229
|
-
if
|
|
230
|
-
return f"{DRAWING_TYPE_TO_CPP[udt]}{{}}"
|
|
231
|
-
return None
|
|
321
|
+
udt_name = self._identifier_udt_type(target_name)
|
|
322
|
+
return udt_name if udt_name in self._udt_defs else None
|
|
232
323
|
|
|
233
324
|
def _visit_rhs_value(self, value_node, target_name: str | None = None,
|
|
234
325
|
target_cpp_type: str | None = None) -> str:
|
|
235
326
|
"""Visit an assignment / declaration RHS.
|
|
236
327
|
|
|
237
328
|
A bare ``na`` lowers to a type-appropriate initializer for the target
|
|
238
|
-
instead of ``na<double>()``: drawing
|
|
239
|
-
|
|
240
|
-
``bool`` use ``na<T>()``. Without
|
|
241
|
-
``
|
|
242
|
-
(
|
|
329
|
+
instead of ``na<double>()``: drawing and authored UDT handles
|
|
330
|
+
brace-init to their na object (``Box{}`` / ``State{}``), while
|
|
331
|
+
``std::string``/``int``/``int64_t``/``bool`` use ``na<T>()``. Without
|
|
332
|
+
this, ``State s = na;`` and ``string x = na;`` would both emit
|
|
333
|
+
``na<double>()`` and fail to compile (no viable assignment/conversion).
|
|
334
|
+
Every other RHS lowers unchanged.
|
|
243
335
|
"""
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
336
|
+
drawing_target = self._drawing_target_cpp_type(
|
|
337
|
+
target_name,
|
|
338
|
+
target_cpp_type,
|
|
339
|
+
)
|
|
340
|
+
if self._is_na_expr(value_node):
|
|
341
|
+
if drawing_target is not None:
|
|
342
|
+
return f"{drawing_target}{{}}"
|
|
343
|
+
if target_cpp_type in self._udt_defs:
|
|
344
|
+
return f"{target_cpp_type}{{}}"
|
|
345
|
+
if self._is_nullable_collection_cpp_type(target_cpp_type):
|
|
346
|
+
# Maps and matrices use default construction for a typed
|
|
347
|
+
# ``na`` ID. Their ``*.new`` factories create a valid ID,
|
|
348
|
+
# including a valid empty collection.
|
|
349
|
+
return f"{target_cpp_type}{{}}"
|
|
248
350
|
if target_cpp_type in ("std::string", "int", "int64_t", "bool"):
|
|
249
351
|
return f"na<{target_cpp_type}>()"
|
|
352
|
+
if (isinstance(value_node, Ternary)
|
|
353
|
+
and (self._is_nullable_collection_cpp_type(target_cpp_type)
|
|
354
|
+
or drawing_target is not None
|
|
355
|
+
or target_cpp_type in self._udt_defs)):
|
|
356
|
+
# C++ cannot deduce a common type for ``na<double>()`` and a
|
|
357
|
+
# collection/drawing handle. Pine's ternary is target typed, so
|
|
358
|
+
# propagate the exact declared/reassignment target into both arms.
|
|
359
|
+
# Arrays and scalar ternaries retain the established generic path;
|
|
360
|
+
# target typing is safe only for collections with nullable runtime
|
|
361
|
+
# representations.
|
|
362
|
+
branch_target = drawing_target or target_cpp_type
|
|
363
|
+
condition = self._visit_expr(value_node.condition)
|
|
364
|
+
true_value = self._visit_rhs_value(
|
|
365
|
+
value_node.true_val,
|
|
366
|
+
target_name,
|
|
367
|
+
target_cpp_type=branch_target,
|
|
368
|
+
)
|
|
369
|
+
false_value = self._visit_rhs_value(
|
|
370
|
+
value_node.false_val,
|
|
371
|
+
target_name,
|
|
372
|
+
target_cpp_type=branch_target,
|
|
373
|
+
)
|
|
374
|
+
return (
|
|
375
|
+
f"(({condition}) ? ({true_value}) : ({false_value}))"
|
|
376
|
+
)
|
|
250
377
|
return self._visit_expr(value_node)
|
|
251
378
|
|
|
252
379
|
def _visit_ident(self, node: Identifier) -> str:
|
|
@@ -254,6 +381,9 @@ class ExprVisitor:
|
|
|
254
381
|
# Bare 'na' identifier → na<double>()
|
|
255
382
|
if name == "na":
|
|
256
383
|
return "na<double>()"
|
|
384
|
+
pending_outer = getattr(self, "_pending_decl_outer_alias", {})
|
|
385
|
+
if name in pending_outer:
|
|
386
|
+
return pending_outer[name]
|
|
257
387
|
# Function parameters that are series — read current value
|
|
258
388
|
if name in self._current_func_series_params:
|
|
259
389
|
return f"{self._safe_name(name)}[0]"
|
|
@@ -267,7 +397,9 @@ class ExprVisitor:
|
|
|
267
397
|
# Inline known constants (literals). Input-backed names are NOT inlined
|
|
268
398
|
# because strategy_set_input() can override them at runtime; emit the
|
|
269
399
|
# variable name and let get_input_*() produce the current value.
|
|
270
|
-
if name in self._known_vars
|
|
400
|
+
if (name in self._known_vars
|
|
401
|
+
and name not in self._input_backed_vars
|
|
402
|
+
and not self._known_var_is_lexically_shadowed(name)):
|
|
271
403
|
val = self._known_vars[name]
|
|
272
404
|
if isinstance(val, bool):
|
|
273
405
|
return "true" if val else "false"
|
|
@@ -302,7 +434,7 @@ class ExprVisitor:
|
|
|
302
434
|
# Apply per-call-site var remap (for function-local vars)
|
|
303
435
|
if self._active_var_remap and safe in self._active_var_remap:
|
|
304
436
|
safe = self._active_var_remap[safe]
|
|
305
|
-
if name
|
|
437
|
+
if self._binding_is_series(name, safe):
|
|
306
438
|
return f"{safe}[0]"
|
|
307
439
|
# Safety net: by here the name resolved to none of the builtins,
|
|
308
440
|
# constants, parameters, or declared variables handled above, so
|
|
@@ -363,6 +495,16 @@ class ExprVisitor:
|
|
|
363
495
|
return True
|
|
364
496
|
return False
|
|
365
497
|
|
|
498
|
+
def _visit_mutable_expr(self, node: ASTNode) -> str:
|
|
499
|
+
"""Lower an assignment target with UDT journal capture enabled."""
|
|
500
|
+
|
|
501
|
+
previous = getattr(self, "_udt_mutable_expr_depth", 0)
|
|
502
|
+
self._udt_mutable_expr_depth = previous + 1
|
|
503
|
+
try:
|
|
504
|
+
return self._visit_expr(node)
|
|
505
|
+
finally:
|
|
506
|
+
self._udt_mutable_expr_depth = previous
|
|
507
|
+
|
|
366
508
|
def _visit_member_access(self, node: MemberAccess) -> str:
|
|
367
509
|
# UDT field whose type was a drawing primitive (label/line/box/
|
|
368
510
|
# linefill/polyline/table/chart.point) — the field is dropped from
|
|
@@ -371,6 +513,34 @@ class ExprVisitor:
|
|
|
371
513
|
# See: pineforge-codegen issue #10.
|
|
372
514
|
if self._is_omitted_udt_field(node):
|
|
373
515
|
return "/* drawing field omitted */ 0"
|
|
516
|
+
|
|
517
|
+
# User-defined objects are numeric handles into a per-type arena. The
|
|
518
|
+
# arena lookup returns a real record lvalue, so this one lowering serves
|
|
519
|
+
# reads, assignment targets, nested field chains and method receivers.
|
|
520
|
+
owner_spec = self._type_spec_from_expr(node.object)
|
|
521
|
+
if (
|
|
522
|
+
owner_spec is not None
|
|
523
|
+
and owner_spec.kind == "udt"
|
|
524
|
+
and owner_spec.name in self._udt_defs
|
|
525
|
+
):
|
|
526
|
+
owner = self._visit_expr(node.object)
|
|
527
|
+
arena = self._udt_arena_member_name(owner_spec.name)
|
|
528
|
+
field_spec = (
|
|
529
|
+
self._udt_field_type_specs.get(owner_spec.name, {}).get(
|
|
530
|
+
node.member
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
mutable_collection = (
|
|
534
|
+
field_spec is not None
|
|
535
|
+
and field_spec.kind in {"array", "map", "matrix"}
|
|
536
|
+
)
|
|
537
|
+
access = (
|
|
538
|
+
"get"
|
|
539
|
+
if getattr(self, "_udt_mutable_expr_depth", 0)
|
|
540
|
+
or mutable_collection
|
|
541
|
+
else "read"
|
|
542
|
+
)
|
|
543
|
+
return f"{arena}.{access}({owner}).{node.member}"
|
|
374
544
|
if isinstance(node.object, Identifier):
|
|
375
545
|
ns = node.object.name
|
|
376
546
|
if ns == "strategy":
|
|
@@ -750,16 +920,118 @@ class ExprVisitor:
|
|
|
750
920
|
safe = self._safe_name(name)
|
|
751
921
|
if self._active_var_remap and safe in self._active_var_remap:
|
|
752
922
|
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}"
|
|
757
923
|
return f"{safe}.{node.member}"
|
|
758
924
|
if name not in self.ctx.series_vars:
|
|
759
925
|
# Unknown identifier — likely an enum value
|
|
760
926
|
return f'std::string("{node.member}")'
|
|
761
927
|
return f"{obj}.{node.member}"
|
|
762
928
|
|
|
929
|
+
def _operand_na_kind(self, node, cpp_type: str) -> str | None:
|
|
930
|
+
"""Classify a relational operand by the ``na`` sentinel it can carry.
|
|
931
|
+
|
|
932
|
+
Returns:
|
|
933
|
+
|
|
934
|
+
* ``"int"`` — an ``int``/``int64_t`` expression that can hold the
|
|
935
|
+
``INT_MIN`` sentinel. Because that sentinel is a *finite* very-negative
|
|
936
|
+
integer (not NaN), naive C++ diverges from Pine's falsy-on-na rule for
|
|
937
|
+
EVERY relational — ordered (``<`` ``>`` ``<=`` ``>=``) and equality
|
|
938
|
+
(``==`` ``!=``) alike.
|
|
939
|
+
* ``"float"`` — a ``double`` expression that can hold NaN. IEEE already
|
|
940
|
+
yields ``false`` for ``==`` ``<`` ``>`` ``<=`` ``>=`` against NaN
|
|
941
|
+
(matching Pine's falsy), so the ONLY diverging float cell is ``!=``
|
|
942
|
+
(IEEE ``NaN != x`` is true; Pine is falsy).
|
|
943
|
+
* ``None`` — provably not na (numeric/bool literal, inlined
|
|
944
|
+
compile-time constant) or a non-scalar type with no ``is_na`` overload;
|
|
945
|
+
the naive emission is already correct.
|
|
946
|
+
"""
|
|
947
|
+
if cpp_type not in _NA_SCALAR_CPP:
|
|
948
|
+
return None
|
|
949
|
+
# Literals are never na.
|
|
950
|
+
if isinstance(node, (NumberLiteral, BoolLiteral)):
|
|
951
|
+
return None
|
|
952
|
+
# A bare ``na`` lowers to ``na<double>()`` — a real NaN, i.e. it IS na.
|
|
953
|
+
if self._is_na_expr(node):
|
|
954
|
+
return "float"
|
|
955
|
+
# Inlined compile-time constants (non-input known vars) never hold na.
|
|
956
|
+
if (isinstance(node, Identifier)
|
|
957
|
+
and node.name in self._known_vars
|
|
958
|
+
and node.name not in self._input_backed_vars
|
|
959
|
+
and not self._known_var_is_lexically_shadowed(node.name)):
|
|
960
|
+
return None
|
|
961
|
+
return "int" if cpp_type in ("int", "int64_t") else "float"
|
|
962
|
+
|
|
963
|
+
def _emit_na_relational(self, op: str, left: str, right: str) -> str:
|
|
964
|
+
"""Emit an na-aware relational: ``false`` when either operand is ``na``.
|
|
965
|
+
|
|
966
|
+
Mirrors the ``nz()`` lambda idiom (``visit_call``): each operand is
|
|
967
|
+
hoisted to a temporary so a stateful operand expression is evaluated
|
|
968
|
+
exactly once (no double-step), then compared only when neither side is
|
|
969
|
+
na. ``is_na`` resolves via the emitted ``using namespace pineforge;``
|
|
970
|
+
(``double`` -> ``isnan``; integral -> ``== numeric_limits<T>::min()``).
|
|
971
|
+
"""
|
|
972
|
+
return (f"([&]{{ auto _pna_l = ({left}); auto _pna_r = ({right}); "
|
|
973
|
+
f"return !is_na(_pna_l) && !is_na(_pna_r) && "
|
|
974
|
+
f"(_pna_l {op} _pna_r); }}())")
|
|
975
|
+
|
|
976
|
+
def _emit_float_relational(self, op: str, left: str, right: str) -> str:
|
|
977
|
+
"""Emit Pine's na-aware float comparator with its fixed equality band.
|
|
978
|
+
|
|
979
|
+
Pine evaluates each operand once, rejects ``na``, and converts an int
|
|
980
|
+
to float when paired with a float. TradingView oracle probes pin a
|
|
981
|
+
magnitude-independent comparison band: finite operands with
|
|
982
|
+
``abs(left - right) <= 1e-10`` compare equal. Every ordered operator is
|
|
983
|
+
derived from that same predicate, so strict ``<``/``>`` are suppressed
|
|
984
|
+
inside the band while ``<=``/``>=`` accept it.
|
|
985
|
+
|
|
986
|
+
Exact equality is checked first so equal infinities remain equal;
|
|
987
|
+
opposite infinities and finite/infinite pairs remain normally ordered.
|
|
988
|
+
The subtraction can overflow only to infinity, which is safely outside
|
|
989
|
+
the equality band. The preceding ``is_na`` guard keeps every NaN
|
|
990
|
+
comparison falsy, including ``!=``.
|
|
991
|
+
"""
|
|
992
|
+
compare = {
|
|
993
|
+
"==": "_pfc_eq",
|
|
994
|
+
"!=": "!_pfc_eq",
|
|
995
|
+
"<": "(_pfc_l < _pfc_r) && !_pfc_eq",
|
|
996
|
+
">": "(_pfc_l > _pfc_r) && !_pfc_eq",
|
|
997
|
+
"<=": "(_pfc_l < _pfc_r) || _pfc_eq",
|
|
998
|
+
">=": "(_pfc_l > _pfc_r) || _pfc_eq",
|
|
999
|
+
}[op]
|
|
1000
|
+
return (
|
|
1001
|
+
f"([&]{{ auto _pna_l = ({left}); auto _pna_r = ({right}); "
|
|
1002
|
+
"double _pfc_l = static_cast<double>(_pna_l); "
|
|
1003
|
+
"double _pfc_r = static_cast<double>(_pna_r); "
|
|
1004
|
+
"bool _pfc_eq = (_pfc_l == _pfc_r) || "
|
|
1005
|
+
"(std::isfinite(_pfc_l) && std::isfinite(_pfc_r) && "
|
|
1006
|
+
"std::fabs(_pfc_l - _pfc_r) <= 1e-10); "
|
|
1007
|
+
f"return !is_na(_pna_l) && !is_na(_pna_r) && "
|
|
1008
|
+
f"({compare}); }}())"
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
def _lower_relational(self, op: str, left_node, right_node,
|
|
1012
|
+
left_cpp: str, right_cpp: str) -> str:
|
|
1013
|
+
"""Lower a Pine relational with Pine's na and float-precision rules.
|
|
1014
|
+
|
|
1015
|
+
Shared by ``_visit_binop`` and the ``request.security`` expression
|
|
1016
|
+
builder so EVERY relational emission site honours Pine's rules. Any
|
|
1017
|
+
numeric comparison involving an inferred float gets the fixed-band
|
|
1018
|
+
wrapper for all six operators. Pure integer comparisons get the smaller
|
|
1019
|
+
KI-71 wrapper only when an operand can carry the INT_MIN ``na`` sentinel.
|
|
1020
|
+
Non-relational operators and nonnumeric operands fall through unchanged.
|
|
1021
|
+
"""
|
|
1022
|
+
if op in _RELATIONAL_OPS:
|
|
1023
|
+
lt = self._infer_type(left_node)
|
|
1024
|
+
rt = self._infer_type(right_node)
|
|
1025
|
+
if lt in _NA_SCALAR_CPP and rt in _NA_SCALAR_CPP:
|
|
1026
|
+
if "double" in (lt, rt):
|
|
1027
|
+
return self._emit_float_relational(op, left_cpp, right_cpp)
|
|
1028
|
+
lk = self._operand_na_kind(left_node, lt)
|
|
1029
|
+
rk = self._operand_na_kind(right_node, rt)
|
|
1030
|
+
int_na = "int" in (lk, rk)
|
|
1031
|
+
if int_na:
|
|
1032
|
+
return self._emit_na_relational(op, left_cpp, right_cpp)
|
|
1033
|
+
return f"({left_cpp} {op} {right_cpp})"
|
|
1034
|
+
|
|
763
1035
|
def _visit_binop(self, node: BinOp) -> str:
|
|
764
1036
|
left = self._visit_expr(node.left)
|
|
765
1037
|
right = self._visit_expr(node.right)
|
|
@@ -786,7 +1058,7 @@ class ExprVisitor:
|
|
|
786
1058
|
# Ref: https://www.tradingview.com/pine-script-docs/concepts/operators/
|
|
787
1059
|
if node.op == "/":
|
|
788
1060
|
return f"((double)({left}) / (double)({right}))"
|
|
789
|
-
return
|
|
1061
|
+
return self._lower_relational(op, node.left, node.right, left, right)
|
|
790
1062
|
|
|
791
1063
|
def _visit_unaryop(self, node: UnaryOp) -> str:
|
|
792
1064
|
operand = self._visit_expr(node.operand)
|
|
@@ -807,16 +1079,17 @@ class ExprVisitor:
|
|
|
807
1079
|
if name in BAR_FIELDS or name in BAR_SERIES_PUSH:
|
|
808
1080
|
# Index matches Pine: [0] current bar, [k] k bars ago (runtime Series deque).
|
|
809
1081
|
return f"_s_{name}[{idx}]"
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1082
|
+
safe = self._safe_name(name)
|
|
1083
|
+
# Apply per-call-site / exact block-member remap before deciding
|
|
1084
|
+
# whether the current lexical binding is a Series.
|
|
1085
|
+
if self._active_var_remap and safe in self._active_var_remap:
|
|
1086
|
+
safe = self._active_var_remap[safe]
|
|
1087
|
+
if self._binding_is_series(name, safe):
|
|
815
1088
|
# Same Pine [k] semantics as Series in runtime/series.hpp
|
|
816
1089
|
return f"{safe}[{idx}]"
|
|
817
|
-
spec = self.
|
|
1090
|
+
spec = self._collection_spec_for_name(name)
|
|
818
1091
|
if spec is not None and spec.kind in ("array", "map"):
|
|
819
|
-
return f"{self.
|
|
1092
|
+
return f"{self._collection_receiver_expr(name)}[{idx}]"
|
|
820
1093
|
# Handle strategy.* history access (e.g., strategy.position_size[1])
|
|
821
1094
|
if isinstance(node.object, MemberAccess):
|
|
822
1095
|
if isinstance(node.object.object, Identifier):
|
|
@@ -843,9 +1116,57 @@ class ExprVisitor:
|
|
|
843
1116
|
if isinstance(node.object, FuncCall):
|
|
844
1117
|
inner = self._visit_expr(node.object)
|
|
845
1118
|
cpp_t = self._infer_type(node.object)
|
|
846
|
-
if cpp_t not in ("double", "int", "bool"):
|
|
1119
|
+
if cpp_t not in ("double", "int", "int64_t", "bool"):
|
|
847
1120
|
cpp_t = "double"
|
|
848
1121
|
member = self._inline_history_member("hist_call", node)
|
|
1122
|
+
ta_site = self._get_ta_site(node.object)
|
|
1123
|
+
ta_name = (
|
|
1124
|
+
self._ta_name_from_site(ta_site)
|
|
1125
|
+
if ta_site is not None
|
|
1126
|
+
else ""
|
|
1127
|
+
)
|
|
1128
|
+
if (
|
|
1129
|
+
ta_site is not None
|
|
1130
|
+
and ta_name in {"highest", "lowest"}
|
|
1131
|
+
and self._ta_site_uses_precalc(ta_site)
|
|
1132
|
+
):
|
|
1133
|
+
# Static chart Highest/Lowest sites already advance in the
|
|
1134
|
+
# full-bar precalculation pass, including when the source call
|
|
1135
|
+
# sits behind a Pine-v6 lazy boolean edge. Their direct history
|
|
1136
|
+
# operator must use that same chart-bar clock. Advancing only
|
|
1137
|
+
# ``_hist_call`` when the lazy RHS is reached changes ``[1]``
|
|
1138
|
+
# into "previous evaluation" and can permanently miss a first
|
|
1139
|
+
# qualifying signal (Filter V2's source-bound v6 oracle).
|
|
1140
|
+
#
|
|
1141
|
+
# Keep evaluation lazy: this branch only reads the immutable
|
|
1142
|
+
# precalculated result when the expression is reached. Dynamic
|
|
1143
|
+
# runs, UDF sites, and non-precalculated calls retain the
|
|
1144
|
+
# established call-local history fallback below.
|
|
1145
|
+
ta_mem = self._ta_member_name(ta_site)
|
|
1146
|
+
precalc = f"_precalc_{ta_mem}"
|
|
1147
|
+
return (
|
|
1148
|
+
f"([&]() -> {cpp_t} {{ "
|
|
1149
|
+
f"if (_use_precalc) {{ "
|
|
1150
|
+
f"auto _pf_hist_offset_raw = ({idx}); "
|
|
1151
|
+
f"if (is_na(_pf_hist_offset_raw)) return na<{cpp_t}>(); "
|
|
1152
|
+
f"long double _pf_hist_offset_numeric = "
|
|
1153
|
+
f"static_cast<long double>(_pf_hist_offset_raw); "
|
|
1154
|
+
f"if (bar_index_ < 0 || _pf_hist_offset_numeric < 0.0L || "
|
|
1155
|
+
f"_pf_hist_offset_numeric > "
|
|
1156
|
+
f"static_cast<long double>(bar_index_)) "
|
|
1157
|
+
f"return na<{cpp_t}>(); "
|
|
1158
|
+
f"std::size_t _pf_hist_offset = "
|
|
1159
|
+
f"static_cast<std::size_t>(_pf_hist_offset_numeric); "
|
|
1160
|
+
f"std::size_t _pf_hist_bar = "
|
|
1161
|
+
f"static_cast<std::size_t>(bar_index_) - _pf_hist_offset; "
|
|
1162
|
+
f"if (_pf_hist_bar >= {precalc}.size()) "
|
|
1163
|
+
f"return na<{cpp_t}>(); "
|
|
1164
|
+
f"return {precalc}[(std::size_t)_pf_hist_bar]; }} "
|
|
1165
|
+
f"{cpp_t} _hv = ({inner}); "
|
|
1166
|
+
f"if (history_advances_new_bar()) {member}.push(_hv); "
|
|
1167
|
+
f"else {member}.update(_hv); "
|
|
1168
|
+
f"return {member}[(int)({idx})]; }}())"
|
|
1169
|
+
)
|
|
849
1170
|
return (
|
|
850
1171
|
f"([&]() -> {cpp_t} {{ "
|
|
851
1172
|
f"{cpp_t} _hv = ({inner}); "
|