Skip to content

Instantly share code, notes, and snippets.

@elbakramer
Created July 22, 2025 22:46
Show Gist options
  • Save elbakramer/7c9dc0209c64a92acb5e9006ac2bd412 to your computer and use it in GitHub Desktop.
Save elbakramer/7c9dc0209c64a92acb5e9006ac2bd412 to your computer and use it in GitHub Desktop.
from __future__ import annotations
import ast as pyast
import contextlib
import dataclasses
import typing
from collections import ChainMap
from typing import ClassVar
import click
import pynescript.ast as pyneast
from pynescript.ast import NodeVisitor
class PinescriptToPynecoreScriptTransformer(NodeVisitor):
_primitive_types: ClassVar[set[str]] = {
"int",
"float",
"bool",
"string",
}
_casting_types: ClassVar[set[str]] = {
"int",
"float",
"bool",
"color",
"string",
"line",
"linefill",
"label",
"box",
"table",
}
_python_type_to_pine_type_mapping: ClassVar[dict[type, str]] = {
int: "int",
float: "float",
bool: "bool",
str: "string",
}
_namespace_mapping: ClassVar[dict[str, str]] = {
"str": "string",
}
_pine_to_pyne_type_mapping: ClassVar[dict[str, str]] = {
"int": "int",
"float": "float",
"bool": "bool",
"color": "Color",
"string": "str",
"line": "Line",
"linefill": "LineFill",
"label": "Label",
"box": "Box",
"table": "Table",
"polyline": "Polyline",
"chart.point": "ChartPoint",
"array": "list",
"matrix": "Matrx",
"map": "dict",
}
def __init__(self):
self._inputs = []
self._parsing_type = False
self._variable_types = ChainMap()
@contextlib.contextmanager
def type_parser(self):
self._parsing_type = True
try:
yield
finally:
self._parsing_type = False
def enter_block(self):
self._variable_types = self._variable_types.new_child()
def exit_block(self):
self._variable_types = self._variable_types.parents
@contextlib.contextmanager
def local_block(self):
self.enter_block()
try:
yield
finally:
self.exit_block()
@classmethod
def _init_block_result(cls, func_def: pyast.FunctionDef):
assign = pyast.Assign(
targets=[
pyast.Name(
id="__block_result__",
cxt=pyast.Store(),
)
],
value=pyast.Name(
id="na",
ctx=pyast.Load(),
),
)
return_assigned = pyast.Return(
value=pyast.Name(
id="__block_result__",
ctx=pyast.Load(),
)
)
func_def.body.insert(-1, assign)
func_def.body.append(return_assigned)
@classmethod
def _set_block_result(cls, last_stmt: pyast.For | pyast.While | pyast.If):
inner_last_stmt = last_stmt.orelse[-1] if last_stmt.orelse else last_stmt.body[-1]
if isinstance(inner_last_stmt, pyast.Expr):
inner_last_stmt = pyast.Assign(
targets=[
pyast.Name(
id="__block_result__",
cxt=pyast.Store(),
)
],
value=inner_last_stmt.value,
)
if last_stmt.orelse:
last_stmt.orelse[-1] = inner_last_stmt
else:
last_stmt.body[-1] = inner_last_stmt
elif isinstance(inner_last_stmt, pyast.For | pyast.While | pyast.If):
cls._set_block_result(inner_last_stmt)
@classmethod
def _add_return_stmt(cls, func_def: pyast.FunctionDef):
last_stmt = func_def.body[-1]
if isinstance(last_stmt, pyast.Expr):
value = last_stmt.value
last_stmt = pyast.Return(value=value)
func_def.body[-1] = last_stmt
elif isinstance(last_stmt, pyast.For | pyast.While | pyast.If):
cls._init_block_result(func_def)
cls._set_block_result(last_stmt)
def visit_Script(self, node: pyneast.Script) -> pyast.Module:
body = []
for stmt in node.body:
result = self.visit(stmt)
if result is not None:
if isinstance(result, list):
body.extend(result)
else:
body.append(result)
decl_stmt = body[0]
if not isinstance(decl_stmt, pyast.Expr):
msg = "no declaration statement, first statement is not an expression"
raise ValueError(msg)
if not isinstance(decl_stmt.value, pyast.Call):
msg = "no declaration statement, first statement is not a function call"
raise ValueError(msg)
if not isinstance(decl_stmt.value.func, pyast.Name):
msg = "no declaration statement, function is not simple name"
raise ValueError(msg)
if decl_stmt.value.func.id not in {"indicator", "strategy", "library"}:
msg = f"no declaration statement, function name is not in [indicator, strategy, library] but {decl_stmt.value.func.id}"
raise ValueError(msg)
decl_call = decl_stmt.value
main_decorator = pyast.Call(
func=pyast.Attribute(
value=pyast.Name(
id="script",
ctx=pyast.Load(),
),
attr="indicator",
ctx=pyast.Load(),
),
args=decl_call.args,
keywords=decl_call.keywords,
)
body = body[1:]
docstring = pyast.Expr(value=pyast.Constant(value="\n@pyne\n"))
modules = [
"pynecore",
"pynecore.core.pine_cast",
"pynecore.core.series",
"pynecore.lib",
"pynecore.types",
]
imports = []
for module in modules:
import_stmt = pyast.ImportFrom(
module=module,
names=[pyast.alias(name="*")],
level=0,
)
imports.append(import_stmt)
args = []
defaults = []
for input in self._inputs:
if isinstance(input, pyast.Assign):
arg = input.targets[0].id
arg = pyast.arg(arg=arg)
args.append(arg)
default = input.value
defaults.append(default)
elif isinstance(input, pyast.AnnAssign):
arg = input.target.id
annotation = input.annotation
arg = pyast.arg(arg=arg, annotation=annotation)
args.append(arg)
default = input.value
defaults.append(default)
else:
msg = "unexpected input assignment"
raise RuntimeError(msg)
main_func = pyast.FunctionDef(
name="main",
args=pyast.arguments(
posonlyargs=[],
args=args,
kwonlyargs=[],
kw_defaults=[],
defaults=defaults,
),
body=body,
decorator_list=[main_decorator],
)
return pyast.Module(
body=[docstring, *imports, main_func],
type_ignores=[],
)
def visit_Expression(self, node: pyneast.Expression) -> pyast.Expression:
body = self.visit(node.body)
return pyast.Expression(
body=body,
)
def visit_FunctionDef(self, node: pyneast.FunctionDef) -> pyast.FunctionDef:
with self.local_block():
name = node.name
args_and_defaults = [self.visit(arg) for arg in node.args]
args_and_defaults = typing.cast(list[tuple[pyast.arg, pyast.expr | None]], args_and_defaults)
args = [arg for arg, default in args_and_defaults]
defaults = [default for arg, default in args_and_defaults if default is not None]
args = pyast.arguments(
posonlyargs=[],
args=args,
kwonlyargs=[],
kw_defaults=[],
defaults=defaults,
)
body = [self.visit(stmt) for stmt in node.body]
decorator_list = []
func_def = pyast.FunctionDef(
name=name,
args=args,
body=body,
decorator_list=decorator_list,
)
self._add_return_stmt(func_def)
return func_def
def visit_TypeDef(self, node: pyneast.TypeDef):
msg = f"unsupported node {node}"
raise ValueError(msg)
def visit_Assign(self, node: pyneast.Assign) -> pyast.Assign | pyast.AnnAssign | None:
if (
isinstance(node.value, pyneast.Call)
and isinstance(node.value.func, pyneast.Attribute)
and isinstance(node.value.func.value, pyneast.Name)
and node.value.func.value.id == "input"
):
parsing_input = True
else:
parsing_input = False
var_type = node.type
var_mode = node.mode
if not var_type:
try:
value_eval = pyneast.literal_eval(node.value)
except ValueError:
if not isinstance(node.value, pyneast.Call):
var_type = node.type
elif (
isinstance(node.value.func, pyneast.Specialize)
and isinstance(node.value.func.value, pyneast.Attribute)
and node.value.func.value.attr == "new"
):
var_type = pyneast.Specialize(
value=node.value.func.value.value,
args=node.value.func.args,
)
elif isinstance(node.value.func, pyneast.Attribute) and node.value.func.attr.startswith("new"):
var_type = node.value.func.value
if node.value.func.attr.startswith("new_"):
new, id = node.value.func.attr.split("_")
slice = pyneast.Name(id=id, ctx=pyneast.Load())
var_type = pyneast.Specialize(
value=var_type,
args=slice,
)
elif isinstance(node.value.func, pyneast.Name) and node.value.func.id in self._casting_types:
var_type = node.value.func
else:
value_eval_type = type(value_eval)
if value_eval_type in self._python_type_to_pine_type_mapping:
var_type = pyneast.Name(
id=self._python_type_to_pine_type_mapping[value_eval_type], ctx=pyneast.Load()
)
var_key = str(dataclasses.replace(node.target, ctx=pyneast.Load()))
self._variable_types[var_key] = (var_type, var_mode)
target = self.visit(node.target)
value = self.visit(node.value)
if not node.type and not node.mode:
assign = pyast.Assign(
targets=[target],
value=value,
)
if parsing_input:
self._inputs.append(assign)
return
else:
return assign
else:
if not var_type:
msg = f"failed to infer peristent variables's type from {node}"
raise RuntimeError(msg)
with self.type_parser():
annotation = self.visit(var_type)
if node.mode:
annotation = pyast.Subscript(
value=pyast.Name(id="Persistent", ctx=pyast.Load()),
slice=annotation,
ctx=pyast.Load(),
)
assign = pyast.AnnAssign(
target=target,
annotation=annotation,
value=value,
simple=1,
)
if parsing_input:
self._inputs.append(assign)
return
else:
return assign
def visit_ReAssign(self, node: pyneast.ReAssign) -> pyast.Assign:
target = self.visit(node.target)
value = self.visit(node.value)
return pyast.Assign(
targets=[target],
value=value,
)
def visit_AugAssign(self, node: pyneast.AugAssign) -> pyast.AugAssign:
target = self.visit(node.target)
op = self.visit(node.op)
value = self.visit(node.value)
return pyast.AugAssign(
target=target,
op=op,
value=value,
)
def visit_Import(self, node: pyneast.Import) -> pyast.Import:
name = f"lib.{node.namespace}.{node.name}.v{node.version}"
asname = node.alias
return pyast.Import(
names=[
pyast.alias(
name=name,
asname=asname,
)
]
)
def visit_Expr(self, node: pyneast.Expr) -> pyast.Expr:
value = self.visit(node.value)
if isinstance(value, pyast.stmt):
return value
else:
return pyast.Expr(value=value)
def visit_Break(self, node: pyneast.Break) -> pyast.Break:
return pyast.Break()
def visit_Continue(self, node: pyneast.Continue) -> pyast.Continue:
return pyast.Continue()
def visit_BoolOp(self, node: pyneast.BoolOp) -> pyast.BoolOp:
op = self.visit(node.op)
values = [self.visit(value) for value in node.values]
return pyast.BoolOp(
op=op,
values=values,
)
def visit_BinOp(self, node: pyneast.BinOp) -> pyast.BinOp:
left = self.visit(node.left)
op = self.visit(node.op)
right = self.visit(node.right)
return pyast.BinOp(
left=left,
op=op,
right=right,
)
def visit_UnaryOp(self, node: pyneast.UnaryOp) -> pyast.UnaryOp:
op = self.visit(node.op)
operand = self.visit(node.operand)
return pyast.UnaryOp(
op=op,
operand=operand,
)
def visit_Conditional(self, node: pyneast.Conditional) -> pyast.IfExp:
test = self.visit(node.test)
body = self.visit(node.body)
orelse = self.visit(node.orelse)
return pyast.IfExp(
test=test,
body=body,
orelse=orelse,
)
def visit_Compare(self, node: pyneast.Compare) -> pyast.Compare:
left = self.visit(node.left)
ops = [self.visit(op) for op in node.ops]
comparators = [self.visit(comp) for comp in node.comparators]
return pyast.Compare(
left=left,
ops=ops,
comparators=comparators,
)
def visit_Call(self, node: pyneast.Call) -> pyast.Call:
if isinstance(node.func, pyneast.Name) and node.func.id in self._casting_types and len(node.args) == 1:
if isinstance(node.args[0].value, pyneast.Name) and node.args[0].value.id == "na":
typ_id = self._pine_to_pyne_type_mapping[node.func.id]
if typ_id in self._namespace_mapping:
typ_id = self._namespace_mapping[typ_id]
func = pyast.Name(id="NA", ctx=pyast.Load())
args = [
pyast.Name(
id=typ_id,
ctx=pyast.Load(),
)
]
call = pyast.Call(
func=func,
args=args,
keywords=[],
)
else:
func = pyast.Name(id=f"cast_{node.func.id}", ctx=pyast.Load())
args_and_keywords = [self.visit(arg) for arg in node.args]
args = [arg for arg in args_and_keywords if isinstance(arg, pyast.expr)]
keywords = [keyword for keyword in args_and_keywords if isinstance(keyword, pyast.keyword)]
call = pyast.Call(
func=func,
args=args,
keywords=keywords,
)
else:
func = self.visit(node.func)
args_and_keywords = [self.visit(arg) for arg in node.args]
args = [arg for arg in args_and_keywords if isinstance(arg, pyast.expr)]
keywords = [keyword for keyword in args_and_keywords if isinstance(keyword, pyast.keyword)]
call = pyast.Call(
func=func,
args=args,
keywords=keywords,
)
if isinstance(node.func, pyneast.Attribute) and str(node.func.value) in self._variable_types:
typ, mod = self._variable_types[str(node.func.value)]
typ_name = typ
while not isinstance(typ_name, pyneast.Name):
if isinstance(typ_name, pyneast.Specialize):
typ_name = typ_name.value
elif isinstance(typ_name, pyneast.Attribute):
typ_name = typ_name.value
else:
msg = "unexpected type"
raise RuntimeError(msg)
call_func = typing.cast(pyast.Attribute, call.func)
instance = call_func.value
method = call_func.attr
namespace = pyast.Name(id=typ_name.id, ctx=pyast.Load())
call.func = pyast.Attribute(
value=namespace,
attr=method,
)
call.args.insert(0, instance)
return call
def visit_Constant(self, node: pyneast.Constant) -> pyast.Constant:
value = node.value
kind = None
constant = pyast.Constant(
value=value,
kind=kind,
)
if node.kind == "#":
func = pyast.Attribute(
value=pyast.Name(
id="color",
ctx=pyast.Load(),
),
attr="new",
ctx=pyast.Load(),
)
args = [constant]
constant = pyast.Call(
func=func,
args=args,
keywords=[],
)
return constant
def visit_Attribute(self, node: pyneast.Attribute) -> pyast.Attribute | pyast.Name:
if isinstance(node.value, pyneast.Name) and node.value.id == "chart" and node.attr == "point":
id = "ChartPoint"
ctx = self.visit(node.ctx)
return pyast.Name(
id=id,
ctx=ctx,
)
else:
value = self.visit(node.value)
attr = node.attr
ctx = self.visit(node.ctx)
return pyast.Attribute(
value=value,
attr=attr,
ctx=ctx,
)
def visit_Subscript(self, node: pyneast.Subscript) -> pyast.Subscript:
if not node.slice:
value = pyast.Name(
id=self._pine_to_pyne_type_mapping["array"],
ctx=pyast.Load(),
)
slice = self.visit(node.value)
ctx = self.visit(node.ctx)
return pyast.Subscript(
value=value,
slice=slice,
ctx=ctx,
)
else:
value = self.visit(node.value)
slice = self.visit(node.slice)
ctx = self.visit(node.ctx)
if not hasattr(value, "ctx"):
return pyast.Call(
func=pyast.Name(
id="inline_series",
ctx=pyast.Load(),
),
args=[value, slice],
keywords=[],
)
else:
return pyast.Subscript(
value=value,
slice=slice,
ctx=ctx,
)
def visit_Name(self, node: pyneast.Name) -> pyast.Name:
if self._parsing_type and node.id in self._pine_to_pyne_type_mapping:
id = self._pine_to_pyne_type_mapping[node.id]
ctx = self.visit(node.ctx)
return pyast.Name(
id=id,
ctx=ctx,
)
else:
id = node.id
if id in self._namespace_mapping:
id = self._namespace_mapping[id]
ctx = self.visit(node.ctx)
return pyast.Name(
id=id,
ctx=ctx,
)
def visit_Tuple(self, node: pyneast.Tuple) -> pyast.Tuple:
elts = [self.visit(elt) for elt in node.elts]
ctx = self.visit(node.ctx)
return pyast.Tuple(
elts=elts,
ctx=ctx,
)
def visit_ForTo(self, node: pyneast.ForTo) -> pyast.For:
with self.local_block():
target = self.visit(node.target)
body = [self.visit(stmt) for stmt in node.body]
start = self.visit(node.start)
end = self.visit(node.end)
step = self.visit(node.step) if node.step else None
args = [start, end]
if step:
args.append(step)
iter = pyast.Call(
func=pyast.Name(id="pine_range", ctx=pyast.Load()),
args=args,
keywords=[],
)
orelse = []
return pyast.For(
target=target,
iter=iter,
body=body,
orelse=orelse,
)
def visit_ForIn(self, node: pyneast.ForIn) -> pyast.For:
with self.local_block():
target = self.visit(node.target)
body = [self.visit(stmt) for stmt in node.body]
iter = self.visit(node.iter)
orelse = []
return pyast.For(
target=target,
iter=iter,
body=body,
orelse=orelse,
)
def visit_While(self, node: pyneast.While) -> pyast.While:
with self.local_block():
test = self.visit(node.test)
body = [self.visit(stmt) for stmt in node.body]
orelse = []
return pyast.While(
test=test,
body=body,
orelse=orelse,
)
def visit_If(self, node: pyneast.If) -> pyast.If:
with self.local_block():
test = self.visit(node.test)
body = [self.visit(stmt) for stmt in node.body]
orelse = [self.visit(stmt) for stmt in node.orelse]
return pyast.If(
test=test,
body=body,
orelse=orelse,
)
def visit_Switch(self, node: pyneast.Switch) -> pyast.Match | pyast.If:
with self.local_block():
if node.subject:
subject = self.visit(node.subject)
cases = [self.visit(case) for case in node.cases]
return pyast.Match(
subject=subject,
cases=cases,
)
else:
case = node.cases[-1]
case = typing.cast(pyneast.Case, case)
if case.pattern:
test = self.visit(case.pattern)
body = self.visit(case.body)
if_chain = pyast.If(
test=test,
body=body,
orelse=[],
)
else:
if_chain = self.visit(case.body)
for case in reversed(node.cases[:-1]):
case = typing.cast(pyneast.Case, case)
test = self.visit(case.pattern)
body = self.visit(case.body)
if_chain = pyast.If(
test=test,
body=body,
orelse=[if_chain],
)
if not isinstance(if_chain, pyast.If):
if_chain = pyast.If(
test=pyast.Constant(
value=True,
kind=None,
),
body=if_chain,
orelse=[],
)
return if_chain
def visit_Qualify(self, node: pyneast.Qualify) -> pyast.Subscript:
return pyast.Subscript(
value=pyast.Name(
id=node.qualifier.__class__.__name__,
ctx=pyast.Load(),
),
slice=self.visit(node.value),
ctx=pyast.Load(),
)
def visit_Specialize(self, node: pyneast.Specialize) -> pyast.expr:
if not isinstance(node.value, pyneast.Attribute) or not (
isinstance(node.value.value, pyneast.Name)
and node.value.value.id in {"array", "map"}
and node.value.attr == "new"
):
value = self.visit(node.value)
slice = self.visit(node.args)
return pyast.Subscript(
value=value,
slice=slice,
ctx=pyast.Load(),
)
if node.value.value.id == "map":
return self.visit(node.value)
if not isinstance(node.args, pyneast.Name):
value = self.visit(node.value)
slice = self.visit(node.args)
return pyast.Subscript(
value=value,
slice=slice,
ctx=pyast.Load(),
)
suffix = node.args.id
attr = self.visit(node.value)
attr = typing.cast(pyast.Attribute, attr)
attr.attr = f"{attr.attr}_{suffix}"
return attr
def visit_Var(self, node: pyneast.Var):
msg = f"unsupported node {node}"
raise ValueError(msg)
def visit_VarIp(self, node: pyneast.VarIp):
msg = f"unsupported node {node}"
raise ValueError(msg)
def visit_Const(self, node: pyneast.Const):
msg = f"unsupported node {node}"
raise ValueError(msg)
def visit_Input(self, node: pyneast.Input):
msg = f"unsupported node {node}"
raise ValueError(msg)
def visit_Simple(self, node: pyneast.Simple):
msg = f"unsupported node {node}"
raise ValueError(msg)
def visit_Series(self, node: pyneast.Series):
msg = f"unsupported node {node}"
raise ValueError(msg)
def visit_Load(self, node: pyneast.Load) -> pyast.Load:
return pyast.Load()
def visit_Store(self, node: pyneast.Store) -> pyast.Store:
return pyast.Store()
def visit_And(self, node: pyneast.And) -> pyast.And:
return pyast.And()
def visit_Or(self, node: pyneast.Or) -> pyast.Or:
return pyast.Or()
def visit_Add(self, node: pyneast.Add) -> pyast.Add:
return pyast.Add()
def visit_Sub(self, node: pyneast.Sub) -> pyast.Sub:
return pyast.Sub()
def visit_Mult(self, node: pyneast.Mult) -> pyast.Mult:
return pyast.Mult()
def visit_Div(self, node: pyneast.Div) -> pyast.Div:
return pyast.Div()
def visit_Mod(self, node: pyneast.Mod) -> pyast.Mod:
return pyast.Mod()
def visit_Not(self, node: pyneast.Not) -> pyast.Not:
return pyast.Not()
def visit_UAdd(self, node: pyneast.UAdd) -> pyast.UAdd:
return pyast.UAdd()
def visit_USub(self, node: pyneast.USub) -> pyast.USub:
return pyast.USub()
def visit_Eq(self, node: pyneast.Eq) -> pyast.Eq:
return pyast.Eq()
def visit_NotEq(self, node: pyneast.NotEq) -> pyast.NotEq:
return pyast.NotEq()
def visit_Lt(self, node: pyneast.Lt) -> pyast.Lt:
return pyast.Lt()
def visit_LtE(self, node: pyneast.LtE) -> pyast.LtE:
return pyast.LtE()
def visit_Gt(self, node: pyneast.Gt) -> pyast.Gt:
return pyast.Gt()
def visit_GtE(self, node: pyneast.GtE) -> pyast.GtE:
return pyast.GtE()
def visit_Param(self, node: pyneast.Param) -> tuple[pyast.arg, pyast.expr | None]:
arg = node.name
with self.type_parser():
annotation = self.visit(node.type) if node.type else None
arg = pyast.arg(arg=arg, annotation=annotation)
default = self.visit(node.default) if node.default else None
return arg, default
def visit_Arg(self, node: pyneast.Arg) -> pyast.expr | pyast.keyword:
arg = node.name
value = self.visit(node.value)
return (
pyast.keyword(
arg=arg,
value=value,
)
if arg
else value
)
def visit_Case(self, node: pyneast.Case) -> pyast.match_case:
if not node.pattern:
pattern = pyast.MatchAs()
elif isinstance(node.pattern, pyneast.Constant):
value = node.pattern.value
if isinstance(value, bool):
pattern = pyast.MatchSingleton(value=value)
else:
value = pyast.Constant(value=value)
pattern = pyast.MatchValue(value=value)
else:
value = self.visit(node.pattern)
pattern = pyast.MatchValue(value=value)
body = [self.visit(stmt) for stmt in node.body]
return pyast.match_case(
pattern=pattern,
body=body,
)
def visit_Comment(self, node: pyneast.Comment):
msg = f"unsupported node {node}"
raise ValueError(msg)
def compile(content: str) -> str:
transformer = PinescriptToPynecoreScriptTransformer()
pynescript_ast = pyneast.parse(content)
pynecore_ast = transformer.visit_Script(pynescript_ast)
pynecore_ast = pyast.fix_missing_locations(pynecore_ast)
code = pyast.unparse(pynecore_ast)
return code
@click.command
@click.argument("filename", type=click.Path())
def main(filename):
with open(filename, encoding="utf-8") as f:
content = f.read()
click.echo(compile(content))
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment