
Compiler Series: Zin Language Design
Outlining the core ideas behind Zin.
Before diving into implementation, I want to outline the core ideas behind Zin. The language is designed around one principle: everything is a type.
Everything is a type
I always found a language wierd if it does not support the same set of operations on base types as it does on complex types. That goes for C/C++, Go, Zig and a lot of other languages too! I remember watching a video of Chris Lattner and he also explained this for the Swift language: Int in Swift for example, is implemented as a struct, not what other languages would call a primitive. You can view the Swift source code for Int here. It is a template so it's a little messy to read but it ends up as something like this:
public struct Int : FixedWidthInteger, SignedInteger, _ExpressibleByBuiltinIntegerLiteral {
// Under the hood, Int is just a wrapper around an LLVM primitive integer
@usableFromInline
internal var _value: Builtin.Int64
@_transparent
public init(_ _v: Builtin.Int64) {
self._value = _v
}
// math operations and other functions which Int supports
}
This to me seems like a much better design because everything is unified. You know what you can do in this language with every type. In other languages, you cannot do something like let (quotient, remainder) = 100.quotientAndRemainder(dividingBy: 3). In other words, operations you can do on primitives do not apply to custom types. I find this wierd and I find it better to have a unified type system like Swift or Rust have.
With that being said, since this will be a VM backed language, there are no special VM opcodes for arithmetic, I/O, or any built-in operations. Addition isn't a bytecode instruction, rather it's a method on Int. File I/O isn't a syscall wrapper, it's a method on File. Every operation goes through the same type system.
This means native types and language-defined types work the same way. An Int is a type with an add method backed by a native implementation. A File is a type with a write method backed by a native implementation. User-defined types follow the exact same pattern.
Types and impl blocks
Types are defined with type, and their methods go in impl blocks:
type File {
_value: NativeFile
}
impl File {
fn from_fd(self, fd: Int): Void {
self._value = self._value.from_fd(fd._value)
}
fn open(self, path: String): Void {
self._value = self._value.open(path._value)
}
fn write(self, data: String): Void {
self._value.write(data._value)
}
fn read(self, size: Int): String {
result: String
result._value = self._value.read(size._value)
return result
}
fn close(self): Void {
self._value.close()
}
}
The _value field holds the native backing type. This is how Zin will bridge language types to native implementations. Language type is the public API, the native type does the actual work. The goal is to have a core of some sort, and then to implement types which you would usually consider primitive (int, float, maybe even string, array etc) in this core. Then, once that is done, you can implement your whole standard library by using this core and utilizing some added builtin functions from the compiler.
Hello World
Hello world should for the beginning stages be simple. Create a file, assign it a file descriptor for STDOUT and print a value:
import "std/io"
fn main(): Void {
stdout: io.File
stdout.from_fd(1)
stdout.write("Hello World!")
stdout.close()
}
No magic print function. You open a file descriptor, write to it, close it. Everything is explicit.
Also as you can see, I am a big fan of Go's style for packages/modules. Importing based on file system location and treating it like a namespace once imported. It is a very simple and effective way of namespacing the code in reusable components.
Enums
I like Rust enums where variants can carry data (also similar to a Zig tagged union):
enum Option {
Some(value: Type)
None
}
This gives you sum types for error handling and nullable values without needing null pointers or exceptions.
Why this design
The unified type approach means:
- No special cases - the VM doesn't need dedicated opcodes for every primitive operation. It just needs to call methods on types.
- Clean native interop - wrapping a native type is the same pattern whether it's an integer, a file, or a socket.
- Extensibility - adding a new built-in type doesn't require changing the VM. Just define a type with a native backing.
Stack-based VM
The VM will be stack-based to start. It's the simplest architecture to implement. You push values to the stack, pop them, call methods. Once the initial design works, later on I will think about switching to a register based VM but for the initial version, stack is just easier to do IMO.
The goal
The end goal is to create some simple programs with it. I want to be able to write a simple TCP server in it, and eventually an HTTP server on top of it. All those types can be in the standard library.
In the next chapter, I will be implementing a streaming tokenizer so stay tuned.