@pineforge/codegen-pyodide 0.7.6 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +453 -15
- package/pineforge_codegen/analyzer/call_handlers.py +200 -11
- package/pineforge_codegen/analyzer/contracts.py +62 -0
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +532 -51
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +331 -30
- package/pineforge_codegen/codegen/helpers.py +26 -1
- package/pineforge_codegen/codegen/input.py +39 -2
- package/pineforge_codegen/codegen/security.py +321 -9
- package/pineforge_codegen/codegen/ta.py +4 -1
- package/pineforge_codegen/codegen/tables.py +63 -7
- package/pineforge_codegen/codegen/types.py +383 -21
- package/pineforge_codegen/codegen/visit_call.py +159 -22
- package/pineforge_codegen/codegen/visit_expr.py +91 -3
- package/pineforge_codegen/codegen/visit_stmt.py +188 -26
- package/pineforge_codegen/lexer.py +68 -3
- package/pineforge_codegen/parser.py +106 -49
- package/pineforge_codegen/support_checker.py +361 -37
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +4 -18
- package/pineforge_codegen-0.7.6.tar.gz +0 -0
|
@@ -100,12 +100,16 @@ from ..ast_nodes import (
|
|
|
100
100
|
)
|
|
101
101
|
from ..symbols import TypeSpec
|
|
102
102
|
from .tables import (
|
|
103
|
-
|
|
103
|
+
ARRAY_NEW_CTORS,
|
|
104
104
|
TA_RETURNS_BOOL,
|
|
105
105
|
TA_TUPLE_FIELDS,
|
|
106
106
|
MATRIX_RETURNING_METHODS,
|
|
107
107
|
)
|
|
108
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
|
+
|
|
109
113
|
|
|
110
114
|
class StmtVisitor:
|
|
111
115
|
"""Statement-level visitor methods shared across the codegen.
|
|
@@ -259,7 +263,7 @@ class StmtVisitor:
|
|
|
259
263
|
if is_global_member and isinstance(node.value, FuncCall) and self._is_input_call(node.value):
|
|
260
264
|
func_name_i, namespace_i = self._resolve_callee(node.value.callee)
|
|
261
265
|
is_static_global_input = (
|
|
262
|
-
|
|
266
|
+
not self._is_source_input(node.value)
|
|
263
267
|
and node.name not in self._array_vars
|
|
264
268
|
and node.name not in getattr(self, "_matrix_specs", {})
|
|
265
269
|
and node.name not in getattr(self, "_map_vars", {})
|
|
@@ -292,7 +296,7 @@ class StmtVisitor:
|
|
|
292
296
|
# array.new_float() etc., plus array-returning copy/slice.
|
|
293
297
|
if isinstance(node.value, FuncCall):
|
|
294
298
|
func_name, namespace = self._resolve_callee(node.value.callee)
|
|
295
|
-
if namespace == "array" and func_name in
|
|
299
|
+
if namespace == "array" and func_name in ARRAY_NEW_CTORS | {"new", "from", "copy", "slice"}:
|
|
296
300
|
self._array_vars.add(node.name)
|
|
297
301
|
spec = self._type_spec_from_expr(node.value) or self._array_spec_for_name(node.name)
|
|
298
302
|
self._collection_types.setdefault(node.name, spec)
|
|
@@ -356,16 +360,19 @@ class StmtVisitor:
|
|
|
356
360
|
lines.append(f"{pad}{cpp_type} {safe};")
|
|
357
361
|
return
|
|
358
362
|
|
|
359
|
-
#
|
|
360
|
-
# table
|
|
363
|
+
# Visual/drawing function assignments (line.new, label.new, box.new,
|
|
364
|
+
# table.new, ...) are no-ops in a backtest, but the assigned variable may
|
|
365
|
+
# still be referenced later (e.g. pushed into an array<line>, or used as a
|
|
366
|
+
# handle by sibling set_* calls). Emit a default-valued local declaration
|
|
367
|
+
# so those references compile; the value is inert. Global members are
|
|
368
|
+
# already declared at class scope, so only locals need this. (Previously
|
|
369
|
+
# only `table` results were declared, which dropped loop-local line/label
|
|
370
|
+
# handles and produced "use of undeclared identifier".)
|
|
361
371
|
if isinstance(node.value, FuncCall) and self._is_skip_expr(node.value):
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
cpp_type = self._type_for_decl(node)
|
|
367
|
-
default = "0" if cpp_type in ("int", "double") else ('std::string("")' if cpp_type == "std::string" else "false")
|
|
368
|
-
lines.append(f"{pad}{cpp_type} {safe} = {default};")
|
|
372
|
+
if not is_global_member:
|
|
373
|
+
cpp_type = self._type_for_decl(node)
|
|
374
|
+
default = "0" if cpp_type in ("int", "double") else ('std::string("")' if cpp_type == "std::string" else "false")
|
|
375
|
+
lines.append(f"{pad}{cpp_type} {safe} = {default};")
|
|
369
376
|
return
|
|
370
377
|
|
|
371
378
|
# TA call
|
|
@@ -399,12 +406,57 @@ class StmtVisitor:
|
|
|
399
406
|
self._visit_if_switch_expr(node.value, safe, lines, indent)
|
|
400
407
|
return
|
|
401
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
|
+
|
|
402
454
|
# General declaration
|
|
403
|
-
|
|
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)
|
|
404
457
|
if is_global_member:
|
|
405
458
|
lines.append(f"{pad}{safe} = {cpp_val};")
|
|
406
459
|
else:
|
|
407
|
-
cpp_type = self._type_for_decl(node)
|
|
408
460
|
lines.append(f"{pad}{cpp_type} {safe} = {cpp_val};")
|
|
409
461
|
|
|
410
462
|
@staticmethod
|
|
@@ -474,6 +526,12 @@ class StmtVisitor:
|
|
|
474
526
|
if self._active_var_remap and safe in self._active_var_remap:
|
|
475
527
|
safe = self._active_var_remap[safe]
|
|
476
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
|
+
|
|
477
535
|
if target_name in self.ctx.series_vars:
|
|
478
536
|
val_cpp = self._visit_expr(node.value)
|
|
479
537
|
if node.op == ":=":
|
|
@@ -507,7 +565,7 @@ class StmtVisitor:
|
|
|
507
565
|
f"expected {self._type_spec_to_cpp(lhs_spec)}, "
|
|
508
566
|
f"got {self._type_spec_to_cpp(rhs_spec)}",
|
|
509
567
|
)
|
|
510
|
-
val_cpp = self.
|
|
568
|
+
val_cpp = self._visit_rhs_value(node.value, target_name)
|
|
511
569
|
if node.op == ":=":
|
|
512
570
|
lines.append(f"{pad}{safe} = {val_cpp};")
|
|
513
571
|
else:
|
|
@@ -517,7 +575,7 @@ class StmtVisitor:
|
|
|
517
575
|
else:
|
|
518
576
|
lines.append(f"{pad}{safe} {node.op} {val_cpp};")
|
|
519
577
|
else:
|
|
520
|
-
val_cpp = self.
|
|
578
|
+
val_cpp = self._visit_rhs_value(node.value, target_name)
|
|
521
579
|
if node.op == ":=":
|
|
522
580
|
lines.append(f"{pad}{safe} = {val_cpp};")
|
|
523
581
|
else:
|
|
@@ -542,7 +600,20 @@ class StmtVisitor:
|
|
|
542
600
|
if name == "_":
|
|
543
601
|
continue
|
|
544
602
|
if i < len(fields):
|
|
545
|
-
|
|
603
|
+
field_expr = f"{result_var}.{fields[i]}"
|
|
604
|
+
# A history-referenced destructured name (e.g.
|
|
605
|
+
# ``[v, dir] = ta.supertrend(...)`` with ``dir[1]`` used
|
|
606
|
+
# later) is tracked in ``series_vars`` and declared as a
|
|
607
|
+
# ``Series<T>`` class member. Pushing into that member keeps
|
|
608
|
+
# its history buffer advancing so ``dir[n]`` resolves; a
|
|
609
|
+
# fresh ``double`` local would shadow the member and make
|
|
610
|
+
# ``dir[n]`` a scalar subscript (clang error). Non-series
|
|
611
|
+
# destructured names keep the plain scalar declaration.
|
|
612
|
+
if name in self.ctx.series_vars:
|
|
613
|
+
safe = self._safe_name(name)
|
|
614
|
+
lines.append(f"{pad}{safe}.push({field_expr});")
|
|
615
|
+
else:
|
|
616
|
+
lines.append(f"{pad}double {name} = {field_expr};")
|
|
546
617
|
return
|
|
547
618
|
|
|
548
619
|
# User-defined function returning a tuple: use C++17 structured bindings
|
|
@@ -585,7 +656,30 @@ class StmtVisitor:
|
|
|
585
656
|
|
|
586
657
|
lines.append(f"{pad}/* unsupported tuple assignment */")
|
|
587
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
|
+
|
|
588
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:
|
|
589
683
|
pad = " " * indent
|
|
590
684
|
|
|
591
685
|
# TA hoisting: inside per-call-site function variants, execute ALL
|
|
@@ -622,20 +716,73 @@ class StmtVisitor:
|
|
|
622
716
|
pad = " " * indent
|
|
623
717
|
start = self._visit_expr(node.start)
|
|
624
718
|
end = self._visit_expr(node.end)
|
|
625
|
-
step = self._visit_expr(node.step) if node.step else "1"
|
|
626
719
|
var = node.var # new AST uses .var instead of .var_name
|
|
627
|
-
|
|
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
|
+
)
|
|
628
747
|
# Register the loop counter so reads of it inside the body resolve (the
|
|
629
748
|
# unknown-identifier guard in _visit_ident would otherwise flag it).
|
|
630
749
|
saved_loop = self._current_loop_vars
|
|
631
750
|
self._current_loop_vars = set(self._current_loop_vars)
|
|
632
751
|
if var:
|
|
633
752
|
self._current_loop_vars.add(var)
|
|
634
|
-
|
|
635
|
-
|
|
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)
|
|
636
759
|
self._current_loop_vars = saved_loop
|
|
637
760
|
lines.append(f"{pad}}}")
|
|
638
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
|
+
|
|
639
786
|
def _visit_for_in(self, node, lines: list[str], indent: int) -> None:
|
|
640
787
|
pad = " " * indent
|
|
641
788
|
iterable = self._visit_expr(node.iterable)
|
|
@@ -649,12 +796,17 @@ class StmtVisitor:
|
|
|
649
796
|
self._current_loop_vars.add(v)
|
|
650
797
|
if node.var:
|
|
651
798
|
v_cpp = self._safe_name(node.var)
|
|
652
|
-
|
|
799
|
+
ref = "&" if self._loop_elem_is_writeback_udt(node.iterable) else ""
|
|
800
|
+
lines.append(f"{pad}for (auto{ref} {v_cpp} : {iterable}) {{")
|
|
653
801
|
elif node.vars:
|
|
654
802
|
bindings = ", ".join(node.vars)
|
|
655
803
|
lines.append(f"{pad}for (auto [{bindings}] : {iterable}) {{")
|
|
656
|
-
|
|
657
|
-
|
|
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)
|
|
658
810
|
lines.append(f"{pad}}}")
|
|
659
811
|
self._current_loop_vars = saved_loop
|
|
660
812
|
|
|
@@ -662,8 +814,12 @@ class StmtVisitor:
|
|
|
662
814
|
pad = " " * indent
|
|
663
815
|
cond = self._visit_expr(node.condition)
|
|
664
816
|
lines.append(f"{pad}while ({cond}) {{")
|
|
665
|
-
|
|
666
|
-
|
|
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)
|
|
667
823
|
lines.append(f"{pad}}}")
|
|
668
824
|
|
|
669
825
|
def _visit_switch(self, node: SwitchStmt, lines: list[str], indent: int) -> None:
|
|
@@ -712,6 +868,12 @@ class StmtVisitor:
|
|
|
712
868
|
# Check if it's a skip expr
|
|
713
869
|
if self._is_skip_expr(stmt.expr):
|
|
714
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
|
|
715
877
|
cpp = self._visit_expr(stmt.expr)
|
|
716
878
|
pad = " " * indent
|
|
717
879
|
lines.append(f"{pad}{target} = {cpp};")
|
|
@@ -273,9 +273,16 @@ class Lexer:
|
|
|
273
273
|
self._read_token()
|
|
274
274
|
if not self._at_end() and self.source[self.pos] == "\n":
|
|
275
275
|
self._advance()
|
|
276
|
-
# If parens closed on this line,
|
|
276
|
+
# If parens closed on this line, end the statement UNLESS the line
|
|
277
|
+
# ends with a continuation token (e.g. trailing operator), in which
|
|
278
|
+
# case the next line continues the same logical line.
|
|
277
279
|
if self.paren_depth == 0 and emitted_in_parens:
|
|
278
|
-
|
|
280
|
+
last_token = self.tokens[-1] if self.tokens else None
|
|
281
|
+
if last_token and last_token.type in self.CONTINUATION_TOKENS:
|
|
282
|
+
self._in_continuation = True
|
|
283
|
+
else:
|
|
284
|
+
self._in_continuation = False
|
|
285
|
+
self._emit(TokenType.NEWLINE, "\\n", self.line - 1, self.col)
|
|
279
286
|
return
|
|
280
287
|
|
|
281
288
|
# Indentation handling
|
|
@@ -290,9 +297,24 @@ class Lexer:
|
|
|
290
297
|
else:
|
|
291
298
|
indent_level = len(raw) // 4
|
|
292
299
|
|
|
300
|
+
# Operator-first line continuation: when a line *begins* with a binary
|
|
301
|
+
# / ternary operator that cannot start a statement (e.g. ``? x``,
|
|
302
|
+
# ``: y``, ``+ z``, ``and w``), it continues the previous logical line
|
|
303
|
+
# even though that line did not *end* with an operator (the break was
|
|
304
|
+
# placed before the operator instead of after it). Suppress this line's
|
|
305
|
+
# INDENT/DEDENT and drop the NEWLINE that ended the prior line so the
|
|
306
|
+
# parser sees one contiguous expression. Only applies outside parens
|
|
307
|
+
# and when not already in an end-of-line continuation.
|
|
308
|
+
starts_with_cont_op = (
|
|
309
|
+
not self._in_continuation
|
|
310
|
+
and self._line_starts_with_continuation_op()
|
|
311
|
+
)
|
|
312
|
+
if starts_with_cont_op and self.tokens and self.tokens[-1].type == TokenType.NEWLINE:
|
|
313
|
+
self.tokens.pop()
|
|
314
|
+
|
|
293
315
|
# If we're in a continuation (previous line ended with an operator),
|
|
294
316
|
# suppress INDENT/DEDENT — the indentation is cosmetic, not structural
|
|
295
|
-
if not self._in_continuation:
|
|
317
|
+
if not self._in_continuation and not starts_with_cont_op:
|
|
296
318
|
current_indent = self.indent_stack[-1]
|
|
297
319
|
if indent_level > current_indent:
|
|
298
320
|
self.indent_stack.append(indent_level)
|
|
@@ -329,6 +351,49 @@ class Lexer:
|
|
|
329
351
|
else:
|
|
330
352
|
self._in_continuation = False
|
|
331
353
|
|
|
354
|
+
def _line_starts_with_continuation_op(self) -> bool:
|
|
355
|
+
"""True when the upcoming line content begins with a binary/ternary
|
|
356
|
+
operator that can never begin a statement, so the line is a
|
|
357
|
+
continuation of the previous logical line.
|
|
358
|
+
|
|
359
|
+
Called with ``self.pos`` positioned at the first non-whitespace
|
|
360
|
+
character of the line. Deliberately conservative: ``-`` is excluded
|
|
361
|
+
(ambiguous leading unary minus) and ``.`` is excluded (``.5`` is a
|
|
362
|
+
leading-dot number, not member access). The included operators
|
|
363
|
+
(``? : + * / % == != > < >= <= and or``) cannot legally start a Pine
|
|
364
|
+
statement, so suppressing the line break for them never merges two
|
|
365
|
+
independent statements that previously parsed."""
|
|
366
|
+
src = self.source
|
|
367
|
+
p = self.pos
|
|
368
|
+
n = len(src)
|
|
369
|
+
if p >= n:
|
|
370
|
+
return False
|
|
371
|
+
c = src[p]
|
|
372
|
+
c2 = src[p + 1] if p + 1 < n else ""
|
|
373
|
+
# Two-character comparison operators.
|
|
374
|
+
if c2 == "=" and c in ("=", "!", ">", "<"):
|
|
375
|
+
return True
|
|
376
|
+
# ':' ternary-else continuation, but not ':=' (reassignment).
|
|
377
|
+
if c == ":" and c2 != "=":
|
|
378
|
+
return True
|
|
379
|
+
# Single-character operators that cannot start a statement.
|
|
380
|
+
if c in "?+*%><":
|
|
381
|
+
return True
|
|
382
|
+
# '/' division continuation, but never '//' (comment).
|
|
383
|
+
if c == "/" and c2 != "/":
|
|
384
|
+
return True
|
|
385
|
+
# 'and' / 'or' keyword continuation (require a word boundary so names
|
|
386
|
+
# like ``android`` / ``organic`` are not misread).
|
|
387
|
+
def _kw(word: str) -> bool:
|
|
388
|
+
end = p + len(word)
|
|
389
|
+
if src[p:end] != word:
|
|
390
|
+
return False
|
|
391
|
+
nxt = src[end] if end < n else ""
|
|
392
|
+
return not (nxt.isalnum() or nxt == "_")
|
|
393
|
+
if _kw("and") or _kw("or"):
|
|
394
|
+
return True
|
|
395
|
+
return False
|
|
396
|
+
|
|
332
397
|
def _advance_to(self, target: int) -> None:
|
|
333
398
|
while self.pos < target and self.pos < len(self.source):
|
|
334
399
|
self._advance()
|
|
@@ -197,6 +197,13 @@ class Parser:
|
|
|
197
197
|
# Check that the IDENT is followed by = (not == ) to confirm declaration
|
|
198
198
|
if self._peek(2).type == TokenType.EQUALS:
|
|
199
199
|
return self._parse_typed_decl()
|
|
200
|
+
# Postfix-array type-annotated declaration: float[] x = ..., int[] x = ...
|
|
201
|
+
if (cur.type in TYPE_KEYWORDS
|
|
202
|
+
and self._peek().type == TokenType.LBRACKET
|
|
203
|
+
and self._peek(2).type == TokenType.RBRACKET
|
|
204
|
+
and self._peek(3).type == TokenType.IDENT
|
|
205
|
+
and self._peek(4).type == TokenType.EQUALS):
|
|
206
|
+
return self._parse_typed_decl()
|
|
200
207
|
|
|
201
208
|
# IDENT-prefixed type-annotated declaration: ``Sample s = ...``,
|
|
202
209
|
# ``array<Sample> arr = ...``, ``matrix<float> m = ...`` — when the
|
|
@@ -393,35 +400,42 @@ class Parser:
|
|
|
393
400
|
return self._set_loc(node, start_tok)
|
|
394
401
|
|
|
395
402
|
def _parse_type_hint_string(self) -> str:
|
|
396
|
-
"""Parse primitive, UDT, array<T>,
|
|
403
|
+
"""Parse primitive, UDT, array<T>, map<K,V>, or postfix-array (``T[]``) hints."""
|
|
397
404
|
base = self._advance().value
|
|
398
|
-
if
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
depth += 1
|
|
408
|
-
parts.append("<")
|
|
409
|
-
self._advance()
|
|
410
|
-
continue
|
|
411
|
-
if tok.type == TokenType.GT:
|
|
412
|
-
if depth == 0:
|
|
405
|
+
if self._check(TokenType.LT):
|
|
406
|
+
parts: list[str] = []
|
|
407
|
+
depth = 0
|
|
408
|
+
self._advance() # <
|
|
409
|
+
while not self._at_end():
|
|
410
|
+
tok = self._current()
|
|
411
|
+
if tok.type == TokenType.LT:
|
|
412
|
+
depth += 1
|
|
413
|
+
parts.append("<")
|
|
413
414
|
self._advance()
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
415
|
+
continue
|
|
416
|
+
if tok.type == TokenType.GT:
|
|
417
|
+
if depth == 0:
|
|
418
|
+
self._advance()
|
|
419
|
+
break
|
|
420
|
+
depth -= 1
|
|
421
|
+
parts.append(">")
|
|
422
|
+
self._advance()
|
|
423
|
+
continue
|
|
424
|
+
if tok.type == TokenType.COMMA:
|
|
425
|
+
parts.append(",")
|
|
426
|
+
else:
|
|
427
|
+
parts.append(str(tok.value))
|
|
417
428
|
self._advance()
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
429
|
+
base = f"{base}<{''.join(parts)}>"
|
|
430
|
+
|
|
431
|
+
# Pine postfix-array shorthand: `float[]` == `array<float>`, `T[]` == `array<T>`.
|
|
432
|
+
# Without this the trailing `[ ]` is left unconsumed, the following name
|
|
433
|
+
# fails to parse, and the whole declaration is silently dropped.
|
|
434
|
+
while self._check(TokenType.LBRACKET) and self._peek().type == TokenType.RBRACKET:
|
|
435
|
+
self._advance() # [
|
|
436
|
+
self._advance() # ]
|
|
437
|
+
base = f"array<{base}>"
|
|
438
|
+
return base
|
|
425
439
|
|
|
426
440
|
def _parse_template_args(self) -> list[str]:
|
|
427
441
|
"""Parse and return generic args after a member name, e.g. new<K,V>()."""
|
|
@@ -487,6 +501,16 @@ class Parser:
|
|
|
487
501
|
and self._current().value not in ("na",)):
|
|
488
502
|
# Complex type: array<float>, table, etc.
|
|
489
503
|
type_hint = self._parse_type_hint_string()
|
|
504
|
+
elif (self._current().type == TokenType.IDENT
|
|
505
|
+
and self._peek().type == TokenType.LBRACKET
|
|
506
|
+
and self._peek(2).type == TokenType.RBRACKET):
|
|
507
|
+
# Postfix-array of a non-primitive / UDT element type, e.g.
|
|
508
|
+
# ``var line[] lines = ...`` or ``var store[] xs = ...``. The
|
|
509
|
+
# empty ``[]`` can only form a type here (a subscript index would
|
|
510
|
+
# be non-empty), so this is unambiguously ``array<T>``. Without
|
|
511
|
+
# this branch the ``[]`` is left unconsumed, the name fails to
|
|
512
|
+
# parse, and the whole declaration is silently dropped.
|
|
513
|
+
type_hint = self._parse_type_hint_string()
|
|
490
514
|
|
|
491
515
|
name_tok = self._consume(TokenType.IDENT)
|
|
492
516
|
self._consume(TokenType.EQUALS)
|
|
@@ -552,39 +576,64 @@ class Parser:
|
|
|
552
576
|
|
|
553
577
|
# -- Function definition --
|
|
554
578
|
|
|
579
|
+
def _parse_param_type_annotation(self) -> str | None:
|
|
580
|
+
"""Consume an optional function-parameter type annotation and return it
|
|
581
|
+
as a canonical hint string ('float', 'string', 'array<line>', 'pivot',
|
|
582
|
+
'chart.point', ...), or ``None`` for a bare untyped param.
|
|
583
|
+
|
|
584
|
+
Leaves the parser positioned at the parameter name. Supports the Pine
|
|
585
|
+
qualifier prefixes (``series`` / ``simple`` / ``const`` — consumed but
|
|
586
|
+
not part of the C++ type), builtin types, user-defined / drawing type
|
|
587
|
+
names, and the ``T[]`` postfix-array shorthand (normalised to
|
|
588
|
+
``array<T>`` by ``_parse_type_hint_string``).
|
|
589
|
+
"""
|
|
590
|
+
TYPE_TOKENS = {TokenType.TYPE_INT, TokenType.TYPE_FLOAT,
|
|
591
|
+
TokenType.TYPE_BOOL, TokenType.TYPE_STRING}
|
|
592
|
+
# Optional qualifiers — they do not affect the C++ param type.
|
|
593
|
+
while self._check(TokenType.IDENT) and self._current().value in (
|
|
594
|
+
"series", "simple", "const",
|
|
595
|
+
):
|
|
596
|
+
self._advance()
|
|
597
|
+
# Is there a type annotation before the parameter name? A builtin type
|
|
598
|
+
# token always is; an IDENT is a type only if followed by another IDENT
|
|
599
|
+
# (``line ln``), by ``[`` (``line[] arr``), or by ``<`` (the generic
|
|
600
|
+
# collection syntax ``array<float> xs`` / ``matrix<float> m`` /
|
|
601
|
+
# ``map<string,float> mp``). Without the ``<`` case the generic type is
|
|
602
|
+
# mis-consumed as the parameter name and the whole function definition
|
|
603
|
+
# silently fails to parse (its body leaks to top-level scope).
|
|
604
|
+
has_type = False
|
|
605
|
+
if self._current().type in TYPE_TOKENS:
|
|
606
|
+
has_type = True
|
|
607
|
+
elif self._check(TokenType.IDENT):
|
|
608
|
+
nxt = self._peek().type
|
|
609
|
+
if nxt in (TokenType.IDENT, TokenType.LBRACKET, TokenType.LT):
|
|
610
|
+
has_type = True
|
|
611
|
+
if not has_type:
|
|
612
|
+
return None
|
|
613
|
+
return self._parse_type_hint_string()
|
|
614
|
+
|
|
555
615
|
def _parse_func_def(self) -> FuncDef:
|
|
556
616
|
"""Parse: name(param1, param2) => expr_or_block"""
|
|
557
617
|
start_tok = self._current()
|
|
558
618
|
name = self._consume(TokenType.IDENT).value
|
|
559
619
|
self._consume(TokenType.LPAREN)
|
|
560
|
-
TYPE_TOKENS = {TokenType.TYPE_INT, TokenType.TYPE_FLOAT,
|
|
561
|
-
TokenType.TYPE_BOOL, TokenType.TYPE_STRING}
|
|
562
620
|
params = []
|
|
621
|
+
param_type_hints: list = []
|
|
622
|
+
param_defaults: list = []
|
|
563
623
|
while not self._check(TokenType.RPAREN):
|
|
564
|
-
#
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
param_name = self._consume(TokenType.IDENT).value
|
|
570
|
-
if self._check(TokenType.EQUALS):
|
|
571
|
-
self._advance()
|
|
572
|
-
self._parse_expression()
|
|
573
|
-
params.append(param_name)
|
|
574
|
-
self._match(TokenType.COMMA)
|
|
575
|
-
continue
|
|
576
|
-
# Handle optional type annotation: type param (e.g., int len, float src)
|
|
577
|
-
if self._current().type in TYPE_TOKENS:
|
|
578
|
-
self._advance() # skip the type annotation
|
|
624
|
+
# Consume the optional type annotation (builtin / user / drawing /
|
|
625
|
+
# ``T[]``), returning the canonical hint string. Handles ``float[] arr``,
|
|
626
|
+
# ``line[] ln``, ``color c``, ``SDZone z``, ``string tf``, as well as
|
|
627
|
+
# the untyped bare-name case.
|
|
628
|
+
hint = self._parse_param_type_annotation()
|
|
579
629
|
param_name = self._consume(TokenType.IDENT).value
|
|
580
|
-
|
|
581
|
-
# 'param_name' was actually a type name parsed as IDENT, next is real name
|
|
582
|
-
param_name = self._consume(TokenType.IDENT).value
|
|
583
|
-
# Skip default value: param = expr
|
|
630
|
+
pdefault = None
|
|
584
631
|
if self._check(TokenType.EQUALS):
|
|
585
632
|
self._advance() # consume '='
|
|
586
|
-
self._parse_expression() #
|
|
633
|
+
pdefault = self._parse_expression() # default value
|
|
587
634
|
params.append(param_name)
|
|
635
|
+
param_type_hints.append(hint)
|
|
636
|
+
param_defaults.append(pdefault)
|
|
588
637
|
self._match(TokenType.COMMA)
|
|
589
638
|
self._consume(TokenType.RPAREN)
|
|
590
639
|
self._skip_newlines()
|
|
@@ -601,6 +650,13 @@ class Parser:
|
|
|
601
650
|
expr = self._parse_expression()
|
|
602
651
|
node = FuncDef(name=name, params=params, body=[ExprStmt(expr=expr)], is_single_expr=True)
|
|
603
652
|
|
|
653
|
+
# Record per-param type hints + defaults (mirrors _parse_method_def) so
|
|
654
|
+
# the analyzer can type UDT/string/array params and the codegen can emit
|
|
655
|
+
# them with the correct C++ type (``pivot hi``, ``std::string s``).
|
|
656
|
+
node.annotations = {
|
|
657
|
+
"param_type_hints": param_type_hints,
|
|
658
|
+
"param_defaults": param_defaults,
|
|
659
|
+
}
|
|
604
660
|
return self._set_loc(node, start_tok)
|
|
605
661
|
|
|
606
662
|
def _parse_type_or_enum_decl(self):
|
|
@@ -677,7 +733,8 @@ class Parser:
|
|
|
677
733
|
if self._current().type in TYPE_KEYWORDS:
|
|
678
734
|
param_type = self._parse_type_hint_string()
|
|
679
735
|
elif (self._current().type == TokenType.IDENT
|
|
680
|
-
and self._peek().type
|
|
736
|
+
and self._peek().type in (TokenType.IDENT, TokenType.LBRACKET, TokenType.LT)):
|
|
737
|
+
# ``line ln`` / ``float[] arr`` / ``array<float> xs`` typed param.
|
|
681
738
|
param_type = self._parse_type_hint_string()
|
|
682
739
|
p = self._consume(TokenType.IDENT).value
|
|
683
740
|
pdefault = None
|