SMOKE-YFullstack engineer: hardware to deep learing |
When I found out that Jonathan Blow was creating a game using a game engine he wrote, in a programming language he developed, I thought, "Hey, I could probably do that."
"Imitation is not just the sincerest form of flattery-it's the sincerest form of learning." -George Bernard Shaw.
I had written a game engine from scratch, a game/prototype from scratch, but never a compiler. After writing a few interpreters, and reading books such as "Writing an Interpreter in go", "Crafting Interpreters", etc... I went on to design a language after noticing a few issues with c/c++ while making my game engine.
GOAL: remove all objectively bad features about C while adding as little 0 cost abstractions/syntax sugars as possible.
Introducing Zeus, yet another C alternative. Zeus solves all the above issues and you can take a look at the examples here. I want to spend the rest of the essay on a few interesting choices I took while designing and implementing Zeus.
#import "lib/str.zs"
stringVariable :String = #fill "Hello, World"
File lib/str.zs implements the struct String, and #fill fills its members(stringVariable.mem points to an address in the data segment and stringVariable.len holds the length of the string which is computed at compile time).
Holding and printing a backtrace can also be implemented in user space. We just need the compiler to insert certain functions at the start and end of functions. This is a feature yet to be implemented.
Foo *foo = (Foo*)malloc(sizeof(Foo));
//or
if(root->type == ASTType::PROC_CALL){
ASTProcCall *proc = (ASTProcCall*)root;
....
};
Zeus makes such pointer casting easy by introducing the type 'ptr'. Any pointer of any type can cast to ptr and ptr can cast to any type of pointer without explicit casting. Hence, no explicit casting is required when calling malloc as malloc is declared in Zeus as:
malloc :: proc_decl(u64) -> ptr
*) Just mention the entry file which contains the main function. The compiler should be able to find all the files that the main file depends on.
*) Auto-cast should be painless. You don't have to mention the cast on LHS and RHS. For example:
long long x = (long long) y; //why?
Instead in Zeus adding '$' tells the compiler that you would like to cast the expression into the required type.
s64 x = $ y
*) The compiler should be able to figure out the type of the variable when the type of the expression can be found.
In cpp, you would do
auto x = 4;
But in Zeus:
x := 4
I wanted the number of keywords to be as less as possible. They should only be different when they do different things such as switch and if. 'switch' generates a jump table, but 'if' is a cmp instruction. They should not be unified under the same keyword. But for and while can be unified under the same keyword as they do the same thing in the same manner. In Zeus, we have only 1 keyword for loop, which is 'for'.
//C-style for loop
for "outer-loop" g:=0...3{
x = 0
//C-style while loop
for x != 4{
if x == 3{
continue "outer-loop"
}
x = x + 1
}
}
swap :: proc(x,y :u32) -> (u32, u32){
//swapping without 3rd variable
return y, x
}
bar :: proc(){
defer printf("Bye, World\n")
defer {
printf("Another defer statement")
}
}
compTimeProc :: proc #comptime (){
//This will print the license while compiling
license("LICENSE.txt")
log("This is being printed from a procedure being executed at compile time")
log("The VM executing the bytecodes is bootstrapped and written in Zeus!")
}