1 · High Level¶
High-level SPy code that looks close to Python
Note
This page is auto-generated from the examples in
examples/1_high_level/.
Want to play with these examples yourself?
Try them in the online playground.
hello.spy¶
Things to notice:
- Every SPy program defines
main()as its entry point. SPy libraries do not need amain()function.
Output:
hello_add.spy¶
Things to notice:
- Red functions require type annotations.
- After redshift, print is specialized.
- Press the
redshiftbutton and study the output — this is a good first example to understand what redshift does.
Redshift is the compilation phase during which SPy applies:
- partial evaluation of all blue expressions and functions,
- concretization of all generic types,
- static dispatch of all function calls, and
- type checking.
The distinction between "blue" (compile-time) and "red" (runtime) is central to SPy and will be explained in the next examples.
def add(x: i32, y: i32) -> i32:
return x + y
def main() -> None:
print("Hello from SPy 🥸")
print(add(10, 20))
Output:
var_const.spy¶
This example presents the only deviance from Python's grammar in SPy,
associated with two keywords, var and const.
It is in particular related to a notable semantics difference for module-level code: after imports, the modules are frozen (immutable) and module-level assignments create by default blue and immutable (const) variables.
However:
-
vardeclares a mutable global variable: reassignment is then allowed, although it still requires aglobaldeclaration inside the function. -
varglobals are especially useful for mutable pointers (e.g. gc_ptr[T]) which appear in low-level and advanced examples.
Local variables are by default var and red, with two exceptions:
-
constinside a function declares a local blue constant: it is fully resolved at redshift time and inlined as a literal (runspy redshiftto see). -
Local variables assigned only once with literals or blue variables are by default blue.
from __spy__ import COLOR
# plain module-level assignment: immutable (const) by default
N: i32 = 10
# `var` makes the global mutable
var counter: i32 = 0
def increment() -> None:
global counter
counter = counter + 1
def main() -> None:
# `const` declares a local blue constant, resolved at redshift time
const LIMIT = 5
# LIMIT = 6 # would raise: "x is const" — uncomment to see the error
# equivalent to `const LIMIT2 = 10`
LIMIT2 = 10 # blue, since assigned once
# (if a name is assigned only once outside a loop, it's tagged as const)
# however, no error would be raised if a developer adds another `LIMIT2 = ...`
# this is getting advanced, don't worry if it's not clear yet
LIMIT3 = LIMIT + LIMIT2 # also blue, since LIMIT and LIMIT2 are blue
LIMIT4 = LIMIT + LIMIT2 # var and red because of the next assignment
print("COLOR(LIMIT)", COLOR(LIMIT)) # blue
print("COLOR(LIMIT2)", COLOR(LIMIT2)) # blue
print("COLOR(LIMIT3)", COLOR(LIMIT3)) # blue
print("COLOR(LIMIT4)", COLOR(LIMIT4)) # red
LIMIT4 = 10
print("N+1:", N+1)
# N = 20 # would raise: "x is const" — uncomment to see the error
print("counter:", counter)
for i in range(LIMIT):
increment()
print("counter:", counter)
Output:
COLOR(LIMIT) blue
COLOR(LIMIT2) blue
COLOR(LIMIT3) blue
COLOR(LIMIT4) red
N+1: 11
counter: 0
counter: 5
factorial.spy¶
Things to notice:
i32is a concrete 32-bit integer type (unlike Python's arbitrary-precisionint).- SPy primitive types include i8, i32, i64, u8, u32, u64, f32, f64.
forloops andrange()work as in Python.- After redshift, the for loop is compiled to efficient code without runtime dispatch and boxing.
import time
def factorial(n: i32) -> i32:
res = 1
for i in range(n):
res *= i + 1
return res
def main() -> None:
print(factorial(5))
# this is needed to force gcc to compile the function, else it just precomputes the result
# print(factorial(int(time.time())))
Output:
fibo.spy¶
Things to notice:
- Currently,
intis just an alias toi32. - Recursive functions work as in Python, with full type inference on return values.
- Timing shows the speedup from compilation vs CPython interpretation.
- In SPy,
main()does not need to be explicitly called.
Note: SPy's performance comes from the compiler, not the interpreter. The interpreter
is currently much slower than CPython — the goal is to reach CPython speed, but this
is not yet achieved. The plan to get there is to implement the SPy interpreter in SPy
itself, so that it can be compiled. To get compiled performance in the meantime,
install SPy locally and run spy build -x fibo.spy. The playground does not yet
support spy build.
from time import time
def fibo(n: int) -> int:
if n <= 1:
return n
else:
return fibo(n - 1) + fibo(n - 2)
def main() -> None:
t_start = time()
result = fibo(15)
t_end = time()
print(result)
print("#", str(t_end - t_start), "s")
# uncomment this line to run with a Python interpreter
# main()
Output:
exit_code.spy¶
Things to notice:
main()can takeargvas alist[str]and can return ani32exit code.- The exit code is propagated to the OS.
argvcontains the script/executable name and the script arguments.
def main(argv: list[str]) -> i32:
print("argv:")
for arg in argv:
print(arg)
print("exit code: 88")
return 88
Output:
collections.spy¶
Things to notice:
- Tuples, dicts, and lists work as in Python.
- Types are inferred from usage:
a = (2, "SPy")gives atuple[i32, str]. - Heterogeneous tuples are fully supported; dict and list require uniform types.
-
The length and element types of tuples must be known at compile time;
tuple[T, ...]is not supported yet (but will be). -
After redshift, collection operations are specialized and dispatch-free.
Notes:
-
interp_variants without type limitations exist:from __spy__ import interp_tuple, interp_list, interp_dict. For example,interp_list[dynamic](1, "a", int)is supported. However, they can only be used in blue functions if you want to be able to build. -
The static versions are still missing many methods, but they are implemented in SPy's own standard library, so contributions are welcome and relatively easy.
def main() -> None:
a = (2, "SPy")
print(a[0])
print(a[1])
print(a)
b = {"a": 20}
print(b["a"])
print(b)
l = [""]
l[0] = "Hello"
l.append("SPy")
print(l[0], l[1])
print(l)
Output:
2
SPy
(2, 'SPy')
20
<spy `_dict::dict[str, i32]::_dict` object at <ADDR>>
Hello SPy
<spy `_list::list[str]::_ListImpl` object at <ADDR>>
str_bytes.spy¶
Things to notice:
- str supports: +, *, ==, !=, [], len(), replace(), upper(), isascii(), ...
- str can be converted to numeric types with explicit casts: i32(s), f64(s), ...
- bytes literals use b'...' syntax.
- bytes supports: len(), [], ==, !=, +, *, repr(), ...
ord()works on single-character str or bytes literals.
Warning: str and bytes are still missing many methods, but they can be implemented in SPy's own standard library, so contributions are welcome and relatively easy.
def demo_str() -> None:
s = "Hello"
# concatenation and repetition
print(s + ", world!")
print(s * 3)
# length and indexing
print(len(s))
print(s[1])
# comparison
print(s == "Hello")
print(s != "World")
print(s <= "World")
print(s >= "World")
print(s < "World")
print(s > "World")
# methods
print(s.upper())
print(s.isascii())
print(s.replace("l", "r"))
# explicit conversion from str to numeric types
n = i32("42")
print(n + 1)
def demo_bytes() -> None:
b = b"Hello"
# length and indexing (returns u8)
print(len(b))
byte: u8 = b[0]
print(byte) # 72 (ASCII code of 'H')
# concatenation and repetition
print(b + b", world!")
print(b * 2)
# comparison
print(b == b"Hello")
print(b != b"World")
print(repr(b"hi\nbye"))
assert ord(b"A") == u8(65)
# non ASCII characters not yet supported
assert ord("A") == 65
def main() -> None:
print("=== str ===")
demo_str()
print("=== bytes ===")
demo_bytes()
assert "hello".encode("utf-8") == b"hello"
assert "àèìòù".encode("utf-8").decode("utf-8") == "àèìòù"
Output:
=== str ===
Hello, world!
HelloHelloHello
5
e
True
True
True
False
True
False
HELLO
True
Herro
43
=== bytes ===
5
72
b'Hello, world!'
b'HelloHello'
True
True
b'hi\nbye'
fileio.spy¶
Things to notice:
open(),write(),close(), and iteration over lines work as in Python.- Context managers (
with) are not yet supported (but in the roadmap).
def main() -> None:
f = open("/tmp/foo.txt", "w")
for i in range(10):
f.write("hello " + str(i) + "\n")
f.close()
f2 = open("/tmp/foo.txt")
for line in f2:
print(line)
Output: