@pineforge/codegen-pyodide 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -258,6 +258,10 @@ class StmtVisitor:
258
258
  # Global-scope non-var vars are class members — emit assignment, not declaration
259
259
  is_global_member = node.name in self._global_member_vars
260
260
 
261
+ def remember_local_type(cpp_type: str | None) -> None:
262
+ if cpp_type and not is_global_member:
263
+ self._current_func_local_types[node.name] = cpp_type
264
+
261
265
  # Check if it is a static (non-series) global member variable already evaluated inside _inputs_initialized_ block
262
266
  is_static_global_input = False
263
267
  if is_global_member and isinstance(node.value, FuncCall) and self._is_input_call(node.value):
@@ -284,7 +288,7 @@ class StmtVisitor:
284
288
  title = self._get_input_title(node.value, var_name=node.name)
285
289
  cpp_val = self._render_input_value(node.value, func_name_i, namespace_i, title)
286
290
  if node.name in self.ctx.series_vars:
287
- lines.append(f"{pad}{safe}.push({cpp_val});")
291
+ self._emit_history_series_write(lines, pad, safe, cpp_val)
288
292
  elif is_global_member:
289
293
  lines.append(f"{pad}{safe} = {cpp_val};")
290
294
  else:
@@ -381,9 +385,12 @@ class StmtVisitor:
381
385
  compute_args = self._ta_compute_args_for_site(site)
382
386
  ret_type = "bool" if self._ta_name_from_site(site) in TA_RETURNS_BOOL else "double"
383
387
  ta_name = self._ta_member_name(site)
384
- ta_expr = f"(is_first_tick_ ? {ta_name}.compute({compute_args}) : {ta_name}.recompute({compute_args}))"
388
+ ta_expr = (
389
+ f"(history_advances_new_bar() ? {ta_name}.compute({compute_args}) "
390
+ f": {ta_name}.recompute({compute_args}))"
391
+ )
385
392
  if node.name in self.ctx.series_vars:
386
- lines.append(f"{pad}{safe}.push({ta_expr});")
393
+ self._emit_history_series_write(lines, pad, safe, ta_expr)
387
394
  elif is_global_member:
388
395
  lines.append(f"{pad}{safe} = {ta_expr};")
389
396
  else:
@@ -393,7 +400,7 @@ class StmtVisitor:
393
400
  # Non-var series variable — push instead of declare
394
401
  if node.name in self.ctx.series_vars:
395
402
  cpp_val = self._visit_expr(node.value)
396
- lines.append(f"{pad}{safe}.push({cpp_val});")
403
+ self._emit_history_series_write(lines, pad, safe, cpp_val)
397
404
  return
398
405
 
399
406
  # If/switch expression: x = if cond ... else ...
@@ -451,12 +458,29 @@ class StmtVisitor:
451
458
  lines.append(f"{pad}{cpp_type}& {safe} = {cpp_val};")
452
459
  return
453
460
 
461
+ # Global-scope UDT array-element alias (BUG: Pine array elements of a
462
+ # user-defined type are references). A global loop-local bound from
463
+ # ``arr.get(i)`` and later field-mutated must ALIAS the element so the
464
+ # mutation writes back into the array. Emit a fresh per-iteration
465
+ # ``UDT& z = arr[i];`` reference (any hoisted class member was
466
+ # suppressed) — identical to the non-hoisted function-local alias path.
467
+ # Gated by _register_udt_array_get_ref_locals (which only records global-
468
+ # scope names); the ``_current_func_body is None`` guard confines this to
469
+ # global scope, so a function-local sharing the name keeps its own path.
470
+ if (node.name in self._udt_array_get_ref_locals
471
+ and getattr(self, "_current_func_body", None) is None):
472
+ udt_t = self._udt_var_types.get(node.name)
473
+ cpp_val = self._visit_rhs_value(node.value, node.name, target_cpp_type=udt_t)
474
+ lines.append(f"{pad}{udt_t}& {safe} = {cpp_val};")
475
+ return
476
+
454
477
  # General declaration
455
478
  cpp_type = self._type_for_decl(node) if not is_global_member else None
456
479
  cpp_val = self._visit_rhs_value(node.value, node.name, target_cpp_type=cpp_type)
457
480
  if is_global_member:
458
481
  lines.append(f"{pad}{safe} = {cpp_val};")
459
482
  else:
483
+ remember_local_type(cpp_type)
460
484
  lines.append(f"{pad}{cpp_type} {safe} = {cpp_val};")
461
485
 
462
486
  @staticmethod
@@ -591,7 +615,11 @@ class StmtVisitor:
591
615
  compute_args = self._ta_compute_args_for_site(site)
592
616
  ta_mem = self._ta_member_name(site)
593
617
  result_var = f"_result_{ta_mem}"
594
- lines.append(f"{pad}auto {result_var} = (is_first_tick_ ? {ta_mem}.compute({compute_args}) : {ta_mem}.recompute({compute_args}));")
618
+ lines.append(
619
+ f"{pad}auto {result_var} = (history_advances_new_bar() ? "
620
+ f"{ta_mem}.compute({compute_args}) : "
621
+ f"{ta_mem}.recompute({compute_args}));"
622
+ )
595
623
 
596
624
  ta_name = self._ta_name_from_site(site)
597
625
  fields = TA_TUPLE_FIELDS.get(ta_name, [f"field{i}" for i in range(len(node.names))])
@@ -611,7 +639,9 @@ class StmtVisitor:
611
639
  # destructured names keep the plain scalar declaration.
612
640
  if name in self.ctx.series_vars:
613
641
  safe = self._safe_name(name)
614
- lines.append(f"{pad}{safe}.push({field_expr});")
642
+ self._emit_history_series_write(
643
+ lines, pad, safe, field_expr
644
+ )
615
645
  else:
616
646
  lines.append(f"{pad}double {name} = {field_expr};")
617
647
  return
@@ -716,40 +746,37 @@ class StmtVisitor:
716
746
  pad = " " * indent
717
747
  start = self._visit_expr(node.start)
718
748
  end = self._visit_expr(node.end)
719
- var = node.var # new AST uses .var instead of .var_name
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
- )
749
+ step = self._visit_expr(node.step) if node.step is not None else "1"
750
+ var = self._safe_name(node.var) # new AST uses .var instead of .var_name
751
+
752
+ # Pine infers loop direction from the initial ``from``/``to`` values for
753
+ # both implicit and explicit ``by`` loops. The ``by`` value is a positive
754
+ # magnitude; descending loops subtract it. ``to`` can change during the
755
+ # loop, so refresh the cached end expression after each iteration while
756
+ # keeping the initial direction and step fixed.
757
+ fid = self._for_counter
758
+ self._for_counter += 1
759
+ s_var = f"_for_start_{fid}"
760
+ e_var = f"_for_end_{fid}"
761
+ step_var = f"_for_step_{fid}"
762
+ down_var = f"_for_down_{fid}"
763
+ lines.append(f"{pad}int {s_var} = ({start});")
764
+ lines.append(f"{pad}int {e_var} = ({end});")
765
+ lines.append(f"{pad}int {step_var} = ({step});")
766
+ lines.append(f"{pad}if ({step_var} < 0) {step_var} = -{step_var};")
767
+ lines.append(f"{pad}if ({step_var} == 0) {step_var} = 1;")
768
+ lines.append(f"{pad}const bool {down_var} = ({s_var} > {e_var});")
769
+ lines.append(
770
+ f"{pad}for (int {var} = {s_var}; "
771
+ f"({down_var} ? ({var} >= {e_var}) : ({var} <= {e_var})); "
772
+ f"{var} += ({down_var} ? -{step_var} : {step_var}), {e_var} = ({end})) {{"
773
+ )
747
774
  # Register the loop counter so reads of it inside the body resolve (the
748
775
  # unknown-identifier guard in _visit_ident would otherwise flag it).
749
776
  saved_loop = self._current_loop_vars
750
777
  self._current_loop_vars = set(self._current_loop_vars)
751
- if var:
752
- self._current_loop_vars.add(var)
778
+ if node.var:
779
+ self._current_loop_vars.add(node.var)
753
780
  _blk_saved = self._push_block_var_remap(node)
754
781
  try:
755
782
  for s in node.body:
@@ -787,9 +814,19 @@ class StmtVisitor:
787
814
  pad = " " * indent
788
815
  iterable = self._visit_expr(node.iterable)
789
816
  saved_loop = self._current_loop_vars
817
+ saved_loop_specs = self._current_loop_var_specs
790
818
  self._current_loop_vars = set(self._current_loop_vars)
819
+ self._current_loop_var_specs = dict(self._current_loop_var_specs)
820
+ iterable_spec = self._type_spec_from_expr(node.iterable)
821
+ elem_spec = (
822
+ iterable_spec.element
823
+ if iterable_spec is not None and iterable_spec.kind == "array"
824
+ else None
825
+ )
791
826
  if node.var:
792
827
  self._current_loop_vars.add(node.var)
828
+ if elem_spec is not None:
829
+ self._current_loop_var_specs[node.var] = elem_spec
793
830
  if node.vars:
794
831
  for v in node.vars:
795
832
  if v != "_":
@@ -809,6 +846,7 @@ class StmtVisitor:
809
846
  self._pop_block_var_remap(_blk_saved)
810
847
  lines.append(f"{pad}}}")
811
848
  self._current_loop_vars = saved_loop
849
+ self._current_loop_var_specs = saved_loop_specs
812
850
 
813
851
  def _visit_while(self, node: WhileStmt, lines: list[str], indent: int) -> None:
814
852
  pad = " " * indent
@@ -166,6 +166,28 @@ class Parser:
166
166
  # ------------------------------------------------------------------
167
167
 
168
168
  def _parse_statement(self):
169
+ stmt = self._parse_single_statement()
170
+ if not self._check(TokenType.COMMA):
171
+ return stmt
172
+
173
+ stmts: list = []
174
+ self._extend_statement_list(stmts, stmt)
175
+ while self._match(TokenType.COMMA):
176
+ if self._check(TokenType.NEWLINE) or self._check(TokenType.DEDENT) or self._at_end():
177
+ break
178
+ self._extend_statement_list(stmts, self._parse_single_statement())
179
+ return stmts
180
+
181
+ @staticmethod
182
+ def _extend_statement_list(stmts: list, stmt) -> None:
183
+ if stmt is None:
184
+ return
185
+ if isinstance(stmt, list):
186
+ stmts.extend(stmt)
187
+ else:
188
+ stmts.append(stmt)
189
+
190
+ def _parse_single_statement(self):
169
191
  cur = self._current()
170
192
 
171
193
  # Control flow keywords
@@ -192,6 +214,33 @@ class Parser:
192
214
  if cur.type in (TokenType.VAR, TokenType.VARIP):
193
215
  return self._parse_var_keyword_decl()
194
216
 
217
+ # Type-qualifier prefix on a top-level declaration. Pine v6 allows
218
+ # ``const`` / ``simple`` / ``series`` to qualify a typed variable
219
+ # declaration (``const int DEFAULT = 1``). The qualifier carries no
220
+ # C++-side meaning (the engine infers mutability from ``var``/
221
+ # ``varip``), so consume it and parse the underlying declaration.
222
+ # Without this the qualifier is parsed as a bare identifier and the
223
+ # subsequent typed decl leaks the stray ``ExprStmt(const)`` that
224
+ # produces "Unknown variable 'const'" at codegen.
225
+ if (cur.type == TokenType.IDENT
226
+ and cur.value in ("const", "series", "simple")):
227
+ nxt = self._peek()
228
+ # ``const int NAME = ...`` / ``const float[] x = ...``
229
+ typed_after_qual = (
230
+ nxt.type in TYPE_KEYWORDS
231
+ or (nxt.type == TokenType.IDENT and self._is_ident_typed_var_decl(offset=1))
232
+ )
233
+ # ``const NAME = ...`` (qualifier on an untyped decl — rare but
234
+ # legal; ``const`` here is just dropped).
235
+ bare_after_qual = (
236
+ nxt.type == TokenType.IDENT
237
+ and self._peek(2).type == TokenType.EQUALS
238
+ and self._peek(3).type != TokenType.EQUALS
239
+ )
240
+ if typed_after_qual or bare_after_qual:
241
+ self._advance() # consume the qualifier prefix
242
+ return self._parse_single_statement()
243
+
195
244
  # Type-annotated declaration: float x = ..., int x = ...
196
245
  if cur.type in TYPE_KEYWORDS and self._peek().type == TokenType.IDENT:
197
246
  # Check that the IDENT is followed by = (not == ) to confirm declaration
@@ -273,7 +322,7 @@ class Parser:
273
322
 
274
323
  return self._set_loc(ExprStmt(expr=expr), start_tok)
275
324
 
276
- def _is_ident_typed_var_decl(self) -> bool:
325
+ def _is_ident_typed_var_decl(self, offset: int = 0) -> bool:
277
326
  """Look ahead for ``IDENT [<...>] IDENT '='`` (UDT-typed declaration).
278
327
 
279
328
  Triggered by ``Sample s = expr`` / ``array<Sample> w = expr`` /
@@ -283,15 +332,21 @@ class Parser:
283
332
  ``true``, ``false``), and function-definition shapes
284
333
  (``IDENT '(' ... ')' '=>'``).
285
334
 
335
+ ``offset`` shifts the starting position (used to look ahead past a
336
+ consumed type-qualifier prefix like ``const``).
337
+
286
338
  Probe: data/validation/udt-method-probe-19-array-of-udt-method.
287
339
  """
288
- cur = self._current()
340
+ base = self.pos + offset
341
+ if base >= len(self.tokens):
342
+ return False
343
+ cur = self.tokens[base]
289
344
  if cur.type != TokenType.IDENT:
290
345
  return False
291
346
  if cur.value in ("enum", "type", "strategy", "indicator", "na", "true", "false"):
292
347
  return False
293
348
  # Skip past optional generic args after the type ident: IDENT [< ... >]
294
- i = self.pos + 1
349
+ i = base + 1
295
350
  if i < len(self.tokens) and self.tokens[i].type == TokenType.LT:
296
351
  depth = 1
297
352
  i += 1
@@ -374,12 +429,24 @@ class Parser:
374
429
  first = VarDecl(name=name_tok.value, value=value)
375
430
  self._set_loc(first, start_tok)
376
431
 
377
- # Check for comma-separated additional declarations: x=1, y=2, z=3
378
- if not self._check(TokenType.COMMA):
432
+ # Check for comma-separated additional declarations: x=1, y=2, z=3.
433
+ # Other comma-separated simple statements (``a := 1, b := 2`` or
434
+ # ``array.fill(a, na), array.set(a, 0, 1)``) are handled by the
435
+ # statement wrapper above, so do not greedily consume their comma.
436
+ if not (
437
+ self._check(TokenType.COMMA)
438
+ and self._peek().type == TokenType.IDENT
439
+ and self._peek(2).type == TokenType.EQUALS
440
+ ):
379
441
  return first
380
442
 
381
443
  decls = [first]
382
- while self._match(TokenType.COMMA):
444
+ while (
445
+ self._check(TokenType.COMMA)
446
+ and self._peek().type == TokenType.IDENT
447
+ and self._peek(2).type == TokenType.EQUALS
448
+ ):
449
+ self._advance()
383
450
  st = self._current()
384
451
  n = self._consume(TokenType.IDENT)
385
452
  self._consume(TokenType.EQUALS)
@@ -133,8 +133,8 @@ _ta("kcw", _sig([("source", F), ("length", I), ("mult", F, 1.5)]))
133
133
  # --- Volatility & Range ---
134
134
  _ta("atr", _sig([("length", I)]))
135
135
  _ta("tr", _sig([("handle_na", B, False)]))
136
- _ta("stdev", _sig([("source", F), ("length", I)]))
137
- _ta("variance",_sig([("source", F), ("length", I)]))
136
+ _ta("stdev", _sig([("source", F), ("length", I), ("biased", B, True)]))
137
+ _ta("variance",_sig([("source", F), ("length", I), ("biased", B, True)]))
138
138
 
139
139
  # --- Trend ---
140
140
  _ta("supertrend", _sig([("factor", F), ("atrPeriod", I)],
@@ -342,10 +342,14 @@ UNSUPPORTED_NAMESPACE_VARS: dict[str, str] = {
342
342
  # request.security parameter rules.
343
343
  # Codegen supports symbol/timeframe/expression plus gaps/lookahead (read in
344
344
  # _eval_security_* emission and forwarded to register_security_eval).
345
- # ignore_invalid_symbol/currency are accepted by the parser but silently
346
- # dropped by codegen — reject loudly to surface unsupported behavior.
345
+ # `currency` is still rejected loudly (it changes the returned values via FX
346
+ # conversion, which codegen drops — silently wrong). `ignore_invalid_symbol` is
347
+ # a guaranteed no-op here: codegen forces request.security onto the current
348
+ # chart symbol (see the "symbol must reference current chart symbol" check),
349
+ # which is always valid, so the flag can never change the result — accept+ignore.
347
350
  SECURITY_ALLOWED_PARAMS: frozenset[str] = frozenset(
348
351
  {"symbol", "timeframe", "expression", "gaps", "lookahead",
352
+ "ignore_invalid_symbol",
349
353
  # Data-adjustment constants — silently accepted and ignored by codegen;
350
354
  # the underlying engine uses a fixed unadjusted data source.
351
355
  "backadjustment", "settlement_as_close", "adjustment"}
@@ -462,6 +466,12 @@ class SupportChecker:
462
466
  # (``panel.cell(...)``) is a visual sink whose args may carry visual
463
467
  # constants, so it routes through ``_visit_children_const_ok``.
464
468
  self._visual_container_vars: set[str] = set()
469
+ # User helpers whose body is only a visual sink (for example
470
+ # ``cell(table t, ..., align) => t.cell(..., text_halign=align)``).
471
+ # A call to such a helper is itself a visual context, so style
472
+ # constants passed through its arguments are safe and should not trip
473
+ # the free-expression constant-namespace rejection.
474
+ self._visual_sink_funcs: set[str] = set()
465
475
  self._drawing_tuple_vars: set[str] = set()
466
476
  self._func_tuple_drawing_returns: dict[str, list[bool]] = {}
467
477
  # Track whether we are inside an if/ternary condition expression.
@@ -520,6 +530,8 @@ class SupportChecker:
520
530
  if tuple_returns:
521
531
  self._func_tuple_drawing_returns[stmt.name] = tuple_returns
522
532
  self._collect_visual_container_params(stmt)
533
+ if self._func_body_is_visual_sink(stmt):
534
+ self._visual_sink_funcs.add(stmt.name)
523
535
  elif isinstance(stmt, MethodDef):
524
536
  self._user_methods.add(stmt.name)
525
537
  self._collect_visual_container_params(stmt)
@@ -535,6 +547,27 @@ class SupportChecker:
535
547
  if hint and str(hint).replace(" ", "") in _VISUAL_CONTAINER_TYPES:
536
548
  self._visual_container_vars.add(pname)
537
549
 
550
+ def _expr_is_visual_sink_call(self, expr: ASTNode | None) -> bool:
551
+ if not isinstance(expr, FuncCall):
552
+ return False
553
+ ns, name = _qualified_name(expr.callee)
554
+ if ns is None and name in SKIP_FUNC_NAMES:
555
+ return True
556
+ if ns is not None and ns in SKIP_NAMESPACES:
557
+ return True
558
+ if ns in _DRAWING_NOOP_BY_NS and name in _DRAWING_NOOP_BY_NS[ns]:
559
+ return True
560
+ return ns is not None and ns in self._visual_container_vars
561
+
562
+ def _func_body_is_visual_sink(self, fn: FuncDef | MethodDef) -> bool:
563
+ if not fn.body:
564
+ return False
565
+ for stmt in fn.body:
566
+ expr = stmt.expr if isinstance(stmt, ExprStmt) else None
567
+ if not self._expr_is_visual_sink_call(expr):
568
+ return False
569
+ return True
570
+
538
571
  @staticmethod
539
572
  def _type_name_contains_drawing(type_name: str | None) -> bool:
540
573
  if not type_name:
@@ -1207,6 +1240,10 @@ class SupportChecker:
1207
1240
  self._visit_children_const_ok(node)
1208
1241
  return
1209
1242
 
1243
+ if ns is None and name in self._visual_sink_funcs:
1244
+ self._visit_children_const_ok(node)
1245
+ return
1246
+
1210
1247
  self._visit_children(node)
1211
1248
 
1212
1249
  def _visit_Identifier(self, node: Identifier) -> None:
Binary file
package/release.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "codegen": "0.8.1",
2
+ "codegen": "0.9.0",
3
3
  "pyodide": "314.0.0",
4
4
  "python": "3.14.0",
5
5
  "emscripten": "emscripten_5_0_3",
6
- "sha256": "85ac44383967dd8c9825a276661eab568b16ecb89fcb929a3b9b638b6a4f371c"
6
+ "sha256": "fc828c2a32ba93bbeeffa8e0a4c4696a417a6cc64448c1b70bdce2267cd051d9"
7
7
  }
package/tables.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "CODEGEN_VERSION": "0.8.1",
2
+ "CODEGEN_VERSION": "0.9.0",
3
3
  "TA_CLASS_MAP_KEYS": [
4
4
  "sma",
5
5
  "ema",
@@ -577,11 +577,13 @@
577
577
  ],
578
578
  "ta.stdev": [
579
579
  "source",
580
- "length"
580
+ "length",
581
+ "biased"
581
582
  ],
582
583
  "ta.variance": [
583
584
  "source",
584
- "length"
585
+ "length",
586
+ "biased"
585
587
  ],
586
588
  "ta.supertrend": [
587
589
  "factor",
@@ -1574,6 +1576,7 @@
1574
1576
  "backadjustment",
1575
1577
  "expression",
1576
1578
  "gaps",
1579
+ "ignore_invalid_symbol",
1577
1580
  "lookahead",
1578
1581
  "settlement_as_close",
1579
1582
  "symbol",
Binary file