Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 45 additions & 13 deletions pineforge_codegen/analyzer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,19 @@ def __init__(self, ast: Program, filename: str = "<stdin>") -> None:
self._global_var_decls: list[tuple[str, PineType]] = []
self._global_expr_map: dict[str, Any] = {}
self._var_member_init_exprs: dict[str, Any] = {}
# Exact declaration-node ownership for persistent vars. The member
# name can differ from ``node.name`` when sibling blocks declare the
# same raw identifier, so codegen must not reconstruct this mapping
# from names when it emits declaration-site initialization.
self._var_member_metadata_by_node: dict[int, tuple] = {}
# Block-scoped ``var``/``varip`` name-collision disambiguation.
# Two same-named block-scoped vars in SIBLING non-global, non-function
# scopes (e.g. ``var bool valid`` declared inside ``if A`` and again
# inside ``if B``) would otherwise dedupe to ONE C++ member and
# cross-contaminate. ``_block_node_stack`` tracks the enclosing
# block AST nodes during analysis; ``_block_var_owner`` maps a raw
# block-var name to the id() of the FIRST block that declared it;
# ``_block_var_renames`` maps id(block_node) -> {raw_name: unique}
# branch/loop body owners during analysis; ``_block_var_owner`` maps a
# raw block-var name to the id() of the FIRST body that declared it;
# ``_block_var_renames`` maps id(body_owner) -> {raw_name: unique}
# for every later colliding block so codegen can activate the
# rename via ``_active_var_remap`` while emitting that block.
self._block_node_stack: list[Any] = []
Expand Down Expand Up @@ -341,6 +346,7 @@ def analyze(self) -> AnalyzerContext:
global_var_decls=self._global_var_decls,
global_expr_map=pure_global_expr_map,
var_member_init_exprs=self._var_member_init_exprs,
var_member_metadata_by_node=self._var_member_metadata_by_node,
func_ta_ranges=self._func_ta_ranges,
func_call_cs_map=self._func_call_cs_map,
func_call_site_counts=self._func_call_site_count,
Expand Down Expand Up @@ -1277,6 +1283,16 @@ def _visit_VarDecl(self, node: VarDecl) -> PineType:
member_name = f"{node.name}__blk{self._block_var_seq}"
self._block_var_renames.setdefault(block_id, {})[node.name] = member_name
self._var_members.append((member_name, val_type, init_str))
scope_cursor = self._symbols.current_scope
is_callable_scoped = False
while scope_cursor is not None:
if scope_cursor.name.startswith(("func_", "method_")):
is_callable_scoped = True
break
scope_cursor = scope_cursor.parent
self._var_member_metadata_by_node[id(node)] = (
node, member_name, val_type, init_str, is_callable_scoped,
)
# Capture the init AST too so codegen can inspect the RHS callee
# (used to detect int64-returning builtins like ``time()`` and
# promote the symbol storage type to ``int64_t``).
Expand Down Expand Up @@ -1675,16 +1691,23 @@ def _visit_MethodDef(self, node) -> PineType:
def _visit_IfStmt(self, node: IfStmt) -> PineType:
old_global = self._global_scope
self._global_scope = False
self._block_node_stack.append(node)
try:
self._visit(node.condition)
body_type = PineType.VOID
for stmt in node.body:
body_type = self._visit(stmt)
for stmt in node.else_body:
self._visit(stmt)
self._block_node_stack.append(node.body)
try:
for stmt in node.body:
body_type = self._visit(stmt)
finally:
self._block_node_stack.pop()
if node.else_body:
self._block_node_stack.append(node.else_body)
try:
for stmt in node.else_body:
self._visit(stmt)
finally:
self._block_node_stack.pop()
finally:
self._block_node_stack.pop()
self._global_scope = old_global
# If used as expression (x = if ...), return last expr type
return body_type
Expand Down Expand Up @@ -1776,10 +1799,19 @@ def _visit_SwitchStmt(self, node: SwitchStmt) -> PineType:
for case_expr, case_body in node.cases:
if case_expr:
self._visit(case_expr)
for stmt in case_body:
result_type = self._visit(stmt)
for stmt in node.default_body:
self._visit(stmt)
self._block_node_stack.append(case_body)
try:
for stmt in case_body:
result_type = self._visit(stmt)
finally:
self._block_node_stack.pop()
if node.default_body:
self._block_node_stack.append(node.default_body)
try:
for stmt in node.default_body:
self._visit(stmt)
finally:
self._block_node_stack.pop()
finally:
self._global_scope = old_global
# If used as expression (x = switch ...), return last expr type
Expand Down
3 changes: 3 additions & 0 deletions pineforge_codegen/analyzer/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ class AnalyzerContext:
global_var_decls: list = field(default_factory=list) # [(name, PineType)] non-var global scope vars
global_expr_map: dict = field(default_factory=dict) # name -> defining AST expr (global, non-var)
var_member_init_exprs: dict = field(default_factory=dict) # var-member name -> init AST expr
# id(VarDecl) -> (node, emitted member name, PineType, rendered initializer,
# is function/method scoped)
var_member_metadata_by_node: dict = field(default_factory=dict)
# Per-call-site TA member cloning for user functions:
func_ta_ranges: dict = field(default_factory=dict) # func_name -> (start_idx, end_idx)
func_call_cs_map: dict = field(default_factory=dict) # call_node_id -> (func_name, call_site_index)
Expand Down
138 changes: 131 additions & 7 deletions pineforge_codegen/codegen/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,114 @@ def __init__(self, ctx: AnalyzerContext) -> None:
# history reads off security-helper series.
self._max_bars_back_cap: int | None = self._compute_max_bars_back_cap()

# Non-series persistent scalars whose initializer cannot run in the
# C++ constructor are initialized at their Pine declaration site. The
# source-order metadata and collision-safe once flags must be prepared
# before function-instance naming and class-member emission begin.
self._prepare_runtime_scalar_var_initializers()

def _scalar_var_init_depends_on_runtime_input(self, init_ast) -> bool:
"""Whether a nominally constant initializer depends on input state.

``_resolve_known`` intentionally folds input defaults for constructor
sizing and other compile-time decisions. A Pine ``var`` initializer,
however, must observe any host override installed before the first
bar. Treat direct input aliases and arithmetic aliases derived from
them as declaration-time expressions even when their default happens
to fold to a C++ literal.
"""
if init_ast is None:
return False
runtime_names = set(self._input_backed_vars) | set(self._derived_input_expr)
for node in self._walk_ast(init_ast):
if isinstance(node, FuncCall) and self._is_input_call(node):
return True
if isinstance(node, Identifier) and node.name in runtime_names:
return True
return False

def _is_runtime_scalar_var_initializer(
self, name: str, ptype, init_str: str, init_ast) -> bool:
"""Return True for a persistent primitive that must init in execution.

Series and aggregate state keep their existing specialized preamble
routes. Function-local vars keep their per-function-variant route. This
predicate only selects primitive global/on-bar-scope members for the
declaration-site once guards prepared below.
"""
if init_ast is None or name in self.ctx.series_vars:
return False
if name in self._visual_drop_vars:
return False
if name in self._array_vars or name in self._map_vars \
or name in self._matrix_specs:
return False
type_spec = self._collection_types.get(name)
if type_spec is not None and type_spec.kind in {
"array", "map", "matrix", "udt",
}:
return False
udt_type = self._udt_var_types.get(name)
if udt_type in self._udt_defs or udt_type in DRAWING_TYPE_TO_CPP:
return False

ctor_val = self._resolve_known(init_str)
ctor_val = self._typed_na_init(ctor_val, name, ptype)
return (
not self._is_compile_time_value(ctor_val)
or self._scalar_var_init_depends_on_runtime_input(init_ast)
)

def _prepare_runtime_scalar_var_initializers(self) -> None:
"""Index declaration-site scalar ``var`` initialization and flags.

The analyzer supplies exact metadata for every VarDecl, including
sibling-block disambiguation and callable ownership. Its insertion
order follows source analysis and reaches declarations nested inside
if/switch expressions, while the ownership bit excludes function and
method bodies. This lets emission preserve ordinary dependency order
and Pine's lazy first-entry semantics for conditional declarations.
"""
self._runtime_scalar_var_init_by_node: dict[int, dict] = {}
self._runtime_scalar_var_init_members: set[str] = set()

used_names = set(self._all_member_names)
# ``_all_member_names`` historically covers persistent ``var`` and
# Series members only. Plain global declarations are class members as
# well, so include them before minting a generated flag; otherwise a
# legal user binding such as ``_pf_var_init_seeded = 1`` can collide
# with the flag for ``var seeded = low``.
used_names.update(
self._safe_name(name) for name, _ptype in self.ctx.global_var_decls
)
metadata_by_node = getattr(
self.ctx, "var_member_metadata_by_node", {}
) or {}
for node_id, meta in metadata_by_node.items():
stmt, member_name, ptype, init_str, is_callable_scoped = meta
if is_callable_scoped:
continue
if not isinstance(stmt, VarDecl) or not (stmt.is_var or stmt.is_varip):
continue
if not self._is_runtime_scalar_var_initializer(
member_name, ptype, init_str, stmt.value):
continue

base_flag = f"_pf_var_init_{self._safe_name(member_name)}"
flag = base_flag
suffix = 2
while flag in used_names:
flag = f"{base_flag}_{suffix}"
suffix += 1
used_names.add(flag)
self._all_member_names.add(flag)
self._runtime_scalar_var_init_members.add(member_name)
self._runtime_scalar_var_init_by_node[node_id] = {
"member_name": member_name,
"ptype": ptype,
"flag": flag,
}

# ------------------------------------------------------------------
# Context-sensitive (call-path) instance machinery
# ------------------------------------------------------------------
Expand Down Expand Up @@ -1099,7 +1207,7 @@ def _walk_global_scope_with_loopflag(self, stmts, in_loop):
a separate scope and must not be attributed to a global member. The
``in_loop`` flag is True once inside any for/while loop body."""
for s in stmts:
if isinstance(s, FuncDef):
if isinstance(s, (FuncDef, MethodDef)):
continue
yield s, in_loop
child_in_loop = in_loop or isinstance(s, (ForStmt, ForInStmt, WhileStmt))
Expand Down Expand Up @@ -2408,7 +2516,16 @@ def generate(self) -> str:
if name in self.ctx.series_vars:
lines.append(f" Series<{cpp_type}> {safe}{_mbb};")
else:
lines.append(f" {cpp_type} {safe};")
if name in self._runtime_scalar_var_init_members:
# A conditional declaration may not execute for many bars,
# while COOF rollback still value-copies every member. Give
# pending runtime vars a typed Pine-na sentinel so that copy
# is always defined; the declaration-site guard overwrites
# it on the first actual execution.
pending = self._typed_na_init("na<double>()", name, ptype)
lines.append(f" {cpp_type} {safe} = {pending};")
else:
lines.append(f" {cpp_type} {safe};")

# 6. Non-var series vars
for name in sorted(self.ctx.series_vars):
Expand Down Expand Up @@ -2532,7 +2649,14 @@ def generate(self) -> str:
if self.ctx.var_members:
lines.append(" bool _var_initialized = false;")

# 9a. Per-function-variant ``var`` init flags. A function-scoped
# 9a. Per-member flags for primitive runtime ``var`` / ``varip``
# initializers. Unlike the global aggregate/Series latch above, these
# live at the declaration site so prior statements are available and a
# conditional declaration initializes on its first actual execution.
for info in self._runtime_scalar_var_init_by_node.values():
lines.append(f" bool {info['flag']} = false;")

# 9b. Per-function-variant ``var`` init flags. A function-scoped
# ``var`` (Pine "init once" semantics) is a function-local static:
# its initializer runs on the FIRST call to that function variant
# (with the first bar's values the function actually sees) and the
Expand All @@ -2550,21 +2674,21 @@ def generate(self) -> str:
else:
lines.append(f" bool _fvinit_{self._func_safe_name(fi.name)} = false;")

# 9a2. ``var`` init flags for fresh context-sensitive helper instances.
# 9b2. ``var`` init flags for fresh context-sensitive helper instances.
for inst in self._fresh_instances:
if inst["fname"] in self.ctx.func_var_members and inst["var_remap"]:
lines.append(f" bool _fvinit_{inst['name']} = false;")

# 9b. _ta_initialized_ flag for runtime TA re-sizing (first on_bar only).
# 9c. _ta_initialized_ flag for runtime TA re-sizing (first on_bar only).
if self.ctx.ta_call_sites:
lines.append(" bool _ta_initialized_ = false;")

# 9c. _inputs_initialized_ flag for cached global inputs.
# 9d. _inputs_initialized_ flag for cached global inputs.
lines.append(" bool _inputs_initialized_ = false;")

lines.append("")

# 9d. Historical execution rollback checkpoint. Derive the member
# 9e. Historical execution rollback checkpoint. Derive the member
# inventory from the declarations above so every future generated
# state category is captured automatically (or generation fails loudly
# if it introduces an unfamiliar declaration form).
Expand Down
5 changes: 5 additions & 0 deletions pineforge_codegen/codegen/emit_top.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,11 @@ def _emit_constructor(self, lines: list[str]) -> None:
# own scope (with its active var remap for clones).
if name in getattr(self, "_func_local_var_names", ()):
continue
# Runtime primitive initializers execute at their Pine declaration
# site under a per-member once flag. This preserves source-order
# dependencies and first-entry semantics for conditional blocks.
if name in self._runtime_scalar_var_init_members:
continue
# UDT-typed var members (``var SDZone z = na``) default-construct to
# na via the struct's in-class ``__pf_na = true``; a ctor init like
# ``z(na<double>())`` would not type-match the struct member.
Expand Down
Loading
Loading