r/Compilers • u/undercover_fts • 2h ago
Tulpar: a compiled language with Kazakh keywords, Python-like syntax, and a from-scratch x86-64 backend (no LLVM, no libc)
Hi all! I've been working on Tulpar (Тұлпар), a statically typed, Python-flavored language whose keywords and standard library are in Kazakh. It compiles to native x86-64 code. The compiler is written in Rust, with no LLVM and no C compiler. The runtime is no_std and uses raw syscalls, with no libc.
анықта фиб(n: бүтін) -> бүтін:
егер n < 2:
қайтар n
қайтар фиб(n - 1) + фиб(n - 2)
әр і ішінде аралық(10):
жаз(f"фиб({і}) = {фиб(і)}")
(анықта = def, егер = if, қайтар = return, әр … ішінде = for … in, жаз = print, бүтін = int.)
Why Kazakh? [1–2 sentences of your own: why this language, why now, who it's for.]
Language design
- Static types with local inference: x = 5 is a бүтін. Parameter types are required. Return types are inferred, except for recursive functions.
- Python-style indentation, classes with single inheritance and virtual methods, tuples and multiple return values, comprehensions, f-strings, sets, dicts, and lambdas (no closures yet).
- Memory is managed by a conservative mark-sweep GC with size-class spans. There is also optional manual memory inside қолмен: blocks (бөл / босат, pointer arithmetic). Manual blocks can hold references to GC objects, and the GC treats them as roots.
- Strings are UTF-8, but indexing and slicing are by character, so ұзындық("Қазақстан") == 9. Case conversion handles Cyrillic and Kazakh-specific letters.
- Error messages are in Kazakh, with the source line and a caret. Runtime errors print a stack trace with file, line and function.
Implementation
Pipeline: lexer -> parser -> type checker (AST -> typed HIR) -> lowering to a three-address IR with basic blocks -> optimizer -> codegen.
- Optimizer passes: CFG simplification, constant folding, copy propagation, DCE, inlining of small functions, and if-conversion to cmov.
- Register allocation is linear scan with loop-depth weights.
- Codegen emits x86-64 assembly (System V ABI). GNU as and ld then produce a static ELF. So I don't have my own assembler or linker yet.
- The runtime (GC, strings, lists, dicts, formatting, TCP sockets) is no_std Rust on raw syscalls.
- Unicode identifiers work, so you can write the whole program in Cyrillic.
Embeddable interpreter
The compiler is also a library crate with a HIR interpreter, so Tulpar can be embedded in a Rust host. A program can declare сыртқы анықта … (external) functions that the host implements. The interpreter has no file, network or raw-memory access. It limits the number of steps and the recursion depth. It returns errors as Result with a call trace, so the host can roll back a transaction. Host calls are re-entrant. Every test runs in both compiled and interpreted mode, and the outputs must match.
Performance
Wall-clock time of a single run on 8 cores, with CPython 3.14, gcc -O2 and rustc -O for comparison:
- Collatz, 3M numbers: Tulpar 0.59 s, Rust 0.34 s, C 0.47 s, Python 33 s.
- Sieve of Eratosthenes, 2·10⁷: Tulpar 0.13 s, Rust 0.11 s, C 0.10 s, Python 2.2 s.
- Float loop, 10⁸ iterations: Tulpar 0.13 s, Rust 0.12 s, C 0.13 s, Python 18 s.
- fib(42): Tulpar 1.55 s, Rust 0.65 s, C 0.39 s, Python 36 s.
Plain loops are close to C and Rust. Recursion is about 2.4x slower than C, and branchy code is about 1.7x slower. There is no recursion unrolling, vectorization or smarter cmov selection yet.
For testing I use a differential fuzzer that compares random programs between -O0, the optimized build and a Python reference.
Known limitations
- Linux x86-64 only.
- No exceptions (try), no user-defined generics, no closures, and импорт is include-like with no namespaces.
- Integers are 64-bit and wrap on overflow.
- Networking is TCP/IPv4 only, with no TLS or DNS, and single-threaded.
- The GC is single-threaded and conservative, so a false pointer can keep some memory alive.
Questions for you
- Closures: I'd like to add them without making every function value a heap-allocated pair. Any advice on a lightweight lambda-lifting or closure-conversion scheme with a conservative GC?
- Error handling: I'm torn between try/except, Result-style returns and something Go-like. What fits a Python-flavored static language best?
- Replacing as and ld with a built-in assembler and ELF writer: is it worth the effort for a project this size?
Repo (MIT): https://github.com/kuanysh228/tulpar
Feedback on the design is very welcome, especially from people who have done a native backend without LLVM.