Skip to content

2 · Metaprogramming

Blue functions, metafunctions, and compile-time computation

Note

This page is auto-generated from the examples in examples/2_metaprogramming/. Want to play with these examples yourself? Try them in the online playground.

bluefunc_pi.spy

Things to notice:

  • @blue funcs are completely resolved at redshift time.
  • get_pi is not present in the redshifted or .c file.
  • Type annotations in blue functions are optional and default to dynamic.

Note: redshift --linearize "linearises" the __block__, which can give simpler output for the print calls with more than one argument.

examples/2_metaprogramming/bluefunc_pi.spy
@blue
def get_pi():
    """
    Compute an approximation of PI using the Leibniz series
    """
    tol = 0.001
    pi_approx = 0.0
    k = 0
    term = 1.0  # Initial term to enter the loop

    while abs(term) > tol:
        if k % 2 == 0:
            term = 1.0 / (2 * k + 1)
        else:
            term = -1 * 1.0 / (2 * k + 1)

        pi_approx = pi_approx + term
        k = k + 1

    return 4 * pi_approx


def main() -> None:
    pi = get_pi()
    print("pi:", pi)
    print("tau:", 2 * pi)

Output:

pi: 3.143588659585789
tau: 6.287177319171578

bluefunc_adder.spy

Things to notice:

  • @blue funcs are completely resolved at redshift time.
  • make_adder is not present in the redshifted or .c file.
  • Type annotations in blue functions are optional and default to dynamic.
  • Parameters can be a type to get generics but other types can be used (here i32 and float).

  • Generic functions with the [] syntax are just syntactic sugar for a @blue.generic function factory.

  • Redshift inserts type conversions.

examples/2_metaprogramming/bluefunc_adder.spy
def make_adder[T, x](y: T) -> T:
    return x + y


add_4 = make_adder[i32, 4]
add_pi = make_adder[f64, 3.1415]


def main() -> None:
    print(add_4(38))
    print(add_pi(10))

Output:

42
13.1415

blue_generic.spy

Things to notice:

  • def add[T](...) is syntactic sugar for @blue.generic def add(T): ....
  • Generic functions are called with [] for the parameters, () for values.
  • @blue.generic functions are resolved entirely at redshift time: each add[i32] and add[str] becomes a separate specialized red function.

  • add2 shows the explicit desugaring — both forms are equivalent.

examples/2_metaprogramming/blue_generic.spy
def add[T](x: T, y: T) -> T:
    return x + y


# the function above is syntax sugar for this, i.e. a @blue.generic function factory
@blue.generic
def add2(T):
    def impl(x: T, y: T) -> T:
        return x + y

    return impl


# Functions decorated with @blue.generic are called using square brackets [] instead of parentheses ().
# Other than that, they are identical to other blue functions.


def main() -> None:
    print(add[i32](1, 2))
    print(add[str]("hello ", "world"))

    print(add2[i32](1, 2))
    print(add2[str]("hello ", "world"))

Output:

3
hello world
3
hello world

deco.spy

Things to notice:

  • @blue decorators are applied entirely at import time.
  • double wraps inc at compile time: the resulting inc in red code is already the doubled version — double itself disappears from the output.

  • This is the SPy equivalent of zero-cost compile-time code generation.

  • Blue decorator functions do not need type annotations.
examples/2_metaprogramming/deco.spy
@blue
def double(fn):
    def inner(x: i32) -> i32:
        res = fn(x)
        return res * 2

    return inner


@double
def inc(x: i32) -> i32:
    return x + 1


def main() -> None:
    res = inc(5)
    print(res)

Output:

12

metafunc.spy

Things to notice:

  • @blue.metafunc defines a function that runs at redshift time to dispatch on the static types of its arguments — no runtime cost.

  • Arguments prefixed with m_ are MetaArgs: they carry information known at redshift time, in particular m_x.static_type and m_x.color.

  • Each call site gets a specialized implementation, depending on the MetaArgs.

  • The metafunc returns an OpSpec, which describes which red function will be called at runtime and with which arguments.

  • OpSpec(func) is the simple form: func will be called with the original arguments as-is.

  • Unsupported types raise TypeError at redshift time, not at runtime.

  • This is how SPy's built-in print is implemented internally.

Note on OpSpec — the return value of metafuncs:

  • OpSpec(func): simple form — func is called with the original arguments
  • OpSpec(func, args): complex form — func is called with pre-filled arguments (some values are baked in at redshift time)

  • OpSpec.const(value): the result is a compile-time constant, no call at runtime

  • OpSpec.NULL: signals that this case is not handled
examples/2_metaprogramming/metafunc.spy
from operator import OpSpec


@blue.metafunc
def myprint(m_x):
    if m_x.static_type == int:

        def myprint_int(x: int) -> None:
            print(x)

        return OpSpec(myprint_int)

    if m_x.static_type == str:

        def myprint_str(x: str) -> None:
            print(x)

        return OpSpec(myprint_str)

    raise TypeError("don't know how to print this")


def main() -> None:
    print(42)
    myprint("hello")
    # myprint(5.2)  # raises TypeError, but it's expected

Output:

42
hello

metafunc_variadic.spy

Things to notice:

  • *args_m captures all MetaArgs as a tuple; the number of arguments is known at redshift time via len(args_m), enabling dispatch on it.

  • Each call site is specialized on both the number and types of arguments.

  • T = m0.static_type captures the static type as a blue value, which can then be used as a type annotation in the inner red function.

  • For blue arguments, m_x.blueval gives the actual compile-time value; here all arguments are red so only static_type is used.

  • Mismatched types (e.g. mysum(1, 'hello')) raise TypeError at redshift time — uncomment the last lines to see the errors.

examples/2_metaprogramming/metafunc_variadic.spy
from operator import OpSpec


@blue.metafunc
def mysum(*args_m):
    if len(args_m) == 2:
        m0, m1 = args_m
        T = m0.static_type

        def mysum2(v0: T, v1: T) -> T:
            return v0 + v1

        return OpSpec(mysum2)

    elif len(args_m) == 3:
        m0, m1, m2 = args_m
        T = m0.static_type

        def mysum3(v0: T, v1: T, v2: T) -> T:
            return v0 + v1 + v2

        return OpSpec(mysum3)

    else:
        raise TypeError("invalid number of arguments")


def main() -> None:
    print(mysum(1, 2))
    print(mysum(1, 2, 3))
    print(mysum("hello ", "world"))
    # print(mysum(1, 'hello'))
    # print(mysum(1, 2, 3, 4))

Output:

3
6
hello world