edge 12 min read

Writing Compiler-Friendly Python for Pyvorin Edge

Best practices for writing Python that compiles well with the Pyvorin Edge compiler, including supported types, control flow, and anti-patterns to avoid.

Published Jun 2, 2026

The Compiler's Perspective

The Pyvorin Edge compiler transforms a subset of Python into native machine code via an AST-to-native pipeline. Not all Python is created equal in the eyes of this pipeline. The compiler needs static type information, simple control flow, and predictable memory layouts. When you write "compiler-friendly" Python, you are essentially writing code that the AST analyzer can fully understand and map to low-level operations.

This article teaches you how to write Python that compiles well, what constructs to avoid, and how to refactor slow patterns into fast ones. Every recommendation is grounded in the actual behavior of CompilerBridge, _detect_workload_type(), and _infer_signature_from_source().

Supported Scalar Types

The compiler bridge maintains an explicit type map for scalar arguments and return values:


# From compiler_bridge.py — the internal type map
type_map = {
    "int": ctypes.c_int64,
    "float": ctypes.c_double,
    "bool": ctypes.c_bool,
    "str": ctypes.c_char_p,
}
  

Any type hint not in this map falls back to c_int64. This is dangerous: passing a float when the compiler expects an int64 will truncate or corrupt the value. Always use explicit type hints.

Good vs Bad Type Hints


# GOOD — explicit, supported types
def compute_efficiency(power_w: float, voltage_v: float) -> float:
    return power_w / voltage_v

# BAD — missing hints (falls back to c_int64)
def compute_efficiency(power_w, voltage_v):
    return power_w / voltage_v

# BAD — unsupported type (falls back to c_int64)
def process_reading(reading: dict[str, float]) -> float:
    return reading["value"]

# BAD — Union types are not supported
def clamp(value: float | int, limit: float) -> float:
    return min(value, limit)
  

Supported Collections

The kernel backend supports one-dimensional homogeneous arrays. The first argument of a kernel function is automatically treated as an ArrayArg with ndim=1 and contiguous=True. Mixed-type lists are unsupported because SIMD vectorization requires uniform element size.


# GOOD — homogeneous list of floats
def average_temperature(readings: list[float]) -> float:
    total = 0.0
    for r in readings:
        total = total + r
    return total / len(readings)

# BAD — mixed types
def unsafe_mix(values: list) -> float:
    return sum(values)  # Compiler cannot determine element size

# BAD — nested lists (ndim > 1 not supported in automatic kernel plan)
def matrix_sum(rows: list[list[float]]) -> float:
    total = 0.0
    for row in rows:
        for v in row:
            total = total + v
    return total
  

Supported Control Flow

The compiler recognizes and emits native code for the following constructs:

  • if / elif / else — mapped to conditional jumps
  • for loops over ranges or collections — mapped to counted loops
  • while loops — mapped to conditional backward jumps
  • Arithmetic: +, -, *, /, %, **
  • Comparisons: ==, !=, <, <=, >, >=
  • Safe builtins: abs, min, max, sum, len, round, float, int, bool

Before/After: Rewriting for the Compiler


# BEFORE — dynamic attribute access, not compilable
def check_alert(sensor):
    return sensor.value > sensor.alert_threshold

# AFTER — flat arguments, compilable
def check_alert(value: float, alert_threshold: float) -> bool:
    return value > alert_threshold

# BEFORE — recursion, not supported
def factorial(n: int) -> int:
    if n <= 1:
        return 1
    return n * factorial(n - 1)

# AFTER — iterative loop, fully compilable
def factorial(n: int) -> int:
    result = 1
    i = 1
    while i <= n:
        result = result * i
        i = i + 1
    return result

# BEFORE — list comprehension with complex expression
def squared_evens(nums: list[int]) -> list[int]:
    return [x ** 2 for x in nums if x % 2 == 0]

# AFTER — explicit loop (compilable by kernel backend)
def squared_evens(nums: list[int]) -> list[int]:
    out = []
    for x in nums:
        if x % 2 == 0:
            out.append(x * x)
    return out
  

Avoiding Dynamic Attribute Access

The compiler cannot resolve getattr, setattr, or dotted attribute access at compile time because it does not know the object's memory layout. Flatten your data into primitive arguments or simple dicts with known keys.


# BAD — dynamic attribute access
def get_sensor_location(sensor) -> str:
    return sensor.location

# GOOD — pass primitive values directly
def get_sensor_location(location: str) -> str:
    return location

# BAD — dict access with variable key
def lookup(reading: dict, key: str) -> float:
    return reading[key]

# GOOD — dict access with literal key (works in safe eval, not compiled)
def lookup_value(reading: dict) -> float:
    return reading["value"]
  

Avoiding Recursion

Recursive functions require stack management that the kernel backend does not implement. The scalar backend may compile simple recursion, but tail-call optimization is not guaranteed. Always prefer iterative equivalents for hot paths.


# BAD — recursive fibonacci (will fail or be very slow)
def fib(n: int) -> int:
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

# GOOD — iterative fibonacci (compiles cleanly)
def fib(n: int) -> int:
    if n <= 1:
        return n
    a = 0
    b = 1
    i = 2
    while i <= n:
        c = a + b
        a = b
        b = c
        i = i + 1
    return b
  

Before/After: Slow vs Fast Patterns

Here is a complete side-by-side comparison of a temperature-monitoring rule written the "Pythonic" way versus the "compiler-friendly" way:


# SLOW PATTERN — interpreted, many allocations, dynamic dispatch
class SensorReading:
    def __init__(self, value, unit):
        self.value = value
        self.unit = unit

def evaluate_rule(readings):
    temps = [r.value for r in readings if r.unit == "C"]
    if len(temps) == 0:
        return False
    avg = sum(temps) / len(temps)
    return avg > 25.0

# FAST PATTERN — compilable, zero allocations in hot path
def evaluate_rule(temps_c: list[float]) -> bool:
    total = 0.0
    count = 0
    for t in temps_c:
        total = total + t
        count = count + 1
    if count == 0:
        return False
    avg = total / count
    return avg > 25.0
  

The fast pattern eliminates object construction, attribute access, list comprehensions, and dynamic filtering. The compiler can vectorize the summation loop on ARM64 NEON and inline the entire function into the pipeline's rule-evaluation loop.

Safe Eval Context in Pipeline Rules

When using condition_expr in RuleConfig, the pipeline evaluates the expression in a restricted environment. Only these builtins are available:


safe_dict = {
    "__builtins__": {
        "abs": abs,
        "max": max,
        "min": min,
        "sum": sum,
        "len": len,
        "round": round,
        "float": float,
        "int": int,
        "bool": bool,
    }
}
  

Any function, module, or global variable outside this whitelist will raise a NameError at evaluation time. If you need custom logic, compile it with CompilerBridge.compile_hotpath() and load it as a native module instead.

Checklist for Compiler-Friendly Code

  • ☐ Every function argument has a type hint from the supported set
  • ☐ Return type is explicitly hinted
  • ☐ No custom classes or dataclasses in compiled signatures
  • ☐ No recursion — all loops are for or while
  • ☐ No eval, exec, getattr, or setattr
  • ☐ Lists are homogeneous (list[float], list[int])
  • ☐ Dictionaries use literal string keys if accessed in compiled code
  • ☐ Only safe builtins are used
  • ☐ No exception handling (try / except)