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_piis 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.
@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:
bluefunc_adder.spy¶
Things to notice:
- @blue funcs are completely resolved at redshift time.
make_adderis 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.genericfunction factory. -
Redshift inserts type conversions.
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:
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.genericfunctions are resolved entirely at redshift time: eachadd[i32]andadd[str]becomes a separate specialized red function. -
add2shows the explicit desugaring — both forms are equivalent.
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:
deco.spy¶
Things to notice:
@bluedecorators are applied entirely at import time.-
doublewrapsincat compile time: the resultingincin red code is already the doubled version —doubleitself disappears from the output. -
This is the SPy equivalent of zero-cost compile-time code generation.
- Blue decorator functions do not need type annotations.
@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:
metafunc.spy¶
Things to notice:
-
@blue.metafuncdefines 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 particularm_x.static_typeandm_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:funcwill be called with the original arguments as-is. -
Unsupported types raise
TypeErrorat redshift time, not at runtime. - This is how SPy's built-in
printis implemented internally.
Note on OpSpec — the return value of metafuncs:
OpSpec(func): simple form —funcis called with the original arguments-
OpSpec(func, args): complex form —funcis 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
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:
metafunc_variadic.spy¶
Things to notice:
-
*args_mcaptures all MetaArgs as a tuple; the number of arguments is known at redshift time vialen(args_m), enabling dispatch on it. -
Each call site is specialized on both the number and types of arguments.
-
T = m0.static_typecaptures 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.bluevalgives the actual compile-time value; here all arguments are red so onlystatic_typeis used. -
Mismatched types (e.g.
mysum(1, 'hello')) raiseTypeErrorat redshift time — uncomment the last lines to see the errors.
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: