AST Transformation
How PyneCore uses AST transformation to implement Pine Script behavior
AST Transformation
Import Hook System
The system’s entry point is the import hook, which transforms Python files marked with the @pyne
magic comment:
# Import hook through importlib meta_path system
sys.meta_path.insert(0, PyneImportHook())
The PyneLoader
class performs code transformation in multiple steps, applying the AST transformation chain.
Transformation Chain
PyneCore applies several key transformations to Python code to make it behave like Pine Script:
- Import Lifter - Moves function-level imports to module level
- Import Normalizer - Standardizes import statements
- PersistentSeries Transformer - Manages the hybrid PersistentSeries type
- Library Series Transformer - Prepares library Series variables
- Function Isolation Transformer - Ensures separate state for each function call
- Module Property Transformer - Handles module properties
- Series Transformer - Handles Series variables
- Persistent Transformer - Manages persistent variables
- Input Transformer - Processes input parameters
This order ensures that dependencies between transformations are properly handled. For example, PersistentSeries transformation must happen before both Persistent and Series transformations
Each transformation step modifies the Python AST to implement Pine Script behavior while maintaining Python syntax and readability.
Detailed Transformation Process
Import Lifter
The Import Lifter moves function-level imports to module level.
Original code:
def main():
from pynecore.lib.ta import sma
result = sma(close, 14)
Transformed code:
from pynecore.lib.ta import sma
def main():
result = sma(close, 14)
Key aspects:
- Lifts all pynecore.lib related imports to module level
- Ensures imports are accessible throughout the module
- Prevents duplicate imports
Import Normalizer
The Import Normalizer transforms all PyneCore imports to use a consistent format.
Original code:
from pynecore.lib.ta import sma, ema
from pynecore.lib import plot, close
def main():
plot(close)
plot(sma(close, 14))
plot(ema(close, 14))
Transformed code:
from pynecore import lib
import pynecore.lib.ta
def main():
lib.plot(lib.close)
lib.plot(lib.ta.sma(lib.close, 14))
lib.plot(lib.ta.ema(lib.close, 14))
Key aspects:
- Converts all lib-related imports to ‘from pynecore import lib’
- Transforms variable references to use fully qualified names (lib.ta.sma)
- Maintains compatibility with wildcard imports
- Ensures consistent import style across the codebase
This is very important to make lib level properties work like close
, open
, high
, low
, volume
, etc.
If you would use this kind of import:
a = close
That would not work, because the value would never be updated in the next bar. However, after using the import normalizer, it will work:
a = lib.close
Because the module level variable changed, and we access through the lib module object.
PersistentSeries Transformer
The PersistentSeries transformer converts the combined PersistentSeries type into separate Persistent and Series declarations.
Original code:
ps: PersistentSeries[float] = 1
ps += 1
Transformed code:
p: Persistent[float] = 1
s: Series[float] = p
s += 1
Key aspects:
- Splits PersistentSeries declarations into two separate declarations
- Must be applied before both Persistent and Series transformers
This makes easier to declare variables are both persistent and series.
Library Series Transformer
The Library Series transformer prepares library Series variables (like close, open, high, etc.) for proper handling by the Series transformer.
Original code:
lib.close[1] # Access previous bar's close price
Transformed code:
_lib_close: Series = lib.close
_lib_close[1] # Now works with Series transformer
Key aspects:
- Creates local Series variables for library Series
- Prepares variables for Series transformer processing
If you import a variable from a library, it does not know if it is a series or not. But if you use indexing (subscription) on it, it should initialize it as a series. This is needed, because the AST transformer does not know anything about the other files just the one it is currently transforming.
Function Isolation Transformer
The Function Isolation transformer ensures each function call gets its own isolated scope by wrapping functions with the isolate_function decorator.
Original code:
def compute_avg(source):
return (source + source[1]) / 2
result = compute_avg(close)
Transformed code:
from pynecore.core.function_isolation import isolate_function
__scope_id__ = "8af7c21e_example.py"
def compute_avg(source):
global __scope_id__
return (source + source[1]) / 2
result = isolate_function(compute_avg, "main|compute_avg|0", __scope_id__)(close)
Key aspects:
- Wraps each function call with isolate_function
- Generates a unique call ID for each invocation
- Maintains scope hierarchy information
- Adds scope ID handling to each function
- Excludes standard library and non-transformable functions
Module Property Transformer
The Module Property transformer handles attributes that should be called as functions based on configuration.
Original code:
bar_index = lib.bar_index
time = lib.time
Transformed code:
bar_index = lib.bar_index()
time = lib.time
Key aspects:
- Uses configuration to determine which attributes are properties
- Automatically adds parentheses for property calls
- Preserves normal attributes as is
- Handles dynamic cases with runtime checks
Series Transformer
The Series transformer converts Series annotated variables in Python code into a global SeriesImpl instance with add() and set() operations.
Original code:
s: Series[float] = close
s += 1
previous = s[1]
Transformed code:
from pynecore.core.series import SeriesImpl
__series_main_s__ = SeriesImpl()
__series_function_vars__ = {'main': ['__series_main_s__']}
def main():
s = __series_main_s__.add(close)
s = __series_main_s__.set(s + 1)
previous = __series_main_s__[1]
Key aspects:
- Creates a global SeriesImpl instance for each Series variable
- Converts assignments to add() and set() operations
- Redirects indexing operations to the global instance
- Maintains a registry of all Series variables per function scope
Persistent Transformer
The Persistent transformer converts variables with Persistent type annotation to global variables that maintain their values across function calls.
Original code:
p: Persistent[float] = 0
p += 1
Transformed code:
__persistent_main_p__ = 0
__persistent_function_vars__ = {'main': ['__persistent_main_p__']}
def main():
global __persistent_main_p__
__persistent_main_p__ += 1
Key aspects:
- Creates a global variable for each Persistent variable
- Adds global declarations in functions
- Handles initialization for non-literal values
- Maintains a registry of all Persistent variables by scope
This is the fastest possible way to implement persistent variables.
Input Transformer
The Input transformer processes input parameters and adds necessary ID information.
Original code:
@script.indicator
def main(source=lib.input.source("Source", lib.close)):
result = source * 2
Transformed code:
@script.indicator
def main(source=lib.input.source("Source", lib.close, _id="source")):
source = getattr(lib, source, lib.na)
result = source * 2
Key aspects:
- Adds _id parameter to input calls
- Adds getattr for source inputs at the start of functions
- Enables proper input parameter resolution
- Handles source inputs specially
Example of Complete Transformation
Let’s see a full example of how a simple Pyne code is transformed:
Original Pyne Code:
"""
@pyne
"""
from pynecore import Series, Persistent
from pynecore.lib.ta import sma
from pynecore.lib import close, plot
def main():
# Persistent counter
count: Persistent[int] = 0
count += 1
# Moving average calculation
ma: Series[float] = sma(close, 14)
# Plot results
plot(ma, "MA", color=lib.color.blue)
plot(count, "Count", color=lib.color.red)
Transformed Code:
"""
@pyne
"""
from pynecore import lib
import pynecore.lib.ta
from pynecore.core.series import SeriesImpl
from pynecore.core.function_isolation import isolate_function
# Global variables and scope ID
__scope_id__ = "8af7c21e_example.py"
__persistent_main_count__ = 0
__series_main_ma__ = SeriesImpl()
# Function and variable registries
__persistent_function_vars__ = {'main': ['__persistent_main_count__']}
__series_function_vars__ = {'main': ['__series_main_ma__']}
def main():
global __scope_id__
global __persistent_main_count__
# Persistent counter
__persistent_main_count__ += 1
# Moving average calculation
ma = __series_main_ma__.add(isolate_function(lib.ta.sma, "main|lib.ta.sma|0", __scope_id__)(lib.close, 14))
# Plot results
lib.plot(ma, "MA", color=lib.color.blue)
lib.plot(__persistent_main_count__, "Count", color=lib.color.red)
This example demonstrates how the different transformers work together to convert a simple Pyne script into equivalent Python code that provides Pine Script-like behavior through PyneCore’s runtime system.