Skip to content

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 a main() function.
examples/1_high_level/hello.spy
def main() -> None:
    print("Hello world!")

Output:

Hello world!

hello_add.spy

Things to notice:

  • Red functions require type annotations.
  • After redshift, print is specialized.
  • Press the redshift button 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.

examples/1_high_level/hello_add.spy
def add(x: i32, y: i32) -> i32:
    return x + y


def main() -> None:
    print("Hello from SPy 🥸")
    print(add(10, 20))

Output:

Hello from SPy 🥸
30

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:

  • var declares a mutable global variable: reassignment is then allowed, although it still requires a global declaration inside the function.

  • var globals 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:

  • const inside a function declares a local blue constant: it is fully resolved at redshift time and inlined as a literal (run spy redshift to see).

  • Local variables assigned only once with literals or blue variables are by default blue.

examples/1_high_level/var_const.spy
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:

  • i32 is a concrete 32-bit integer type (unlike Python's arbitrary-precision int).
  • SPy primitive types include i8, i32, i64, u8, u32, u64, f32, f64.
  • for loops and range() work as in Python.
  • After redshift, the for loop is compiled to efficient code without runtime dispatch and boxing.
examples/1_high_level/factorial.spy
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:

120

fibo.spy

Things to notice:

  • Currently, int is just an alias to i32.
  • 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.

examples/1_high_level/fibo.spy
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:

610

exit_code.spy

Things to notice:

  • main() can take argv as a list[str] and can return an i32 exit code.
  • The exit code is propagated to the OS.
  • argv contains the script/executable name and the script arguments.
examples/1_high_level/exit_code.spy
def main(argv: list[str]) -> i32:
    print("argv:")
    for arg in argv:
        print(arg)
    print("exit code: 88")
    return 88

Output:

argv:
<CWD>/1_high_level/exit_code.spy
exit code: 88

collections.spy

Things to notice:

  • Tuples, dicts, and lists work as in Python.
  • Types are inferred from usage: a = (2, "SPy") gives a tuple[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.

examples/1_high_level/collections.spy
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.

examples/1_high_level/str_bytes.spy
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).
examples/1_high_level/fileio.spy
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:

hello 0

hello 1

hello 2

hello 3

hello 4

hello 5

hello 6

hello 7

hello 8

hello 9