spython
A statically-typed subset of Python compiled to LLVM IR. Written in Go.
Overview
spython takes a strict, typed subset of Python, runs it through a clean multi-pass frontend, emits LLVM IR,
and hands off to clang to produce a native binary. Heap memory is managed by the
Boehm–Demers–Weiser conservative garbage collector.
The goal is a practical, readable compiler that’s small enough to reason about end-to-end —
not a full CPython replacement. It’s opinionated: types are inferred from the RHS at first binding
(annotations only required when the RHS is ambiguous, e.g. None or an empty container),
dynamic features are intentionally absent, and the runtime is a single C file.
Pipeline
Each stage is isolated in its own Go package. No shared mutable state, no visitor patterns — just plain data flowing forward.
Example
class Shape:
def __init__(self):
self.name = "shape"
def area(self) -> float:
return 0.0
class Circle(Shape):
def __init__(self, r: float):
super().__init__()
self.name = "circle"
self.r = r
def area(self) -> float:
return 3.14 * self.r * self.r
class Rect(Shape):
def __init__(self, w: float, h: float):
super().__init__()
self.name = "rect"
self.w = w
self.h = h
def area(self) -> float:
return self.w * self.h
shapes: list[Shape] = [Circle(2.0), Rect(3.0, 4.0), Circle(1.0)]
total: float = 0.0
for s in shapes:
total = total + s.area()
print(total) # 27.7
What’s supported
Types
- int, float, bool
- str
- list[T]
- dict[K, V]
- set[T]
- tuple
- bytes, bytearray
- None
- user-defined classes
Control flow
- if / elif / else
- while
- for … in range / list
- for … in set / dict
- for … in iterator
- x in y / x not in y
- break, continue
- return
Functions
- def with type annotations
- recursion, early return
- *args, **kwargs
- keyword-only params
- default arguments
- kwargs & * / ** unpacking
Closures & generators
- lambda expressions
- nested def, by-value capture
- Callable[[Args], Ret]
- yield, yield from
- Iterator[T], next()
- StopIteration
Classes & OO
- single inheritance
- virtual dispatch (vtables)
- super()
- isinstance()
- field inference from __init__
- implicit upcasting
Dunder methods
- __init__, __str__, __repr__
- __eq__, __ne__
- __lt__, __le__, __gt__, __ge__
- __add__, __sub__, __mul__
- __truediv__, __floordiv__, __mod__
- __neg__, __pow__
Imports
- import module
- import module as alias
- from module import name
- multi-file projects
Exceptions
- raise / try / except / finally
- Exception, ArithmeticError
- ZeroDivisionError, ValueError
- TypeError, OSError, …
- user-defined subclasses
- propagation across calls
Container methods
- str: upper/strip/split/…
- list: append/sort(key=)/…
- dict: keys/values/get/…
- set: add/discard
Stdlib — 29 modules
- math, random, time, sys
- io, os, os.path, shutil
- socket, ssl, requests, json
- re, itertools, functools
- heapq, bisect, hashlib, …
- .spy shim + sibling .c, FFI
Operators
- arithmetic: + - * / // % **
- comparison: == != < > <= >=
- logical: and, or, not
- bitwise: & | ^ ~ << >>
- augmented assign: += -= …
Runtime
- print, range, len
- int(), float(), str(), bool()
- isinstance()
- conservative GC (bdwgc)
- list/str/map indexing
What’s not supported
- multiple inheritance, MRO
- decorators (@property, @staticmethod, @classmethod, …)
- async / await
- dynamic typing — a name’s type is fixed at first binding
- comprehensions (list / dict / set / generator)
- metaclasses, __new__, __slots__, descriptors
- context managers (with)
- eval, exec, getattr / setattr / hasattr
- monkey-patching — classes are closed after definition
Try it
# macOS
brew install go llvm bdw-gc
# Debian / Ubuntu
apt install golang clang libgc-dev
git clone https://github.com/hvuhsg/spython-llvm
cd spython-llvm
go build -o spython ./cmd/spython
./spython run testdata/class_polymorphism/main.spy