PythoC: Write C in Python
PythoC is a Python DSL compiler that compiles statically-typed Python to LLVM IR, providing C-equivalent runtime capabilities with Python syntax and compile-time metaprogramming.
Design Philosophy
Core principle: C-level runtime + Python-powered compile-time
- C-compatible runtime: Compiled code maps directly to native machine code with C-level control and performance
- Full access to low-level operations (pointers, manual memory, inline assembly)
- C calling conventions for seamless interoperability
- No runtime overhead beyond what C would have
- Compile-time = Python: Full Python power for metaprogramming, generics, and code generation
- Zero-cost abstractions: Python-level abstractions compile away completely
- Explicit typing: All types must be annotated (like C, unlike Python)
- Explicit control flow: No implicit control flow (no exceptions, no RAII, no destructors)
- Structs are plain data (no methods, no constructors)
- Manual resource management like C
- Optional safety features: Linear types and refinement types provide memory safety guarantees without introducing hidden control flow
- Prevent memory leaks, use-after-free, null pointer dereference, array out-of-bounds
- Completely optional - use only when needed
- No extra runtime overhead
- Convenient Python-C interoperability:
- Python can call PythoC compiled functions at runtime (via ctypes/cffi)
- PythoC can invoke Python code at compile-time for metaprogramming
Quick Start
Installation
Hello World
from pythoc import compile, i32 @compile def add(x: i32, y: i32) -> i32: return x + y # Can compile to native code @compile def main() -> i32: return add(10, 20) # Or call the compiled dynamic library from Python directly result = main()
Run Tests
# Run all tests python test/run_all_tests.py # Run specific test suites python test/run_integration_tests.py python test/run_examples.py
Example: Binary Tree Benchmark
This example demonstrates PythoC's direct mapping to C - compare with test/example/base_binary_tree_test.c:
from __future__ import annotations from pythoc import i32, ptr, compile, nullptr, seq, sizeof from pythoc.libc.stdlib import malloc, free from pythoc.libc.stdio import printf # C: typedef struct tn { struct tn* left; struct tn* right; } treeNode; @compile class TreeNode: left: ptr[TreeNode] right: ptr[TreeNode] # C: treeNode* NewTreeNode(treeNode* left, treeNode* right) @compile def NewTreeNode(left: ptr[TreeNode], right: ptr[TreeNode]) -> ptr[TreeNode]: new: ptr[TreeNode] = ptr[TreeNode](malloc(sizeof(TreeNode))) new.left = left new.right = right return new # C: long ItemCheck(treeNode* tree) @compile def ItemCheck(tree: ptr[TreeNode]) -> i32: if tree.left == nullptr: return 1 else: return 1 + ItemCheck(tree.left) + ItemCheck(tree.right) # C: treeNode* BottomUpTree(unsigned depth) @compile def BottomUpTree(depth: i32) -> ptr[TreeNode]: if depth > 0: return NewTreeNode(BottomUpTree(depth - 1), BottomUpTree(depth - 1)) else: return NewTreeNode(nullptr, nullptr) # C: void DeleteTree(treeNode* tree) @compile def DeleteTree(tree: ptr[TreeNode]): if tree.left != nullptr: DeleteTree(tree.left) DeleteTree(tree.right) free(tree)
C Parity: Full C Capabilities
Supported Features
PythoC provides complete C runtime capabilities:
Primitive types:
- Integers:
i8,i16,i32,i64,u8,u16,u32,u64 - Floats:
f16,f32,f64,bf16,f128 - Boolean:
bool
Composite types:
- Pointers:
ptr[T] - Arrays:
array[T, N]orarray[T, N, M, ...]for multi-dimensional - Structs:
@compile class,@struct class,struct[x: i32, y: i32](named) orstruct[i32, i32](unnamed) - Unions:
@union classorunion[T1, T2, ...] - Enums:
@enum class - Function pointers:
func[[arg_types], return_type]
Control flow:
if/else,while,forloopsbreak,continue,return- Pattern matching:
match/case(enhanced switch) - Scoped
gotoand labels for low-level control (similar to labeled continue/break, limited but safer)
Operations:
- Arithmetic:
+,-,*,/,%,// - Comparison:
==,!=,<,>,<=,>= - Logical:
and,or,not - Bitwise:
&,|,^,~,<<,>> - Pointer arithmetic and dereferencing
C Standard Library:
from pythoc.libc.stdio import printf, scanf from pythoc.libc.stdlib import malloc, free, atoi from pythoc.libc.string import memcpy, strlen from pythoc.libc.math import sin, cos, pow
Not Yet Supported
Features deliberately excluded or pending:
- Fall-through
switch: Usematch/casewith explicit branches - Global variable initialization: Workaround with init functions and effect system
- Variable-length arrays (VLA): Use fixed-size arrays or malloc
- Flexible array members: Use separate size tracking
PythoC Language Core
Beyond C parity, PythoC adds modern type system features (all optional, minimal support):
Algebraic Data Types (ADT) and Pattern Matching
Provide Rust-like enums with payload for type-safe tagged unions:
from pythoc import enum, compile, i32 @enum class Result: Ok: i32 Err: i32 @compile def handle_result(r: Result) -> i32: match r: case (Result.Ok, value): return value case (Result.Err, code): return -code
Linear Types (Optional)
Prevent use-after-free and resource leaks without RAII or destructors:
from pythoc import compile, linear, consume, void, ptr, i8, i32, struct from pythoc.libc.stdlib import malloc, free # Allocator returns resource + linear proof @compile def lmalloc(size: i32) -> struct[ptr[i8], linear]: return malloc(size), linear() # Only the paired deallocator can consume the proof @compile def lfree(ptr: ptr[i8], prf: linear) -> void: free(ptr) consume(prf) # Proof consumed - resource released @compile def safe_usage() -> void: mem, prf = lmalloc(100) # ... use mem ... lfree(mem, prf) # Must call lfree to consume prf # Compile error if prf not consumed!
Motivation: Proof-carrying code pattern. The linear proof proves resource was allocated and must be deallocated. Compiler enforces pairing of alloc/free at compile-time.
Refinement Types (Optional)
Check once, use safely everywhere without runtime overhead:
from pythoc import compile, i32, bool, refined, refine, array, ptr, nullptr # Non-null pointer - check once, use safely everywhere @compile def is_nonnull(p: ptr[i32]) -> bool: return p != nullptr NonNull = refined[is_nonnull] @compile def process_data(p: ptr[i32]) -> i32: for ptr_checked in refine(p, is_nonnull): # Type system knows ptr_checked is non-null return access_ptr(ptr_checked) else: return -1 # Handle null case @compile def access_ptr(p: refined[is_nonnull]) -> i32: return p[0] # Array bounds - check once, access many arrays safely @compile def is_valid_index(idx: i32) -> bool: return 0 <= idx < 10 @compile def process_arrays(i: i32, arr1: array[i32, 10], arr2: array[i32, 10]) -> i32: for idx in refine(i, is_valid_index): # Type system remembers idx is valid a = arr1[idx] # Safe: no bounds check b = arr2[idx] # Safe: no bounds check c = arr1[idx] # Safe: reuse safely return a + b + c else: return -1
Motivation: Check once, encode in type system, use safely everywhere. Typical examples: non-null pointers, valid array indices, non-zero divisors. The type system remembers the property, eliminating redundant runtime checks. Zero overhead for subsequent uses.
Effect System
Compile-time dependency injection with zero runtime overhead. Manage the global state and can be overridden at compile-time.
# rng_lib.py from pythoc import compile, effect, u64, void from types import SimpleNamespace @compile def rng_next() -> u64: return u64(42) @compile def rng_seed(s: u64) -> void: pass RNG = SimpleNamespace(next=rng_next, seed=rng_seed) effect.default(rng=RNG) @compile def use_rng() -> u64: return effect.rng.next() # Another file can override the default RNG implementation from pythoc import * from types import SimpleNamespace @compile def mock_rng_next() -> u64: return u64(43) @compile def mock_rng_seed(s: u64) -> void: pass MockRNG = SimpleNamespace(next=mock_rng_next, seed=mock_rng_seed) with effect(rng=MockRNG, suffix="mock"): from rng_lib import use_rng print(use_rng()) # 43
Defer
Explicit scope-exit actions, lowered to control flow.
from pythoc import compile, defer, linear, consume, void @compile def consumer(t: linear) -> void: consume(t) @compile def ok_defer(n: i32) -> void: for i in seq(n): t = linear() defer(consumer, t) yield i # consumer(t) executes at scope exit and consumes t no what user-code continues or breaks
Cimport
Import C modules directly into PythoC.
cimport is a pure PythoC implementation to parse and import C modules.
It is under a heavy re-design and development currently.
Python as Preprocessor
Use Python's full power at compile-time for metaprogramming:
Generic Types via Python Functions
from pythoc import compile, struct, i32, f64 # Python function generates specialized types and functions def make_point(T): @struct(suffix=T) # suffix creates unique type name class Point: x: T y: T @compile(suffix=T) # suffix creates unique function name def add_points(p1: Point, p2: Point) -> Point: result: Point = Point() result.x = p1.x + p2.x result.y = p1.y + p2.y return result return Point, add_points # Generate specialized versions at compile-time Point_i32, add_i32 = make_point(i32) Point_f64, add_f64 = make_point(f64) @compile def test() -> i32: p1: Point_i32 = Point_i32() p1.x = 10 p2: Point_i32 = Point_i32() p2.x = 20 result: Point_i32 = add_i32(p1, p2) return result.x
Code Generation at Compile-Time
# Python computes values and generates code before compilation UNROLL_COUNT = 4 def make_unrolled_sum(n): @compile def sum_unrolled(arr: array[i32, n]) -> i32: result: i32 = 0 # Python loop runs at compile-time, generates n add operations for i in range(n): result = result + arr[i] return result return sum_unrolled # Generate specialized unrolled versions sum_4 = make_unrolled_sum(4) sum_8 = make_unrolled_sum(8)
Library: Polymorphism
PythoC provides compile-time and runtime polymorphism through the Poly library:
from pythoc import compile, i32, f64, ptr, i8, enum from pythoc.std.poly import Poly # Define specialized implementations (must have same return type) @compile def add_i32(a: i32, b: i32) -> i32: return a + b @compile def add_f64(a: f64, b: f64) -> i32: return i32(a + b) # Convert to i32 for uniform return type @compile def add_mixed(a: i32, b: f64) -> i32: return i32(f64(a) + b) # Create polymorphic function - dispatches based on argument types add = Poly(add_i32, add_f64, add_mixed) # Static dispatch - type known at compile-time (zero overhead) @compile def test_static(): x = add(i32(10), i32(20)) # Calls add_i32 y = add(1.5, 2.5) # Calls add_f64 z = add(i32(10), 2.5) # Calls add_mixed # Extensible - add implementations dynamically @compile def add_str(a: i32, b: ptr[i8]) -> i32: return 42 add.append(add_str) # Runtime dispatch via enum boxing @enum(i32) class Number: Int: i32 Float: f64 @compile def test_runtime(): a: Number = Number(Number.Int, 42) b: Number = Number(Number.Float, 3.14) x: i32 = 10 # add(a, b) # Compilation error: missing f64 + i32 overloads add(x, a) # OK, dynamic dispatch if __name__ == "__main__": test_static() test_runtime()
Key features:
- All implementations must have the same return type
- Compile-time dispatch when argument types are statically known
- Runtime dispatch via enum boxing for dynamic polymorphism
- Extensible - add new implementations via
append()
AST Transformations Features: Inline, Yield, Closures
These features share the same underlying implementation: AST-level code transformation and inlining.
Inline Functions
Zero-overhead abstraction - function body is expanded at call site:
from pythoc import compile, inline, i32 @inline def clamp(value: i32, min_val: i32, max_val: i32) -> i32: if value < min_val: return min_val elif value > max_val: return max_val return value @compile def process(x: i32) -> i32: # clamp body is inlined here - no function call result: i32 = clamp(x, 0, 100) return result
Closures
Nested functions with variable capture - inlined automatically:
from pythoc import compile, i32 @compile def make_adder(base: i32) -> i32: offset: i32 = 10 # Closure captures base and offset def add_both(x: i32) -> i32: return x + base + offset return add_both(5) # Closure body inlined at call site
Yield-based Iterators
Generator functions using yield - inlined automatically at call sites:
from pythoc import compile, i32 @compile def fibonacci(limit: i32) -> i32: """Generator yielding Fibonacci numbers < limit""" a: i32 = 0 b: i32 = 1 while a < limit: yield a new_a: i32 = b new_b: i32 = a + b a = new_a b = new_b @compile def sum_fibonacci(n: i32) -> i32: total: i32 = 0 for num in fibonacci(n): # fibonacci body inlined here total = total + num return total
Common kernel: All three features use the same AST transformation and inlining kernel, maintaining C-level performance with high-level abstractions.
More Examples
An under development pure pythoc C header parser in pythoc/bindings to implement the cimport feature.
More examples can be found in the test/ directory.
- Full examples:
test/example/ - PythoC Integration tests:
test/integration/
Documentation
See docs/ for detailed documentation.
License
MIT License - see LICENSE file for details
Contact: yfleiii@gmail.com