Skip to content
Closed
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
22 changes: 19 additions & 3 deletions pineforge_codegen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from .codegen import CodeGen
from .cpp_allowlist import REASONS, check_generated_cpp
from .diagnostic_codes import diagnostics_catalog, render_diagnostic
from .errors import CompileError, Diagnostic, Level, Phase, SourceLocation
from .errors import (
CodegenInvariantError, CompileError, Diagnostic, Level, Phase, SourceLocation,
)
from .external_requests import lower_no_data_requests
from .builtin_keywords import bind_builtin_keywords
from .finite_ta_length import expand_finite_choice_extrema_lengths
Expand Down Expand Up @@ -105,8 +107,22 @@ def _generate(pine_source: str, check_support: bool, filename: str,
# consumes them directly from the context to emit the on_bar tail
# ``if (trace_enabled_) { trace(...); ... }`` block.
ctx.pf_trace_pragmas = pragmas
gen = CodeGen(ctx, budget=budget)
cpp = gen.generate()
try:
gen = CodeGen(ctx, budget=budget)
cpp = gen.generate()
except CodegenInvariantError as error:
node_kind = error.node_kind
location = getattr(error.node, "loc", None)
if location is None:
raise RuntimeError(
f"Internal error: AST node '{node_kind}' has no source "
"location for its missing value lowering."
) from error
raise CompileError([Diagnostic(
level=Level.ERROR, phase=Phase.CODEGEN, location=location,
message=f"Internal error: no value lowering for AST node '{node_kind}'.",
hint="Report this translator error to PineForge.",
)]) from error
budget.check(phase=Phase.CODEGEN)
apart = gen.block_locals_needing_names - renamed
if apart:
Expand Down
3 changes: 2 additions & 1 deletion pineforge_codegen/analyzer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7959,7 +7959,8 @@ def _visit_MemberAccess(self, node: MemberAccess) -> PineType:

# timeframe.*
if ns == "timeframe":
return PineType.STRING
return sigs.TIMEFRAME_VARIABLES.get(
f"timeframe.{node.member}", PineType.STRING)

# barstate.* (ishistory, isrealtime, islast, isfirst, etc.)
if ns == "barstate":
Expand Down
5 changes: 5 additions & 0 deletions pineforge_codegen/codegen/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ def func_var_storage(owner: str, raw_name: str) -> str:
# string local read with history) live in their own map; the member
# is declared when a payload's helper holds one.
self._security_string_series_declared: bool = False
self._security_persistent_helper_series: set[str] = set()
self._security_string_series: set[str] = set()
# Color var state shares the numeric double map, but reads retain the
# ordinary packed-ARGB int64_t type (including its na sentinel).
Expand Down Expand Up @@ -1268,6 +1269,8 @@ def func_var_storage(owner: str, raw_name: str) -> str:
"symbol_node": item.get("symbol_node"),
"ignore_invalid_node": item.get("ignore_invalid_node"),
})
self._security_selection_local_members = self._security_selection_local_member_names()
self._global_member_vars.difference_update(self._security_selection_local_members)
self._register_global_aggregate_member_types()
self._uses_map = self._detect_map_usage()
self._uses_matrix = self._detect_matrix_usage()
Expand Down Expand Up @@ -5759,6 +5762,8 @@ def _generate_cpp(self) -> str:
# (so user-defined functions can reference them)
seen_global = set()
for name, ptype in self.ctx.global_var_decls:
if name in self._security_selection_local_members:
continue
exact_scalar_override = (
name in self._qualified_func_var_raw_names
and name in self._ordinary_global_binding_names
Expand Down
1 change: 1 addition & 0 deletions pineforge_codegen/codegen/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ def cpp_code_only(cpp: str) -> str:
""".split())
_CPP_TEMPORARY_PATTERN = re.compile(
r"_v\d+|_secval_\d+(?:_v\d+)?|_tuple_result_\d+|_tuple_unused_\d+|__switch_val_\d+"
r"|__selection_value_\d+"
r"|_for_(?:start|end|end_eval)_\d+|_pf_(?:str|array|round)_a\d+"
r"|_pf_every_bar_ta_\d+|_pf_shared_\d+_\d+|__pf_array\w*|__pf_raw_\w+"
r"|_pf_collection_hist_\w+"
Expand Down
1,064 changes: 1,048 additions & 16 deletions pineforge_codegen/codegen/security.py

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pineforge_codegen/codegen/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4447,6 +4447,8 @@ def _infer_type(
last = node.body[-1]
if isinstance(last, ExprStmt):
return self._infer_type(last.expr)
if isinstance(last, (IfStmt, SwitchStmt)):
return self._infer_type(last)
return "double"
if isinstance(node, SwitchStmt):
if self._selection_contains_na(node):
Expand All @@ -4466,6 +4468,8 @@ def _infer_type(
last = case_body[-1]
if isinstance(last, ExprStmt):
return self._infer_type(last.expr)
if isinstance(last, (IfStmt, SwitchStmt)):
return self._infer_type(last)
return "double"
return "double"

Expand Down
93 changes: 90 additions & 3 deletions pineforge_codegen/codegen/visit_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@

from .run_stops import request_stop

from ..errors import Phase
from ..errors import CodegenInvariantError, Phase
from ..symbols import TypeSpec
from ..external_requests import UNPINNED_ANNOTATION
from ..ast_nodes import (
Expand All @@ -98,17 +98,24 @@
BinOp,
BoolLiteral,
ColorLiteral,
ExprStmt,
ForInStmt,
ForStmt,
FuncCall,
Identifier,
IfStmt,
MemberAccess,
NaLiteral,
NumberLiteral,
StrategyDecl,
StringLiteral,
Subscript,
SwitchStmt,
Ternary,
TupleLiteral,
UnaryOp,
VarDecl,
WhileStmt,
)
from .strategy_declaration import DECLARATION_CONSTANTS, NamedConstant
from .helpers import pine_index_int_cast, unary_sign_cpp
Expand Down Expand Up @@ -255,7 +262,7 @@ class ExprVisitor:

def _visit_expr(self, node: ASTNode | None) -> str:
if node is None:
return "/* null */"
raise CodegenInvariantError(node)
if self._budget is not None:
self._budget_visit_count += 1
if self._budget_visit_count % 128 == 0:
Expand Down Expand Up @@ -307,7 +314,87 @@ def _visit_expr(self, node: ASTNode | None) -> str:
)
elems = ", ".join(elems)
return f"std::make_tuple({elems})"
return "/* unknown */"
if isinstance(node, (IfStmt, SwitchStmt)):
return self._selection_value_expr(node)
raise CodegenInvariantError(node)

def _selection_value_expr(self, node: IfStmt | SwitchStmt) -> str:
"""Materialize an admitted block value with the typed statement emitter.

The lambda preserves branch laziness and evaluates a switch selector
once. Its target supplies the same typed missing value as a declaration.
"""
cpp_type, initial = self._selection_expression_slot(node)
target = f"__selection_value_{self._switch_counter}"
self._switch_counter += 1
lines = [
f"([&]() -> {cpp_type} {{",
f" {cpp_type} {target} = {initial};",
]
frame = self._security_fallback_frame
saved_args = None
if getattr(self, "_security_selection_context", None) is not None and frame is not None:
# An inlined helper's statements must sit inside this lambda.
saved_args = frame["args"]
frame["args"] = saved_args[:6] + (lines,)
try:
self._visit_selection_value(
node, target, lines, 1,
target_cpp_type=cpp_type, slot_cpp_type=cpp_type,
)
finally:
if saved_args is not None:
frame["args"] = saved_args
lines.extend((f" return {target};", "}())"))
return "\n".join(lines)

def _selection_expression_slot(self, node) -> tuple[str, str]:
"""The actual result type and missing value of a materialized block."""
pending = [node]
types = []
while pending:
value = pending.pop()
if value is None or self._selection_node_is_na(value):
continue
if isinstance(value, IfStmt):
pending.extend(self._selection_terminal_expr(body)
for body in (value.body, value.else_body))
continue
if isinstance(value, SwitchStmt):
pending.extend(self._selection_terminal_expr(body)
for _, body in value.cases)
pending.append(self._selection_terminal_expr(value.default_body))
continue
if isinstance(value, (ForStmt, ForInStmt, WhileStmt)):
pending.append(self._selection_terminal_expr(value.body))
continue
if isinstance(value, TupleLiteral):
elements = self._infer_selection_tuple_types(node, len(value.elements))
return (f"std::tuple<{', '.join(elements)}>",
self._tuple_default_expr(elements))
if isinstance(value, FuncCall) and isinstance(value.callee, Identifier):
info = self._func_info_map.get(value.callee.name)
if info is not None and info.returns_tuple:
elements = self._infer_selection_tuple_types(
node, info.tuple_element_count,
)
return (f"std::tuple<{', '.join(elements)}>",
self._tuple_default_expr(elements))
spec = self._type_spec_from_expr(value)
types.append(self._type_spec_to_cpp(spec)
if spec is not None and spec.kind != "primitive"
else self._tail_value_cpp_type(
value if isinstance(value, (Assignment, VarDecl))
else ExprStmt(expr=value))
or self._infer_type(value))
if types and set(types) <= {"int", "int64_t", "double"}:
cpp_type = ("double" if "double" in types else
"int64_t" if "int64_t" in types else "int")
else:
cpp_type = types[0] if types else self._infer_type(node)
initial = (self._na_value_for_type(cpp_type)
if cpp_type in self._UNMATCHED_NA_SLOTS else f"{cpp_type}{{}}")
return cpp_type, initial

# A ``?:`` chain continued in its false arms (``a ? x : b ? y : z``) of at
# least this many links is emitted without the per-link brackets: they add
Expand Down
22 changes: 22 additions & 0 deletions pineforge_codegen/codegen/visit_stmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ def _visit_stmt(self, node: ASTNode, lines: list[str], indent: int) -> None:
if self._budget_visit_count % 128 == 0:
self._budget.check(node.loc, Phase.CODEGEN)
pad = " " * indent
if self._emit_security_selection_stmt(node, lines, pad):
return

if isinstance(node, StrategyDecl):
return
Expand Down Expand Up @@ -1673,10 +1675,14 @@ def _visit_block_statements(self, body: list, lines: list[str],
indent: int) -> None:
"""Emit one lexical branch/loop body with its exact var remap."""
saved = self._push_block_var_remap(body)
scope = (self._security_open_scope()
if getattr(self, "_security_selection_context", None) is not None else None)
try:
for stmt in body:
self._visit_stmt(stmt, lines, indent)
finally:
if scope is not None:
self._security_scope_path = scope
self._pop_block_var_remap(saved)

def _emit_block_with_assign(
Expand All @@ -1689,6 +1695,8 @@ def _emit_block_with_assign(
) -> None:
"""Expression-body counterpart of :meth:`_visit_block_statements`."""
saved = self._push_block_var_remap(body)
scope = (self._security_open_scope()
if getattr(self, "_security_selection_context", None) is not None else None)
try:
self._emit_body_with_assign(
body,
Expand All @@ -1698,6 +1706,8 @@ def _emit_block_with_assign(
target_cpp_type=target_cpp_type,
)
finally:
if scope is not None:
self._security_scope_path = scope
self._pop_block_var_remap(saved)

def _visit_if(self, node: IfStmt, lines: list[str], indent: int) -> None:
Expand Down Expand Up @@ -2411,6 +2421,18 @@ def _emit_body_with_assign(
for i, stmt in enumerate(body):
if i == len(body) - 1:
# Last statement — try to turn into assignment
context = getattr(self, "_security_selection_context", None)
if (context is not None and isinstance(stmt, VarDecl)
and id(stmt) in context["declarations"]):
self._visit_stmt(stmt, lines, indent)
name = context["declarations"][id(stmt)]
cpp = (self._security_helper_series_value(name, "0")
if stmt.is_var or stmt.is_varip else name)
cpp = self._coerce_int_slot(
cpp, stmt.value, self._int_slot_cpp_type(None, target_cpp_type))
lines.append(
f"{' ' * indent}{self._array_journal_initializer(target, cpp)}")
return
if isinstance(stmt, ExprStmt):
# Check if it's a skip expr
if self._is_skip_expr(stmt.expr):
Expand Down
Loading
Loading