pineforge-codegen 0.7.5__py3-none-any.whl → 0.8.0__py3-none-any.whl
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.
- pineforge_codegen/codegen/base.py +23 -7
- pineforge_codegen/codegen/emit_top.py +45 -3
- pineforge_codegen/codegen/helpers.py +26 -1
- pineforge_codegen/codegen/input.py +25 -2
- pineforge_codegen/codegen/security.py +31 -0
- pineforge_codegen/codegen/ta.py +4 -1
- pineforge_codegen/codegen/tables.py +4 -4
- pineforge_codegen/codegen/types.py +14 -2
- pineforge_codegen/codegen/visit_call.py +12 -0
- pineforge_codegen/codegen/visit_expr.py +37 -3
- pineforge_codegen/codegen/visit_stmt.py +27 -12
- pineforge_codegen/lexer.py +68 -3
- pineforge_codegen/parser.py +64 -40
- pineforge_codegen/support_checker.py +80 -20
- {pineforge_codegen-0.7.5.dist-info → pineforge_codegen-0.8.0.dist-info}/METADATA +1 -1
- {pineforge_codegen-0.7.5.dist-info → pineforge_codegen-0.8.0.dist-info}/RECORD +18 -18
- {pineforge_codegen-0.7.5.dist-info → pineforge_codegen-0.8.0.dist-info}/WHEEL +0 -0
- {pineforge_codegen-0.7.5.dist-info → pineforge_codegen-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -902,8 +902,15 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
902
902
|
else:
|
|
903
903
|
default = self._default_for_spec(spec)
|
|
904
904
|
lines.append(f" {cpp_type} {f.name} = {default};")
|
|
905
|
+
# NA sentinel (always the last data member). A default-constructed
|
|
906
|
+
# UDT - ``var T x = na``, an array fill slot, ``T.copy()`` no-arg -
|
|
907
|
+
# is na; the ``T.new(...)`` lowering sets this false. This lets
|
|
908
|
+
# ``na(udtVar)`` lower to the ``is_na(const T&)`` overload below
|
|
909
|
+
# instead of failing because no ``is_na`` accepts a struct.
|
|
910
|
+
lines.append(f" bool __pf_na = true;")
|
|
905
911
|
lines.append(f" static {type_name} create() {{ return {type_name}{{}}; }}")
|
|
906
912
|
lines.append("};")
|
|
913
|
+
lines.append(f"inline bool is_na(const {type_name}& _z) {{ return _z.__pf_na; }}")
|
|
907
914
|
lines.append("")
|
|
908
915
|
|
|
909
916
|
# 1c. Enum constants + string tables for str.tostring(enumVar)
|
|
@@ -1048,13 +1055,22 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1048
1055
|
self._map_vars.add(name)
|
|
1049
1056
|
lines.append(f" {self._type_spec_to_cpp(self._map_spec_for_name(name))} {safe};")
|
|
1050
1057
|
continue
|
|
1051
|
-
# Detect UDT vars
|
|
1058
|
+
# Detect UDT vars. Two signals: (1) the analyzer recorded an
|
|
1059
|
+
# explicit UDT type annotation in ``_udt_var_types`` - this is the
|
|
1060
|
+
# ONLY signal when the initializer is ``na`` (``var SDZone z = na``),
|
|
1061
|
+
# where the inferred ``ptype`` is NA->double; (2) the init_str is a
|
|
1062
|
+
# ``TypeName.new(...)`` constructor. Without (1) the member would
|
|
1063
|
+
# decl as ``double`` and the later ``z = SDZone{...}`` would not
|
|
1064
|
+
# compile (assigning SDZone to double).
|
|
1052
1065
|
init_s = str(init_str)
|
|
1053
|
-
udt_type =
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1066
|
+
udt_type = self._udt_var_types.get(name)
|
|
1067
|
+
if udt_type not in self._udt_defs:
|
|
1068
|
+
udt_type = None
|
|
1069
|
+
if udt_type is None:
|
|
1070
|
+
for udt_name in self._udt_defs:
|
|
1071
|
+
if init_s.startswith(f"{udt_name}.new"):
|
|
1072
|
+
udt_type = udt_name
|
|
1073
|
+
break
|
|
1058
1074
|
if udt_type:
|
|
1059
1075
|
lines.append(f" {udt_type} {safe};")
|
|
1060
1076
|
continue
|
|
@@ -1258,7 +1274,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1258
1274
|
if isinstance(val, (int, float)):
|
|
1259
1275
|
return str(val)
|
|
1260
1276
|
if isinstance(val, str):
|
|
1261
|
-
return f'std::string("{val}")'
|
|
1277
|
+
return f'std::string("{self._cpp_string_escape(val)}")'
|
|
1262
1278
|
# Also resolve bar field references
|
|
1263
1279
|
if arg_str in BAR_FIELDS:
|
|
1264
1280
|
return BAR_FIELDS[arg_str]
|
|
@@ -182,11 +182,27 @@ class TopLevelEmitter:
|
|
|
182
182
|
for node in self._walk_ast(self.ctx.ast):
|
|
183
183
|
if not isinstance(node, FuncCall):
|
|
184
184
|
continue
|
|
185
|
-
|
|
186
|
-
if namespace == "input" and func_name == "source":
|
|
185
|
+
if self._is_source_input(node):
|
|
187
186
|
return True
|
|
188
187
|
return False
|
|
189
188
|
|
|
189
|
+
def _typed_na_init(self, cpp_val: str, name: str, ptype) -> str:
|
|
190
|
+
"""Re-type a bare ``na<double>()`` initializer to match a non-double
|
|
191
|
+
member's C++ type. A ``var int x = na`` resolves its RHS to
|
|
192
|
+
``na<double>()`` (a quiet NaN); constructing/pushing that into an int or
|
|
193
|
+
bool member is a NaN->int conversion (UB) that yields garbage and defeats
|
|
194
|
+
``is_na<T>()`` (which checks the type sentinel, e.g. INT_MIN). Returns the
|
|
195
|
+
value unchanged unless it is exactly ``na<double>()`` and the member type
|
|
196
|
+
is non-double."""
|
|
197
|
+
if cpp_val != "na<double>()":
|
|
198
|
+
return cpp_val
|
|
199
|
+
cpp_type = PINE_TYPE_TO_CPP.get(ptype, "double")
|
|
200
|
+
if cpp_type == "int" and self._is_int64_builtin_init(name):
|
|
201
|
+
cpp_type = "int64_t"
|
|
202
|
+
if cpp_type == "double":
|
|
203
|
+
return cpp_val
|
|
204
|
+
return f"na<{cpp_type}>()"
|
|
205
|
+
|
|
190
206
|
def _emit_constructor(self, lines: list[str]) -> None:
|
|
191
207
|
init_parts: list[str] = []
|
|
192
208
|
# TA members with ctor args
|
|
@@ -224,8 +240,14 @@ class TopLevelEmitter:
|
|
|
224
240
|
safe = self._safe_name(name)
|
|
225
241
|
if name in self._array_vars or name in self._map_vars:
|
|
226
242
|
continue
|
|
243
|
+
# UDT-typed var members (``var SDZone z = na``) default-construct to
|
|
244
|
+
# na via the struct's in-class ``__pf_na = true``; a ctor init like
|
|
245
|
+
# ``z(na<double>())`` would not type-match the struct member.
|
|
246
|
+
if name in self._udt_var_types and self._udt_var_types[name] in self._udt_defs:
|
|
247
|
+
continue
|
|
227
248
|
if name not in self.ctx.series_vars:
|
|
228
249
|
cpp_val = self._resolve_known(init_expr)
|
|
250
|
+
cpp_val = self._typed_na_init(cpp_val, name, ptype)
|
|
229
251
|
if self._is_compile_time_value(cpp_val):
|
|
230
252
|
init_parts.append(f"{safe}({cpp_val})")
|
|
231
253
|
# Strategy params that map to engine members
|
|
@@ -394,6 +416,25 @@ class TopLevelEmitter:
|
|
|
394
416
|
lines.append(f" if (is_first_tick_) _s_{field_name}.push({push_expr});")
|
|
395
417
|
lines.append(f" else _s_{field_name}.update({push_expr});")
|
|
396
418
|
|
|
419
|
+
# a1. Push history-referenced scalar bar builtins (time[n], bar_index[n],
|
|
420
|
+
# hl2[n], …). They land in ``series_vars`` and are declared as Series
|
|
421
|
+
# members (base.py section 6) but — unlike user series vars (pushed at
|
|
422
|
+
# their assignment) and bar fields (pushed above) — have no push site,
|
|
423
|
+
# so ``[n]`` would read an unfed buffer (the na sentinel) on every bar.
|
|
424
|
+
# Push each from its scalar lowering. A builtin whose lowering is a
|
|
425
|
+
# self-referential call (e.g. ``time_close`` -> ``time_close()``) is
|
|
426
|
+
# skipped — the call would resolve to the shadowing Series member.
|
|
427
|
+
from .tables import BAR_BUILTINS
|
|
428
|
+
for _bname in sorted(self.ctx.series_vars):
|
|
429
|
+
if _bname in self._var_names:
|
|
430
|
+
continue
|
|
431
|
+
_bexpr = BAR_BUILTINS.get(_bname)
|
|
432
|
+
if _bexpr is None or f"{_bname}(" in _bexpr:
|
|
433
|
+
continue
|
|
434
|
+
_bsafe = self._safe_name(_bname)
|
|
435
|
+
lines.append(f" if (is_first_tick_) {_bsafe}.push({_bexpr});")
|
|
436
|
+
lines.append(f" else {_bsafe}.update({_bexpr});")
|
|
437
|
+
|
|
397
438
|
# a2. Push strategy series
|
|
398
439
|
for svar in sorted(self._strategy_series_vars):
|
|
399
440
|
member = svar.replace("_strat_", "")
|
|
@@ -447,6 +488,7 @@ class TopLevelEmitter:
|
|
|
447
488
|
continue
|
|
448
489
|
if name in self.ctx.series_vars:
|
|
449
490
|
cpp_val = self._resolve_known(init_expr)
|
|
491
|
+
cpp_val = self._typed_na_init(cpp_val, name, ptype)
|
|
450
492
|
lines.append(f" {safe}.push({cpp_val});")
|
|
451
493
|
# Also init cloned copies for per-call-site function variants
|
|
452
494
|
init_emitted: set[str] = set()
|
|
@@ -490,7 +532,7 @@ class TopLevelEmitter:
|
|
|
490
532
|
func_name_i, namespace_i = self._resolve_callee(stmt.value.callee)
|
|
491
533
|
is_static_global_input = (
|
|
492
534
|
stmt.name in self._global_member_vars
|
|
493
|
-
and
|
|
535
|
+
and not self._is_source_input(stmt.value)
|
|
494
536
|
and stmt.name not in self._array_vars
|
|
495
537
|
and stmt.name not in getattr(self, "_matrix_specs", {})
|
|
496
538
|
and stmt.name not in getattr(self, "_map_vars", {})
|
|
@@ -33,6 +33,31 @@ CPP_RESERVED = {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
# Bare C++ identifiers that ``strategy.*`` (and a few other) read-only
|
|
37
|
+
# accessors lower to as zero-arg free-function calls — e.g.
|
|
38
|
+
# ``strategy.grossprofit`` -> ``gross_profit()`` (see codegen/visit_expr.py
|
|
39
|
+
# and codegen/emit_top.py). A user variable emitted with one of these names
|
|
40
|
+
# becomes a class member that shadows the engine accessor, so the codegen
|
|
41
|
+
# would emit ``gross_profit = gross_profit();`` and clang rejects the call
|
|
42
|
+
# ("called object type 'double' is not a function"). Escaping such user
|
|
43
|
+
# identifiers in ``_safe_name`` keeps the two namespaces disjoint; the
|
|
44
|
+
# accessor call strings are emitted verbatim and never routed through
|
|
45
|
+
# ``_safe_name``, so they are unaffected. Keep in sync with the accessor
|
|
46
|
+
# lowerings if new bare-call accessors are added.
|
|
47
|
+
BUILTIN_ACCESSOR_NAMES = {
|
|
48
|
+
"signed_position_size", "position_entry_name",
|
|
49
|
+
"count_wintrades", "count_losstrades", "eventrades",
|
|
50
|
+
"net_profit", "gross_profit", "gross_loss",
|
|
51
|
+
"grossprofit_percent", "grossloss_percent",
|
|
52
|
+
"max_contracts_held_all", "max_contracts_held_long",
|
|
53
|
+
"max_contracts_held_short", "max_drawdown_percent", "max_runup_percent",
|
|
54
|
+
"avg_trade", "avg_trade_percent", "avg_winning_trade", "avg_losing_trade",
|
|
55
|
+
"avg_winning_trade_percent", "avg_losing_trade_percent",
|
|
56
|
+
"margin_liquidation_price", "open_profit", "current_equity",
|
|
57
|
+
"open_trades_capital_held",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
36
61
|
class NamingHelper:
|
|
37
62
|
"""Identifier escaping, callee resolution, and a generic AST walker.
|
|
38
63
|
|
|
@@ -56,7 +81,7 @@ class NamingHelper:
|
|
|
56
81
|
|
|
57
82
|
def _safe_name(self, name: str) -> str:
|
|
58
83
|
"""Rename identifiers that collide with C++ reserved words."""
|
|
59
|
-
if name in CPP_RESERVED:
|
|
84
|
+
if name in CPP_RESERVED or name in BUILTIN_ACCESSOR_NAMES:
|
|
60
85
|
return f"_{name}_"
|
|
61
86
|
return name
|
|
62
87
|
|
|
@@ -156,6 +156,27 @@ class InputHelper:
|
|
|
156
156
|
return "get_input_int"
|
|
157
157
|
return "get_input_double"
|
|
158
158
|
|
|
159
|
+
def _is_source_input(self, node: FuncCall) -> bool:
|
|
160
|
+
"""True if an ``input.*`` call yields a *live per-bar source series*.
|
|
161
|
+
|
|
162
|
+
Covers both ``input.source(<native series>)`` and a bare
|
|
163
|
+
``input(<native series>)`` — in Pine v6 ``input(close)`` is the
|
|
164
|
+
source-input overload and behaves like ``input.source(close)``,
|
|
165
|
+
returning a series that tracks ``close`` every bar (not a constant
|
|
166
|
+
frozen at the first bar). The defval is restricted to the native
|
|
167
|
+
OHLCV series the engine can resolve at runtime (the same set
|
|
168
|
+
``input.source`` is restricted to); a bare ``input(14)`` /
|
|
169
|
+
``input(\"x\")`` / ``input(true)`` stays a frozen scalar."""
|
|
170
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
171
|
+
if namespace == "input" and func_name == "source":
|
|
172
|
+
return True
|
|
173
|
+
if func_name == "input" and namespace is None:
|
|
174
|
+
default = self._get_input_default(node)
|
|
175
|
+
if (isinstance(default, Identifier)
|
|
176
|
+
and default.name in self._NATIVE_SOURCE_SERIES):
|
|
177
|
+
return True
|
|
178
|
+
return False
|
|
179
|
+
|
|
159
180
|
def _source_defval_to_base_series(self, default) -> str:
|
|
160
181
|
"""Map an input.source defval (close/high/hl2/…) to its engine base
|
|
161
182
|
source series member (``_src_close_`` …). Falls back to
|
|
@@ -175,8 +196,10 @@ class InputHelper:
|
|
|
175
196
|
series and we read its current value; a subscripted source var is
|
|
176
197
|
already tracked as a series var by the analyzer so ``src[1]`` lowers
|
|
177
198
|
to a Series subscript. Every other input type routes through the
|
|
178
|
-
scalar getter table.
|
|
179
|
-
|
|
199
|
+
scalar getter table. A bare ``input(<native series>)`` is the Pine
|
|
200
|
+
source-input overload and is rendered identically to
|
|
201
|
+
``input.source``."""
|
|
202
|
+
if self._is_source_input(node):
|
|
180
203
|
default = self._get_input_default(node)
|
|
181
204
|
base = self._source_defval_to_base_series(default)
|
|
182
205
|
return f'get_input_source("{title}", {base})[0]'
|
|
@@ -1232,6 +1232,37 @@ class SecurityEmitter:
|
|
|
1232
1232
|
|
|
1233
1233
|
self._emit_security_rebinds(sec_id, info, lines, ta_results, indent=2, emitted_lines=lines)
|
|
1234
1234
|
emit_security_ta(post_rebind_ta_indices)
|
|
1235
|
+
returns_tuple = item.get("returns_tuple", False)
|
|
1236
|
+
tuple_size = item.get("tuple_size", 0)
|
|
1237
|
+
if (
|
|
1238
|
+
returns_tuple
|
|
1239
|
+
and tuple_size
|
|
1240
|
+
and tuple_size > 0
|
|
1241
|
+
and isinstance(expr_node, TupleLiteral)
|
|
1242
|
+
):
|
|
1243
|
+
# A tuple body destructures into per-element scalar members
|
|
1244
|
+
# ``_req_sec_{sec_id}_{i}`` (declared in ``base.py`` and reset in
|
|
1245
|
+
# ``clear_security``). Assign each element individually rather
|
|
1246
|
+
# than building the whole ``TupleLiteral`` (which lowers to an
|
|
1247
|
+
# ``std::make_tuple(...)`` against the non-existent aggregate
|
|
1248
|
+
# member ``_req_sec_{sec_id}``).
|
|
1249
|
+
for i, el in enumerate(expr_node.elements):
|
|
1250
|
+
el_cpp = self._build_security_expr(
|
|
1251
|
+
sec_id,
|
|
1252
|
+
el,
|
|
1253
|
+
None,
|
|
1254
|
+
ta_results,
|
|
1255
|
+
security_mutable_names=security_mutable_names,
|
|
1256
|
+
emitted_lines=lines,
|
|
1257
|
+
)
|
|
1258
|
+
lines.append(f" _req_sec_{sec_id}_{i} = {el_cpp};")
|
|
1259
|
+
for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
|
|
1260
|
+
lines.append(
|
|
1261
|
+
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push(bar.{field});"
|
|
1262
|
+
)
|
|
1263
|
+
lines.append(" }")
|
|
1264
|
+
lines.append("")
|
|
1265
|
+
continue
|
|
1235
1266
|
expr_cpp = self._build_security_expr(
|
|
1236
1267
|
sec_id,
|
|
1237
1268
|
expr_node,
|
pineforge_codegen/codegen/ta.py
CHANGED
|
@@ -382,14 +382,14 @@ ORDER_DIRECTION_MAP = {
|
|
|
382
382
|
|
|
383
383
|
# Methods called as ``array.method(arr, ...)`` or ``arr.method(...)``.
|
|
384
384
|
ARRAY_METHODS = {
|
|
385
|
-
"get": lambda a, args: f"{a}[{args[0]}]",
|
|
386
|
-
"set": lambda a, args: f"{a}[{args[0]}] = {args[1]}",
|
|
385
|
+
"get": lambda a, args: f"{a}[({args[0]})]",
|
|
386
|
+
"set": lambda a, args: f"{a}[({args[0]})] = {args[1]}",
|
|
387
387
|
"push": lambda a, args: f"{a}.push_back({args[0]})",
|
|
388
388
|
"unshift": lambda a, args: f"{a}.insert({a}.begin(), {args[0]})",
|
|
389
389
|
"insert": lambda a, args: f"{a}.insert({a}.begin() + (int)({args[0]}), {args[1]})",
|
|
390
390
|
"pop": lambda a, args: f"[&](){{ auto v={a}.back(); {a}.pop_back(); return v; }}()",
|
|
391
391
|
"shift": lambda a, args: f"[&](){{ auto v={a}.front(); {a}.erase({a}.begin()); return v; }}()",
|
|
392
|
-
"remove": lambda a, args: f"[&](){{ auto v={a}[{args[0]}]; {a}.erase({a}.begin()+(int)({args[0]})); return v; }}()",
|
|
392
|
+
"remove": lambda a, args: f"[&](){{ auto v={a}[({args[0]})]; {a}.erase({a}.begin()+(int)({args[0]})); return v; }}()",
|
|
393
393
|
"first": lambda a, args: f"{a}.front()",
|
|
394
394
|
"last": lambda a, args: f"{a}.back()",
|
|
395
395
|
"size": lambda a, args: f"(double){a}.size()",
|
|
@@ -435,7 +435,7 @@ ARRAY_METHODS = {
|
|
|
435
435
|
"mode": lambda a, args: f"[&](){{ std::unordered_map<double,int> m; for(auto v:{a})m[v]++; double best=0; int bc=0; for(auto&[v,c]:m)if(c>bc||(c==bc&&v<best)){{bc=c;best=v;}} return best; }}()",
|
|
436
436
|
"percentile_linear_interpolation": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); double k=({args[0]}/100.0)*c.size()-0.5; int i=std::max(0,(int)k); double f=k-i; if(i+1>=(int)c.size()) return c.back(); return c[i]*(1-f)+c[i+1]*f; }}()",
|
|
437
437
|
"percentile_nearest_rank": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); int r=(int)std::ceil(({args[0]}/100.0)*c.size()); return c[std::min(r-1,(int)c.size()-1)]; }}()",
|
|
438
|
-
"percentrank": lambda a, args: f"[&](){{ if({a}.size()<=1) return na<double>(); double v={a}[{args[0]}]; if(std::isnan(v)) return na<double>(); int le=0; for(auto x:{a}) if(!std::isnan(x) && x<=v) le++; return (double)(le-1)/({a}.size()-1)*100.0; }}()",
|
|
438
|
+
"percentrank": lambda a, args: f"[&](){{ if({a}.size()<=1) return na<double>(); double v={a}[({args[0]})]; if(std::isnan(v)) return na<double>(); int le=0; for(auto x:{a}) if(!std::isnan(x) && x<=v) le++; return (double)(le-1)/({a}.size()-1)*100.0; }}()",
|
|
439
439
|
"abs": lambda a, args: f"[&](){{ std::vector<double> r; for(auto v:{a})r.push_back(std::abs(v)); return r; }}()",
|
|
440
440
|
"join": lambda a, args: "[&](){{ std::string r; for(size_t i=0;i<{arr}.size();i++){{ if(i>0)r+={sep}; r+=std::to_string({arr}[i]); }} return r; }}()".format(arr=a, sep=args[0] if args else 'std::string(",")'),
|
|
441
441
|
"standardize": lambda a, args: f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); s=std::sqrt(s/{a}.size()); std::vector<double> r; for(auto v:{a})r.push_back(s==0?1.0:(v-m)/s); return r; }}()",
|
|
@@ -357,7 +357,12 @@ class TypeInferer:
|
|
|
357
357
|
|
|
358
358
|
def _series_type_for(self, name: str) -> str:
|
|
359
359
|
"""C++ element type for a series variable's history buffer."""
|
|
360
|
-
|
|
360
|
+
from .tables import INT64_BUILTINS
|
|
361
|
+
# A bare int64 bar builtin used as a history series (``time[1]``) needs an
|
|
362
|
+
# int64_t buffer: epoch-ms overflow int32 and the na sentinel would be
|
|
363
|
+
# misdetected. ``_is_int64_builtin_init`` only matches user vars whose
|
|
364
|
+
# init RHS is such a builtin, so also match the builtin name directly.
|
|
365
|
+
if name in INT64_BUILTINS or self._is_int64_builtin_init(name):
|
|
361
366
|
return "int64_t"
|
|
362
367
|
sym = self.ctx.symbols.resolve(name)
|
|
363
368
|
if sym is not None:
|
|
@@ -473,8 +478,15 @@ class TypeInferer:
|
|
|
473
478
|
return "std::string"
|
|
474
479
|
if func_name == "bool":
|
|
475
480
|
return "bool"
|
|
476
|
-
if func_name
|
|
481
|
+
if func_name == "int":
|
|
477
482
|
return "int"
|
|
483
|
+
# ``input.time`` returns an epoch-MS timestamp and ``input.color``
|
|
484
|
+
# a packed ARGB int — both use the ``get_input_int64`` getter
|
|
485
|
+
# (input.py), so their storage must be ``int64_t`` or the value
|
|
486
|
+
# truncates under int32 (e.g. a date-window bound flips sign and
|
|
487
|
+
# the guard is permanently false).
|
|
488
|
+
if func_name in ("color", "time"):
|
|
489
|
+
return "int64_t"
|
|
478
490
|
return "double"
|
|
479
491
|
if namespace == "str":
|
|
480
492
|
if func_name == "split":
|
|
@@ -439,6 +439,13 @@ class CallVisitor:
|
|
|
439
439
|
if namespace == "color":
|
|
440
440
|
return self._visit_color_call(func_name, node)
|
|
441
441
|
|
|
442
|
+
# Bare color(...) cast (cosmetic). The engine has no color-cast helper
|
|
443
|
+
# and colors have no backtest-logic effect, so emit a benign default
|
|
444
|
+
# color (0 = na color, matching the color.new / from_gradient
|
|
445
|
+
# fallbacks). The support checker warns on this construct.
|
|
446
|
+
if namespace is None and func_name == "color" and func_name not in self._func_names:
|
|
447
|
+
return "0"
|
|
448
|
+
|
|
442
449
|
# Skip visual/unsupported namespace calls
|
|
443
450
|
if namespace in SKIP_NAMESPACES or namespace in SKIP_VAR_TYPES:
|
|
444
451
|
return "0"
|
|
@@ -913,6 +920,11 @@ class CallVisitor:
|
|
|
913
920
|
if f_cpp_type == "int" and "na<double>" in val:
|
|
914
921
|
val = val.replace("na<double>()", "0")
|
|
915
922
|
field_inits.append(f".{f.name} = {val}")
|
|
923
|
+
# Mark the constructed object non-na (the struct's ``__pf_na`` is the
|
|
924
|
+
# last declared field, so this designator stays in declaration order).
|
|
925
|
+
# A bare default-constructed UDT keeps ``__pf_na = true`` (na); only a
|
|
926
|
+
# real ``.new(...)`` flips it false so ``na(obj)`` reports correctly.
|
|
927
|
+
field_inits.append(".__pf_na = false")
|
|
916
928
|
return f"{namespace}{{{', '.join(field_inits)}}}"
|
|
917
929
|
|
|
918
930
|
# UDT copy: TypeName.copy(obj)
|
|
@@ -180,7 +180,7 @@ class ExprVisitor:
|
|
|
180
180
|
if isinstance(node, NumberLiteral):
|
|
181
181
|
return str(node.value)
|
|
182
182
|
if isinstance(node, StringLiteral):
|
|
183
|
-
return f'std::string("{node.value}")'
|
|
183
|
+
return f'std::string("{self._cpp_string_escape(node.value)}")'
|
|
184
184
|
if isinstance(node, BoolLiteral):
|
|
185
185
|
return "true" if node.value else "false"
|
|
186
186
|
if isinstance(node, NaLiteral):
|
|
@@ -475,8 +475,17 @@ class ExprVisitor:
|
|
|
475
475
|
if node.member in ("is_heikinashi", "is_kagi", "is_linebreak",
|
|
476
476
|
"is_pnf", "is_range", "is_renko"):
|
|
477
477
|
return "false"
|
|
478
|
-
#
|
|
479
|
-
#
|
|
478
|
+
# Cosmetic / chart-only reads with no batch-mode value (theme
|
|
479
|
+
# colors, viewport bar times). The support checker warns on
|
|
480
|
+
# these (COSMETIC_MEMBERS); emit a benign default (0 = na color /
|
|
481
|
+
# epoch 0). They have no backtest-logic effect.
|
|
482
|
+
if node.member in ("fg_color", "bg_color",
|
|
483
|
+
"left_visible_bar_time",
|
|
484
|
+
"right_visible_bar_time"):
|
|
485
|
+
return "0"
|
|
486
|
+
# Defensive: support_checker (COSMETIC_MEMBERS / UNSUPPORTED_MEMBERS)
|
|
487
|
+
# should already have warned/rejected any unhandled chart.* member.
|
|
488
|
+
# Reaching here is a bug.
|
|
480
489
|
raise ValueError(
|
|
481
490
|
f"codegen: unhandled chart.{node.member} — analyzer should have rejected. "
|
|
482
491
|
f"Add a handler above or extend UNSUPPORTED_MEMBERS."
|
|
@@ -717,6 +726,31 @@ class ExprVisitor:
|
|
|
717
726
|
if series_name not in self._strategy_series_vars:
|
|
718
727
|
self._strategy_series_vars.add(series_name)
|
|
719
728
|
return f"{series_name}[{idx}]"
|
|
729
|
+
# History reference applied directly to an inline call result, e.g.
|
|
730
|
+
# ``ta.highest(high, 10)[1]`` or ``f()[2]``. In Pine the call yields a
|
|
731
|
+
# series, so ``[k]`` reads its value k bars ago — but the call lowers to
|
|
732
|
+
# a freshly-computed C++ scalar, and ``scalar[k]`` is not subscriptable.
|
|
733
|
+
# Materialize the result into a self-contained history buffer: a static
|
|
734
|
+
# ``Series<T>`` that pushes (new bar) / updates (intrabar) the value
|
|
735
|
+
# exactly once per evaluation — same semantics as every other series in
|
|
736
|
+
# the strategy — and read ``[k]`` off it. The inner call is emitted once
|
|
737
|
+
# so its own stateful indicator is not double-stepped, and the buffer
|
|
738
|
+
# clears itself on run-start (``is_first_tick_ && bar_index_ == 0``) so a
|
|
739
|
+
# reused strategy handle (parameter sweep) does not leak prior-run history.
|
|
740
|
+
if isinstance(node.object, FuncCall):
|
|
741
|
+
inner = self._visit_expr(node.object)
|
|
742
|
+
cpp_t = self._infer_type(node.object)
|
|
743
|
+
if cpp_t not in ("double", "int", "bool"):
|
|
744
|
+
cpp_t = "double"
|
|
745
|
+
return (
|
|
746
|
+
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
|
+
f"{cpp_t} _hv = ({inner}); "
|
|
750
|
+
f"if (is_first_tick_) _hist_call.push(_hv); "
|
|
751
|
+
f"else _hist_call.update(_hv); "
|
|
752
|
+
f"return _hist_call[(int)({idx})]; }}())"
|
|
753
|
+
)
|
|
720
754
|
obj = self._visit_expr(node.object)
|
|
721
755
|
# If subscripting a non-series variable (e.g., function parameter),
|
|
722
756
|
# src[0] → src (current value), src[N>0] → src (can't access history)
|
|
@@ -100,7 +100,6 @@ from ..ast_nodes import (
|
|
|
100
100
|
)
|
|
101
101
|
from ..symbols import TypeSpec
|
|
102
102
|
from .tables import (
|
|
103
|
-
SKIP_VAR_TYPES,
|
|
104
103
|
TA_RETURNS_BOOL,
|
|
105
104
|
TA_TUPLE_FIELDS,
|
|
106
105
|
MATRIX_RETURNING_METHODS,
|
|
@@ -259,7 +258,7 @@ class StmtVisitor:
|
|
|
259
258
|
if is_global_member and isinstance(node.value, FuncCall) and self._is_input_call(node.value):
|
|
260
259
|
func_name_i, namespace_i = self._resolve_callee(node.value.callee)
|
|
261
260
|
is_static_global_input = (
|
|
262
|
-
|
|
261
|
+
not self._is_source_input(node.value)
|
|
263
262
|
and node.name not in self._array_vars
|
|
264
263
|
and node.name not in getattr(self, "_matrix_specs", {})
|
|
265
264
|
and node.name not in getattr(self, "_map_vars", {})
|
|
@@ -356,16 +355,19 @@ class StmtVisitor:
|
|
|
356
355
|
lines.append(f"{pad}{cpp_type} {safe};")
|
|
357
356
|
return
|
|
358
357
|
|
|
359
|
-
#
|
|
360
|
-
# table
|
|
358
|
+
# Visual/drawing function assignments (line.new, label.new, box.new,
|
|
359
|
+
# table.new, ...) are no-ops in a backtest, but the assigned variable may
|
|
360
|
+
# still be referenced later (e.g. pushed into an array<line>, or used as a
|
|
361
|
+
# handle by sibling set_* calls). Emit a default-valued local declaration
|
|
362
|
+
# so those references compile; the value is inert. Global members are
|
|
363
|
+
# already declared at class scope, so only locals need this. (Previously
|
|
364
|
+
# only `table` results were declared, which dropped loop-local line/label
|
|
365
|
+
# handles and produced "use of undeclared identifier".)
|
|
361
366
|
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};")
|
|
367
|
+
if not is_global_member:
|
|
368
|
+
cpp_type = self._type_for_decl(node)
|
|
369
|
+
default = "0" if cpp_type in ("int", "double") else ('std::string("")' if cpp_type == "std::string" else "false")
|
|
370
|
+
lines.append(f"{pad}{cpp_type} {safe} = {default};")
|
|
369
371
|
return
|
|
370
372
|
|
|
371
373
|
# TA call
|
|
@@ -542,7 +544,20 @@ class StmtVisitor:
|
|
|
542
544
|
if name == "_":
|
|
543
545
|
continue
|
|
544
546
|
if i < len(fields):
|
|
545
|
-
|
|
547
|
+
field_expr = f"{result_var}.{fields[i]}"
|
|
548
|
+
# A history-referenced destructured name (e.g.
|
|
549
|
+
# ``[v, dir] = ta.supertrend(...)`` with ``dir[1]`` used
|
|
550
|
+
# later) is tracked in ``series_vars`` and declared as a
|
|
551
|
+
# ``Series<T>`` class member. Pushing into that member keeps
|
|
552
|
+
# its history buffer advancing so ``dir[n]`` resolves; a
|
|
553
|
+
# fresh ``double`` local would shadow the member and make
|
|
554
|
+
# ``dir[n]`` a scalar subscript (clang error). Non-series
|
|
555
|
+
# destructured names keep the plain scalar declaration.
|
|
556
|
+
if name in self.ctx.series_vars:
|
|
557
|
+
safe = self._safe_name(name)
|
|
558
|
+
lines.append(f"{pad}{safe}.push({field_expr});")
|
|
559
|
+
else:
|
|
560
|
+
lines.append(f"{pad}double {name} = {field_expr};")
|
|
546
561
|
return
|
|
547
562
|
|
|
548
563
|
# User-defined function returning a tuple: use C++17 structured bindings
|
pineforge_codegen/lexer.py
CHANGED
|
@@ -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()
|
pineforge_codegen/parser.py
CHANGED
|
@@ -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)
|
|
@@ -561,24 +585,24 @@ class Parser:
|
|
|
561
585
|
TokenType.TYPE_BOOL, TokenType.TYPE_STRING}
|
|
562
586
|
params = []
|
|
563
587
|
while not self._check(TokenType.RPAREN):
|
|
564
|
-
#
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
# Handle optional type annotation: type param (e.g., int len, float src)
|
|
588
|
+
# Consume optional Pine parameter type qualifiers, e.g. `series float x`,
|
|
589
|
+
# `simple string maType`, `const int n`. Each is one qualifier in front
|
|
590
|
+
# of the (optional) type and the name — NOT a separate parameter.
|
|
591
|
+
while self._check(TokenType.IDENT) and self._current().value in (
|
|
592
|
+
"series",
|
|
593
|
+
"simple",
|
|
594
|
+
"const",
|
|
595
|
+
):
|
|
596
|
+
self._advance()
|
|
597
|
+
# Handle optional type annotation: type param (e.g., int len, float src,
|
|
598
|
+
# string s). Built-in types are dedicated tokens; user-defined types are
|
|
599
|
+
# IDENTs and handled by the "next is IDENT" check below.
|
|
577
600
|
if self._current().type in TYPE_TOKENS:
|
|
578
601
|
self._advance() # skip the type annotation
|
|
579
602
|
param_name = self._consume(TokenType.IDENT).value
|
|
580
603
|
if self._check(TokenType.IDENT):
|
|
581
|
-
# 'param_name' was actually a type name parsed as IDENT,
|
|
604
|
+
# 'param_name' was actually a (user-defined) type name parsed as IDENT,
|
|
605
|
+
# next is the real name.
|
|
582
606
|
param_name = self._consume(TokenType.IDENT).value
|
|
583
607
|
# Skip default value: param = expr
|
|
584
608
|
if self._check(TokenType.EQUALS):
|
|
@@ -80,6 +80,10 @@ SUPPORTED_MATRIX: frozenset[str] = frozenset(set(MATRIX_METHODS) | {"new"})
|
|
|
80
80
|
SUPPORTED_SYMINFO: frozenset[str] = frozenset(SYMINFO_MEMBER_MAP)
|
|
81
81
|
SUPPORTED_COLOR_CONST: frozenset[str] = frozenset(COLOR_CONST_MAP)
|
|
82
82
|
SUPPORTED_COLOR_FUNC: frozenset[str] = frozenset({"new", "rgb", "r", "g", "b", "t"})
|
|
83
|
+
# Cosmetic color builders with no backtest-logic effect. Warned (not rejected);
|
|
84
|
+
# codegen emits a benign default color (0 = na color). color.from_gradient is a
|
|
85
|
+
# charting/plot helper that only tints visual output.
|
|
86
|
+
COSMETIC_COLOR_FUNC: frozenset[str] = frozenset({"from_gradient"})
|
|
83
87
|
SUPPORTED_TIMEFRAME_FUNC: frozenset[str] = frozenset({"change", "in_seconds"})
|
|
84
88
|
SUPPORTED_RUNTIME_FUNC: frozenset[str] = frozenset({"error"})
|
|
85
89
|
# log.* helpers wired into pine_log_{info,warning,error} by codegen/visit_call.
|
|
@@ -96,7 +100,6 @@ HARD_REJECT_FUNC: dict[str, str] = {
|
|
|
96
100
|
"request.seed": "External seed data feeds not available in PineForge.",
|
|
97
101
|
"request.quandl": "External Quandl data not available in PineForge.",
|
|
98
102
|
"request.currency_rate": "Currency conversion data not available in PineForge.",
|
|
99
|
-
"color.from_gradient": "Charting helpers not available in PineForge backtests.",
|
|
100
103
|
# ticker.* chart-type modifiers and cross-symbol constructors — hard reject
|
|
101
104
|
"ticker.heikinashi": (
|
|
102
105
|
"ticker.heikinashi() chart-type modifier / cross-symbol construction not supported — "
|
|
@@ -215,10 +218,17 @@ NOT_YET_FUNC: dict[str, str] = {
|
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
# Bare (no-namespace) function names that codegen has no handler for.
|
|
218
|
-
# Without a handler, the generic emitter at visit_call.py:912 would
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
# Without a handler, the generic emitter at visit_call.py:912 would produce an
|
|
222
|
+
# undeclared C++ symbol. Reject loudly. (Currently empty — the bare color(...)
|
|
223
|
+
# cast moved to COSMETIC_BARE_FUNCS below.)
|
|
224
|
+
UNSUPPORTED_BARE_FUNCS: dict[str, str] = {}
|
|
225
|
+
|
|
226
|
+
# Bare (no-namespace) cosmetic casts with no backtest-logic effect. Warned
|
|
227
|
+
# (not rejected); codegen emits a benign default. ``color(x)`` is a Pine color
|
|
228
|
+
# cast — colors never affect trade logic, so codegen emits a default color
|
|
229
|
+
# (0 = na color) and the strategy keeps running.
|
|
230
|
+
COSMETIC_BARE_FUNCS: dict[str, str] = {
|
|
231
|
+
"color": "color(...) cast has no effect in PineForge backtests (visual only); it emits a default color.",
|
|
222
232
|
}
|
|
223
233
|
|
|
224
234
|
# Whole namespaces with NO codegen support. Any call into one of these
|
|
@@ -230,14 +240,19 @@ UNSUPPORTED_NAMESPACES: dict[str, str] = {
|
|
|
230
240
|
"volume_row": "volume_row.* is not supported in PineForge batch backtests; same reason as footprint.*.",
|
|
231
241
|
}
|
|
232
242
|
|
|
233
|
-
# Member-access references with no batch-mode equivalent.
|
|
234
|
-
#
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
# Member-access references with no batch-mode equivalent. Reject loudly.
|
|
244
|
+
# (Currently empty — the chart.* cosmetic reads moved to COSMETIC_MEMBERS.)
|
|
245
|
+
UNSUPPORTED_MEMBERS: dict[tuple[str, str], str] = {}
|
|
246
|
+
|
|
247
|
+
# Cosmetic / chart-only member reads with no backtest-logic effect. Warned
|
|
248
|
+
# (not rejected); codegen emits the benign default — chart.* falls through
|
|
249
|
+
# SKIP_NAMESPACES in visit_expr.py to ``0`` (and the analyzer types chart.* as
|
|
250
|
+
# COLOR, so the 0 lands in an int color slot and compiles cleanly).
|
|
251
|
+
COSMETIC_MEMBERS: dict[tuple[str, str], str] = {
|
|
252
|
+
("chart", "left_visible_bar_time"): "chart.left_visible_bar_time has no meaning in a batch backtest (no viewport); emits 0.",
|
|
253
|
+
("chart", "right_visible_bar_time"): "chart.right_visible_bar_time has no meaning in a batch backtest (no viewport); emits 0.",
|
|
254
|
+
("chart", "bg_color"): "chart.bg_color has no meaning in a batch backtest (no chart theme); emits a default color.",
|
|
255
|
+
("chart", "fg_color"): "chart.fg_color has no meaning in a batch backtest (no chart theme); emits a default color.",
|
|
241
256
|
}
|
|
242
257
|
|
|
243
258
|
# Constant-only namespaces whose members are drawing/visual style constants
|
|
@@ -692,9 +707,15 @@ class SupportChecker:
|
|
|
692
707
|
self._visit_children(node)
|
|
693
708
|
return
|
|
694
709
|
|
|
695
|
-
# Bare
|
|
696
|
-
#
|
|
697
|
-
|
|
710
|
+
# Bare cosmetic casts (e.g. `color(na)` -> default color). No backtest
|
|
711
|
+
# effect; warn and let codegen emit a benign default color.
|
|
712
|
+
if ns is None and name in COSMETIC_BARE_FUNCS:
|
|
713
|
+
self._warn(node, COSMETIC_BARE_FUNCS[name])
|
|
714
|
+
self._visit_children(node)
|
|
715
|
+
return
|
|
716
|
+
|
|
717
|
+
# Bare-function rejections. Codegen would otherwise fall through to the
|
|
718
|
+
# generic emit at visit_call.py:912 and produce an undeclared C++ symbol.
|
|
698
719
|
if ns is None and self._reject_if_in(
|
|
699
720
|
UNSUPPORTED_BARE_FUNCS,
|
|
700
721
|
name,
|
|
@@ -865,6 +886,16 @@ class SupportChecker:
|
|
|
865
886
|
self._err(node, f"timeframe.{name}(...) is not implemented in PineForge runtime.")
|
|
866
887
|
self._visit_children(node)
|
|
867
888
|
return
|
|
889
|
+
if ns == "color" and name in COSMETIC_COLOR_FUNC:
|
|
890
|
+
# Cosmetic color builder (e.g. color.from_gradient): no backtest
|
|
891
|
+
# effect. Warn and let codegen emit a default color (0).
|
|
892
|
+
self._warn(
|
|
893
|
+
node,
|
|
894
|
+
f"color.{name}(...) has no effect in PineForge backtests "
|
|
895
|
+
f"(visual only); it emits a default color.",
|
|
896
|
+
)
|
|
897
|
+
self._visit_children(node)
|
|
898
|
+
return
|
|
868
899
|
if ns == "color" and name not in SUPPORTED_COLOR_FUNC:
|
|
869
900
|
self._err(node, f"color.{name}(...) is not implemented in PineForge runtime.")
|
|
870
901
|
self._visit_children(node)
|
|
@@ -1001,7 +1032,21 @@ class SupportChecker:
|
|
|
1001
1032
|
lambda k, v: f"{k}.{node.member}: {v}",
|
|
1002
1033
|
):
|
|
1003
1034
|
return
|
|
1004
|
-
#
|
|
1035
|
+
# Cosmetic / chart-only member reads (chart.fg_color/bg_color/visible
|
|
1036
|
+
# bar times). No backtest-logic effect; warn and let codegen emit the
|
|
1037
|
+
# benign default (chart.* -> 0 via SKIP_NAMESPACES in visit_expr.py).
|
|
1038
|
+
if (
|
|
1039
|
+
isinstance(node.object, Identifier)
|
|
1040
|
+
and (node.object.name, node.member) in COSMETIC_MEMBERS
|
|
1041
|
+
):
|
|
1042
|
+
self._warn(
|
|
1043
|
+
node,
|
|
1044
|
+
f"{node.object.name}.{node.member}: "
|
|
1045
|
+
f"{COSMETIC_MEMBERS[(node.object.name, node.member)]}",
|
|
1046
|
+
)
|
|
1047
|
+
self._visit_children(node)
|
|
1048
|
+
return
|
|
1049
|
+
# Specific unsupported (namespace, member) pairs.
|
|
1005
1050
|
if isinstance(node.object, Identifier) and self._reject_if_in(
|
|
1006
1051
|
UNSUPPORTED_MEMBERS,
|
|
1007
1052
|
(node.object.name, node.member),
|
|
@@ -1134,10 +1179,25 @@ class SupportChecker:
|
|
|
1134
1179
|
hint="Codegen only recognizes the barmerge.lookahead_* literal; other values are silently treated as lookahead_off.",
|
|
1135
1180
|
)
|
|
1136
1181
|
if self._is_barmerge_member(lookahead_node, "lookahead_on"):
|
|
1137
|
-
|
|
1182
|
+
# lookahead_on is supported by codegen + engine: base.py forwards
|
|
1183
|
+
# the flag into _security_eval_info, emit_top.py registers it via
|
|
1184
|
+
# register_security_eval(..., lookahead_on=true), and the engine
|
|
1185
|
+
# (engine_security.cpp) dispatches the partial HTF eval and gates
|
|
1186
|
+
# the per-bucket series slot on it. The completed-HTF value becomes
|
|
1187
|
+
# visible from the HTF bucket's first chart bar (TV's forward-look).
|
|
1188
|
+
# We emit a WARNING (not ERROR) because lookahead_on is inherently
|
|
1189
|
+
# data-sensitive: with a 0-offset HTF expression it exposes the
|
|
1190
|
+
# in-progress bucket's value (TV's documented repaint), whereas the
|
|
1191
|
+
# safe non-repainting idiom pairs it with a [1] offset
|
|
1192
|
+
# (e.g. close[1]) to read only completed prior buckets.
|
|
1193
|
+
self._warn(
|
|
1138
1194
|
lookahead_node,
|
|
1139
|
-
"request.security lookahead_on
|
|
1140
|
-
|
|
1195
|
+
"request.security lookahead_on changes HTF timing: the completed "
|
|
1196
|
+
"higher-timeframe value is exposed from the bucket's first chart "
|
|
1197
|
+
"bar. PineForge emits this (engine-supported) but results differ "
|
|
1198
|
+
"from lookahead_off and, with a 0-offset HTF expression, repaint "
|
|
1199
|
+
"future data.",
|
|
1200
|
+
hint="The safe non-repainting form pairs lookahead_on with a [1] offset (e.g. close[1]) to read only completed prior HTF bars.",
|
|
1141
1201
|
)
|
|
1142
1202
|
|
|
1143
1203
|
# Data-adjustment kwargs: codegen emits a numeric constant but the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pineforge-codegen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: PineScript v6 to C++ transpiler that targets the pineforge-engine runtime.
|
|
5
5
|
Project-URL: Homepage, https://github.com/pineforge-4pass/pineforge-codegen-oss
|
|
6
6
|
Project-URL: Issues, https://github.com/pineforge-4pass/pineforge-codegen-oss/issues
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
pineforge_codegen/__init__.py,sha256=O8-sYnIHi1Kwo9h6khfDrKrGXZ85TQJlJfvkkgCo4QU,4158
|
|
2
2
|
pineforge_codegen/ast_nodes.py,sha256=a5wW_wJ8QPy35guqJOVXU6D89kdTVrTNcd-lw0-dTaM,7680
|
|
3
3
|
pineforge_codegen/errors.py,sha256=U6oUuK9GyEDBpSriXzhWyKMuTG-hyk4mND6gVjnxu6A,2931
|
|
4
|
-
pineforge_codegen/lexer.py,sha256=
|
|
5
|
-
pineforge_codegen/parser.py,sha256=
|
|
4
|
+
pineforge_codegen/lexer.py,sha256=4CLxGfBkv2W8EduCOv2e_y1tdqSW1oCqxoQ2lnURLhk,21774
|
|
5
|
+
pineforge_codegen/parser.py,sha256=m2l1da_R_CifdcsMImDyxvKzY_rk_qsj6UVsoRnYWcA,49351
|
|
6
6
|
pineforge_codegen/pragmas.py,sha256=xzXqecJk5nhOFxha3fWA-dq8lNJ69v4w85vZx71zyF4,4550
|
|
7
7
|
pineforge_codegen/signatures.py,sha256=FGwOPjrEgw8a5hOEx3zWFdor6aMb0iJDBIXIa7VDgSA,31139
|
|
8
|
-
pineforge_codegen/support_checker.py,sha256=
|
|
8
|
+
pineforge_codegen/support_checker.py,sha256=TKvkRuQKllhA2cdc2UWW18FUZSid7H7BK0nxidY56k0,63076
|
|
9
9
|
pineforge_codegen/symbols.py,sha256=ixjkvOfkZrhlteQQhSAlOafcdTcSaTshe-rsfUPOK1A,3660
|
|
10
10
|
pineforge_codegen/tokens.py,sha256=SODOOSXKiTW0GMOz41D_IykuAVsUbbSFYS11pqON2rY,12882
|
|
11
11
|
pineforge_codegen/tv_input_choices.py,sha256=4t5q-49hwWquoOMVMbx_WgvFzsZ2Xll24imI7Evsk8I,2508
|
|
@@ -17,19 +17,19 @@ pineforge_codegen/analyzer/diagnostics.py,sha256=XQihiH2vQYcPQ83NwTKU6RAuakRXkuh
|
|
|
17
17
|
pineforge_codegen/analyzer/tables.py,sha256=KiI3gvYGWBw9PgX_PPXPS-fk8dfH54OjMCMGeb3tD0Y,7442
|
|
18
18
|
pineforge_codegen/analyzer/types.py,sha256=HAbEAnxkZxX7d5iFQA6KT0Ocs7kkUprX78z9DMcHVlc,12181
|
|
19
19
|
pineforge_codegen/codegen/__init__.py,sha256=jvwwf93pqyHnFSvGPcD8Mhr_kAk0Uc4W6IqhKu8UIlI,2232
|
|
20
|
-
pineforge_codegen/codegen/base.py,sha256=
|
|
21
|
-
pineforge_codegen/codegen/emit_top.py,sha256=
|
|
22
|
-
pineforge_codegen/codegen/helpers.py,sha256=
|
|
20
|
+
pineforge_codegen/codegen/base.py,sha256=byvd8tReKtaKmJjhL9P9BwJqhXFoCBugYpzkhAUJUaw,75186
|
|
21
|
+
pineforge_codegen/codegen/emit_top.py,sha256=OLmnhcO69ei4-n8BDewxKXLEYm1pVldc0Lmi811vvYY,49090
|
|
22
|
+
pineforge_codegen/codegen/helpers.py,sha256=gtm205xciMW326EQyxLy1mjeQ_5ZNP_t8hWE9zmE_0I,8723
|
|
23
23
|
pineforge_codegen/codegen/helpers_syminfo.py,sha256=GFrx9i2HXSCmGIpIds0cFrtJ6IQiznfXxH8aI8h3DQ4,4878
|
|
24
|
-
pineforge_codegen/codegen/input.py,sha256=
|
|
25
|
-
pineforge_codegen/codegen/security.py,sha256=
|
|
26
|
-
pineforge_codegen/codegen/ta.py,sha256=
|
|
27
|
-
pineforge_codegen/codegen/tables.py,sha256=
|
|
28
|
-
pineforge_codegen/codegen/types.py,sha256=
|
|
29
|
-
pineforge_codegen/codegen/visit_call.py,sha256=
|
|
30
|
-
pineforge_codegen/codegen/visit_expr.py,sha256=
|
|
31
|
-
pineforge_codegen/codegen/visit_stmt.py,sha256=
|
|
32
|
-
pineforge_codegen-0.
|
|
33
|
-
pineforge_codegen-0.
|
|
34
|
-
pineforge_codegen-0.
|
|
35
|
-
pineforge_codegen-0.
|
|
24
|
+
pineforge_codegen/codegen/input.py,sha256=5NNzkZWMt7yTvT0_68Hm8okkbrtP2w8TFQ_iRI2VVOc,16822
|
|
25
|
+
pineforge_codegen/codegen/security.py,sha256=sN8pHHmqgosggYysY9pwZfBH7QRrHsojMyIE22eejlw,69115
|
|
26
|
+
pineforge_codegen/codegen/ta.py,sha256=tXRATmRzqbXtH0PU9ZpTM-cmKlH6RhQ1_xBU6NlAQ54,13062
|
|
27
|
+
pineforge_codegen/codegen/tables.py,sha256=iTNtwEkHhZI2keGUS89rVySE4LuvkBHcfYTG-QG8U0Y,36297
|
|
28
|
+
pineforge_codegen/codegen/types.py,sha256=m87uBmJ8zVOt3v6qBjhV7yfXuTTyuCu-gXkhTl8Koco,30603
|
|
29
|
+
pineforge_codegen/codegen/visit_call.py,sha256=8JWkMbxhaavqGmXpIO7B9D33wc-ePh3GpPq1YrIASnE,75592
|
|
30
|
+
pineforge_codegen/codegen/visit_expr.py,sha256=AxcESavP6WWSTbI1STdCOSha_BoLyR3MuJv8aqHOIA8,38901
|
|
31
|
+
pineforge_codegen/codegen/visit_stmt.py,sha256=xnpXJEBpf24FBi5Ey2I_SgIzSjHe84nbP1SYIYzoMl4,39268
|
|
32
|
+
pineforge_codegen-0.8.0.dist-info/METADATA,sha256=YLX1nKMm5Ea18Qcb1c4XDAzxh9Q0iPSd7vYEUSTbRSc,17599
|
|
33
|
+
pineforge_codegen-0.8.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
34
|
+
pineforge_codegen-0.8.0.dist-info/licenses/LICENSE,sha256=Hf1kZ8OCaQ-nd2i92f2WEX1ZKCc6jqe-rtR4fVENQHY,7186
|
|
35
|
+
pineforge_codegen-0.8.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|