Zinc is a statically-typed, compiled systems programming language with a focus on clarity, performance, and expressiveness. It compiles to native binaries via LLVM and supports generics, receiver functions (methods), a hygienic macro system, and C interoperability through foreign declarations.
clang- LLVM (with
llvm-configonPATH)
makeThis produces a zinc binary in the current directory.
zinc <file> [options]
| Option | Description |
|---|---|
--emit-llvm |
Output LLVM IR (.ll) instead of a native binary |
-o <file>, --output <file> |
Specify the output file name |
-d, --debug |
Enable debug output |
--unused-variable |
Suppress unused variable warnings |
--unused-function |
Suppress unused function warnings |
--unused-struct |
Suppress unused struct warnings |
Example:
zinc hello.zn -o hello
./hellozinc hello.zn --emit-llvm -o hello.ll| Zinc type | Description |
|---|---|
u0 |
No value (function return) |
u1 |
Boolean |
char |
Character |
i8, i16, i32, i64 |
Signed integers |
u8, u16, u32, u64 |
Unsigned integers |
f32, f64 |
Floating-point |
*T |
Pointer to T |
[T] |
Array of T |
(T, U, ...) |
Tuple |
Explicit type:
i32 x = 42
f64 pi = 3.14159
Inferred type (:=):
x := 42
name := "zinc"
i32 add(i32 a, i32 b) {
return a + b
}
struct Point {
f32 x
f32 y
}
Struct literal:
Point p = Point { x: 1.0, y: 2.0 }
A receiver function takes an explicit receiver after the parameter list. The syntax is:
ReturnType functionName(params) ReceiverType self { ... }
f32 length(f32 scale) Vec self {
return scale * self.x
}
// Call:
v.length(2.0)
Receiver with pointer:
void push(i32 item) *Vec self {
self.data[self.len] = item
self.len = self.len + 1
}
Static-style receiver (no instance arguments):
u0 Point:print() {
printf("Point\n")
}
// Call:
Point:print()
Generic structs:
struct Vec[T] {
*T data
u64 len
u64 cap
}
struct Pair[K, V] {
K key
V value
}
Generic functions:
T max[T](T a, T b) {
if a > b {
return a
}
return b
}
Pair[K, V] makePair[K, V](K key, V value) {
Pair[K, V] p
p.key = key
p.value = value
return p
}
Generic receiver functions:
void push[T](T item) *Vec[T] self {
self.data[self.len] = item
self.len = self.len + 1
}
Usage:
Vec[i32] numbers
numbers.push(10)
numbers.push(20)
result := max(a, b)
If / else:
if x > 0 {
return x
} else {
return -x
}
While:
while i < 10 {
i = i + 1
}
For:
for i := 0; i < n; i = i + 1 {
// ...
}
Break / continue / return / defer:
while true {
if done { break }
defer cleanup()
// ...
}
Goto and labels:
start:
if x < 0 { goto end }
x = x - 1
goto start
end:
i32 x = 5
i32 y = 10
swap(&x, &y) // pass by pointer
*i32 ptr = &x
i32 val = *ptr // dereference
[i32] arr = [1, 2, 3, 4, 5]
i32 first = arr[0]
tuple := (1, 2, 3, 4, 5)
type PairList[K, V] Vec[Pair[K, V]]
type NodePtr[T] *Node[T]
Zinc support only two types of visibility: Public: The declaration is public everywhere in every other file. Private: Visible only in the current file.
All file-level declaration supports the keyword pub before the declaration.
pub struct Point { ... } // explicit pub annotation also supported
pub void Push(i32 item) ... // public method
void internal() ... // private
Import another Zinc file with use:
use "path/to/Module"
All public symbols from the imported file become available in the current scope.
Declare external C functions with foreign:
foreign void printf(*char, ...)
foreign void putchar(u8)
These can then be called like normal Zinc functions.
Zinc macros use pattern matching to expand to arbitrary code at compile time. Patterns use:
$name— captures an expression or statement block@name— captures an identifier (introduces a new variable)- Literal keywords for fixed tokens
macro unless $cond $body -> {
if !($cond) {
$body
}
}
macro for @item from $start to $end $body -> {
@item := $start
while @item < $end {
$body
@item = @item + 1
}
}
Usage:
unless is_valid {
return
}
for i from 0 to 10 {
print(i)
}
Macros can use goto and labels internally, making them capable of expressing complex control structures:
macro while $cond $body {
entry:
if !($cond) goto end_while
$body
goto entry
end_while:
}
foreign u0 printf(*char)
u32 main() {
printf("Hello, World!\n")
return 0
}
foreign u0 printf(*char)
struct Point {
f32 x
f32 y
}
u0 Point:print() {
printf("Point\n")
}
u32 main() {
Point p = Point { x: 10, y: 10 }
Point:print()
return 0
}
struct Vec[T] {
*T data
u64 len
u64 cap
}
void push[T](T item) *Vec[T] self {
self.data[self.len] = item
self.len = self.len + 1
}
void main() {
Vec[i32] numbers
numbers.len = 0
numbers.cap = 0
numbers.push(10)
numbers.push(20)
numbers.push(30)
}
struct range {
i64 start
i64 end
i64 current
}
i64 next() *range self {
self.current = self.current + 1
return self.current
}
bool hasNext() *range self {
return self.current < self.end - 1
}
range Range(i64 start, i64 end) {
return range { start: start, end: end, current: 0 }
}
| Phase | File | Responsibility |
|---|---|---|
| Lexical | zlex.c |
Tokenize source text; keyword hashing |
| Syntax | zparse.c |
Build AST from token stream |
| Semantic | zsem.c |
Type checking, scope and symbol resolution |
| Generate | zgen.c |
Emit LLVM IR; compile to native binary |
Supporting components:
zmod.c— handlesuseimports, multi-file compilation, debug printingzmacro.c— expands macro invocations before/during parsing using pattern matchingzmem.c— arena allocator; scoped cleanup withstartScope()/endScope()zvec.h— header-only generic dynamic array used throughout the compilerzhset.h— header-only string hashset for duplicate detection in symbol tables
BSD 3-Clause License. See LICENSE.