
Learning Jai via Advent of Code
February 13th, 2023
Last year I got into the Jai beta. One of my favorite ways to learn a new language is via Advent of Code
- (2019) Learning Rust via Advent of Code
- (2022) Failing to Learn Zig via Advent of Code
- (2023) Learning Jai via Advent of Code
The Jai beta rules are that I am free to talk about the language and share my code. However the compiler is not to be shared.
This is the type of topic where folks have, ahem, strong opinions. Be kind y'all.
Table of Contents
- What is Jai
- Advent of Code
- Memory Management
- Big Ideas
- Medium Ideas
- What do I think about Jai?
- Conclusion
- Bonus Content
What is Jai
Here is my attempt to define Jai. This is unofficial and reflects my own biases.
Jai is a systems language that exists in-between C and C++. It is compiled and statically typed. It strives to be "simple by default". It focuses on problems faced by games.
It's worth pointing out several that Jai does NOT have or do.
- NO destructors or RAII
- NO garbage collection
- NO runtime
- NO virtual functions
- NOT a "memory safe" language
The Philosophy of Jai is not explicitly written down. However the tutorials are full of philosophy and perspective. Based on tutorials, presentations, Q&As, and community I think the Philosophy of Jai's looks very vaguely like this:
- C++ is super overcomplicated, but does some things right
- Jai is what C++ should have been
- Focus on games and real problems they face
- Increasingly high-level languages haven't increased productivity
- Design to solve hard problems
- Avoid small features that only solve easy problems
- Game logic bugs are harder and more frequent than memory bugs
- Modern "memory safe" techniques are too rigid for games
- Debug-mode run-time checks can detect most memory bugs
- Experiment with new ideas in a small, closed beta
I think it's also critical to note that Jai:
- is NOT a finished language
- has experimental ideas that may turn out to be bad
- has papercuts, rough edges, and incomplete features
Please take all that with a grain of salt. These are my early impressions.
Blog Target Audience
I always try to write with an explicit audience in mind. Sometimes it's a deeply technical article for professional programmers. Other times it's for the "dev curious" gamers who want a peak under the hood.
This post has been very difficult to frame. Who is this blog post for? What do I want them to take away? Should this be an objective description of the language or my subjective opinion? So many choices!
Here's what I settled on:
- write for game devs who haven't seen Jai code
- focus on language concepts I think are interesting
The list of things this post is NOT is much longer:
- NOT for Jai beta users
- NOT a Jai tutorial
- NOT a comprehensive guide to Jai
- NOT an argument for or against C/C++/Rust/etc
- NOT a compare and contrast to other languages
- NOT an argument that you should convert to Jai
Who am I
This post is highly subjective and biased so I think it's important to understand where I'm coming from.
I have 15+ years professional experience working in games and VR. I've shipped games in Unity, Unreal, and multiple custom engines. Most of my career has been writing C++ or Unity C#. I've written some Python and Lua. I <3 Rust. In games I worked on projects with 15 to 40 people. I worked on systems such as gameplay, pathfinding, networking, etc. I currently work in BigTech Research.
I'm not a language programmer. I don't know anything about webdev. I think C++ is terrible, but it's shipped everything I've ever worked on. JavaScript is an abomination. I have thoughts on Python and global variables. I don't know anything about backends.
This post is just, like, my opinion, man.

Getting Started with Jai
Jai is very easy to run. The beta is distributed as a vanilla zip file. It contains:
jai.exe
compilerlld.exe
linker- default modules aka "standard library"
- 65 "how to" tutorials
- a handful of examples
That's basically it. Compiling and running a Jai program is as easy as:
- Run
jai.exe foo.jai
- Run
foo.exe
On Windows the compiler will also produce `foo.pdb` which allows for full breakpoint and step debugging in Visual Studio or RemedyBG. I'm a card carrying member of #TeamDebugger and hate when new languages only offer printf
for debugging.
Advent of Code
I am pleased to say that this year I successfully solved all 25 days of Advent of Code exclusively with Jai. This ended up being 4821 lines of code. You can view full source on GitHub.
My solutions are a little sloppy, inefficient, and non-idiomatic. One cool thing about learning a language via Advent of Code is learning from other people's solutions. Unfortunately with Jai being a closed beta I wasn't able to do that this year!
Day 01
Here's my solution to day 01. It's an exceedingly simple program. However I'm assuming most readers have never seen Jai code before.
// [..]u32 is similar to std::vector<u32> // [..][..] is similar to std::vector<std::vector<u32>> day01_part1 :: (elves: [..][..]u32) -> s64 { // := is declare and initialize // var_name : Type = Value; // Type can be deduced, but is statically known at compile-time max_weight := 0; // iterate elves for elf : elves { // sum weight of items carried by elf elf_weight := 0; for weight : elf { elf_weight += weight; } // find elf carrying the most weight max_weight = max(elf_weight, max_weight); } return max_weight; }
This code should be easy to understand. The syntax may be different than you're used to. It's good, just roll with it for now.
Day 04
Here's most of my solution to day 4.
day04_solve :: () { // Read a file to a string input, success := read_entire_file("data/day04.txt"); assert(success); // Solve puzzle and print results part1, part2 := day04_bothparts(input); print("day04 part1: %\n", part1); print("day04 part2: %\n", part2); } // Note multiple return values day04_bothparts :: (input : string) -> u64, u64 { part1 : u64 = 0; part2 : u64 = 0; while input.count > 0 { // next_line is a helper function I wrote // this does NOT allocate. it "slices" input. line : string = next_line(*input); // split is part of the "standard library" // splits "###-###,###-###" in two "###-###" parts elves := split(line,","); assert(elves.count == 2); // declare a helper function inside my function // converts string "###-###" to two ints get_nums :: (s : string) -> int, int { range := split(s, "-"); assert(range.count == 2); return string_to_int(range[0]), string_to_int(range[1]); } a,b := get_nums(elves[0]); x,y := get_nums(elves[1]); // more helpers contains :: (a:int, b:int, x:int, y: int) -> bool { return (a >= x && b <= y) || (x >= a && y <= b); } overlaps :: (a:int, b:int, x:int, y: int) -> bool { return a <= y && x <= b; } // Jai loves terse syntax // This style is encouraged if contains(a,b,x,y) part1 += 1; if overlaps(a,b,x,y) part2 += 1; } return part1, part2; }
This code should also be easy to understand. Compared to C there are a few new features such as nested functions and multiple return values.
Advent of Code is pretty simple. The solutions are well defined. I didn't write a 5000 line Jai program. I wrote 25 Jai tiny programs that are at most a few hundred lines.
Advent Summary
I was going to share more AoC snippets but they're "more of the same". It's all on GitHub if you'd like to look.
Would I recommend AoC to learn Jai? Absolutely! It's a great and fun way to learn the basics of any language, imho.
Would I recommend Jai for competitive programming? Definitely not. Jai isn't designed for leet code. This year's AoC winner invented their own language noulith which is pure code golf sugar. I kinda love it. But that ain't Jai.
If you want to solve puzzles fast use Python. If you want puzzle solutions that run fast use Rust. If you want to learn a new language then use anything.
Memory Management
I want to discuss cool language features. I think it's necessary to go over memory management first.
I. No garbage collection
Jai does not have garbage collection. Memory is managed manually by the programmer.
II. No Destructors
Jai has no destructors. There is no RAII. Users must manually call free on memory or release on handles. Jai does have defer
which makes this a little easier.
// Create a new Foo that we implicitly own and must free foo : *Foo = generate_new_foo(); // free the Foo when this scope ends defer free(foo);
No destructors is very much like C and not at all like C++. If you're a C++ programmer you may be recoiling in horror. I beg you to keep an open mind.
III. Categories of Lifetimes
Jai documentation spends over 7000 words describing its philosophy on memory management. That's longer than my entire blog post! I can't possibly summarize it here.
One subsection resonated with me. Jai theorizes that there are roughly four categories of lifetimes.
- Extremely short lived. Can be thrown away by end of function.
- Short lived + well defined lifetime. Memory allocated "per frame".
- Long lived + well defined owner. Uniquely owned by a subsystem.
- Long lived + unclear owner. Heavily shared, unknown when it may be accessed or freed.
Jai thinks that most program allocations are category 1. That category 4 allocations should be rare in well written programs. And that C++ allocators and destructors are focused on supporting cat 4.
Categories 2 and 3 are best served by arena allocators. Consider a node based tree or graph. One implementation might be to manually `malloc` each node. Then `free` each node individually during shut down. A simpler, faster alternative would be to use an arena and `free` the whole thing in one simple call.
These categories are NOT hard baked into the Jai language. However the ideas are heavily reflected.
IV. Temporary Storage
Jai provides standard access to both a "regular" heap allocator and a super fast temporary allocator for category 1 allocations.
The temp allocator is a simple linear allocator / bump allocator. An allocation is a simple increment into a block of memory. Objects can not be freed individually. Instead the entire block is reset by simply resetting the offset to zero.
silly_temp_print :: (foo: Foo) -> string { // tprint allocates a new buffer // uses the temp allocator return tprint("hello world from %", foo) } // no destructor means we have to deal with memory msg : string = silly_temp_print(my_foo); // awhile later (maybe once per frame) reset_temporary_storage();
In this example we printed a string
using the temp
allocator via tprint
. The function silly_temp_print
doesn't own or maintain the string
. The caller doesn't need to manually call free
because it knows that reset_temporary_storage()
will be called at some point.
The concept of a temporary allocator is baked into Jai. Which means both library authors and users know it exists and can rely on it.
Most allocations in most programs are very short lived. The goal of the temporary alloactor is to make these allocations super cheap in both performance and mental effort.
Big Ideas
At this point we've seen some simple Jai code and learned a little bit about its memory management rules. Now I want to share Jai features and ideas that I think are interesting.
This is explicitly NOT in tutorial order. Can Jai do all the basic things any language can do? Yes. Am I going to tell you how? No.
Instead I'm going to share more advanced ideas. These are the types of things you don't learn on day 1 but may provide the most value on day 1000.
1. Context
In Jai every procedure has an implicit context
pointer. This is similar to how C++ member functions have an implicit this
pointer.
The context contains a few things:
- default memory allocator
- temporary allocator
- logging functions and style
- assertion handler
- cross-platform stack trace
- thread index
This allows useful things like changing the memory allocator or logger when calling into a library. contexts
can also be pushed.
do_stuff :: () { // copy the implicit context new_context := context; // change the default allocator to arena new_context.allocator = my_arena_allocator; // change the logger new_context.logger = my_cool_logger; new_context.logger_data = my_logger_data; // push the context // it pops when scope completes push_context new_context { // new_context is implicitly passed to subroutine // subroutine now uses our allocator and logger call_subroutine(); } }
The context
struct can be extended with additional user data. However this doesn't play nice with .dlls
so there are still design problems to solve.
I hate globals with a fiery passion. I wish all languages had a context
struct. It's quite elegant.
2. Directives
One of the most powerful ideas I've seen in Jai is the rampant use of compiler directives.
Here's an example of #complete
which forces an enum switch to be exhausive at compile-time.
// Declare enum Stuff :: enum { Foo; Bar; Baz; }; // Create variable stuff := Stuff.Baz; // Compile Error: This 'if' was marked #complete... // ... but the following enum value was missing: Baz if #complete stuff == { case .Foo; print("found Foo"); case .Bar; print("found Bar"); }
This is a simple but genuinely useful example. Languages spend a lot of time bikeshedding syntax, keywords, etc. Meanwhile Jai has dozens of compiler directives and they're seemingly added willy nilly.
Here are some of the directives currently available.
#add_context inject data into context #as struct can cast to member #asm inline assembly #bake_arguments bake argument into function #bytes inline binary data #caller_location location of calling code #c_call c calling convention #code statement is code #complete exhaustive enum check #compiler interfaces with compiler internals #compile_time compile-time true/false #cpp_method C++ calling convention #deprecated induces compiler warning #dump dumps bytecode #expand function is a macro #filepath current filepath as a string #foreign foreign procedure #library file for foreign functions #system_library system file for foreign functions #import import module #insert inject code #intrinsic function handled by compiler #load includes target file #module_parameters declare module "argument" #must return value must be used #no_abc disable array bounds checking #no_context function does not take context #no_padding specify no struct padding #no_reset global data persists from compile-time #place specify struct member location #placeholder symbol will be generated at compile-time #procedure_name acquire comp-time procedure name #run execute at compile-time #scope_export function accessible to whole program #scope_file function only accessible to file #specified enum values must be explicit #string multi-line string #symmetric 2-arg function can be called either way #this return proc, struct, or data type #through if-case fall through #type next statement is a type
They do a lot of things. Some simple things. Some big things we'll learn more about.
What I think is rad is how game changing they are given how easy they are to add. They don't require bikeshed committees. They can be sprinkled in without declaring a new keyword that breaks existing programs. They appear to make it radically easier to elegantly add new capabilities.
3. Runtime Reflection
Jai has robust support for run-time reflection. Here is a very simple example.
Foo :: struct { a: s64; b: float; c: string; } Runtime_Walk :: (ti: Type_Info_Struct) { print("Type: %\n", ti.name); for *ti.members { member : *Type_Info_Struct_Member = it; member_ti : *Type_Info = member.type; print(" %", member.name); print(" % bytes", member_ti.runtime_size); mem_type := "unknown"; if member_ti.type == { case .INTEGER; mem_type = "int"; case .FLOAT; mem_type = "float"; case .STRING; mem_type = "string"; case .STRUCT; mem_type = (cast(*Type_Info_Struct)member_ti).name; } print(" %\n", mem_type); } } Runtime_Walk(type_info(Foo));
At run-time this will print:
Type: Foo a 8 bytes int b 4 bytes float c 16 bytes string
This is an exceedingly powerful tool. It's built right into the language with full support for all types - primitives, enums, structs, procedures, etc.
4. Compile Time Code
Jai has extremely powerful compile time capabilities. Like, frighteningly powerful.
First, here is a small syntax tutorial.
// this is compile-time constant because :: // you may have noticed that structs, enums, // and procedures have all used :: foo :: 5; // this is variable because := bar := 5;
#run
Here's a super basic example of compile-time capabilities.
factorial :: (x: int) -> int { if x <= 1 return 1; return x * factorial(x-1); } // :: means compile-time constant // note the use of #run x :: #run factorial(5); print("%\n", x); // compile error because factorial(5) is not constant // y :: factorial(5); // executes at runtime z := factorial(5);
Any code that has #run
will be performed at compile-time. It can call any code in your program. Including code that allocates, reads files from disk, etc. 🤯
#insert
The #insert
directive lets you insert code. Here's a toy example.
// runtime variable x := 3; // insert string as code #insert "x *= 3;"; // runtime value of x is 9 print("%\n", x);
This inserts the string "x *=3;"
as code. This is silly because we could have just written that code like a normal person. However we can combine #insert
with #run
and do things that are starting to become interesting.
// runtime variable x := 3; // helper to generate a string that represents code gen_code :: (v: int) -> string { // compile-time string alloc and format! return tprint("x *= %;", v); } // generate and insert x *= 3; #insert #run gen_code(3); print("%\n", x); // prints 9 // compile-time run factorial(3) to produce 6 // insert code x *= 6 #insert #run gen_code(factorial(3)); print("%\n", x); // print 54
We can insert arbitrary strings
as code. At compile-time we can execute arbitrary Jai code that generates and inserts strings
. 🤯🤯
#code
Thus far we've been operating on strings
. Jai can also operate with type safety.
// runtime variable x := 3; // #expand makes this a "macro" so it can // access variables in its surrounding scope do_stuff :: (c: Code) #expand { // splat the code four times #insert c; #insert c; #insert c; #insert c; } // generate a snippet of code c : Code : #code { x *= 3; }; // at compile-time: expand do_stuff macro // at run-time: execute code four times do_stuff(c); // prints 243 print("%\n", x);
Here we wrote x *= 3;
and stored it in a variable of type Code
. Then we wrote the macro do_stuff
which copy pastes our snippet four times. And we did this with "proper" data types rather than strings
. 🤯🤯🤯
Abstract Syntax Tree
At compile-time the Code
type can be converted to Abstract Syntax Tree nodes, manipulated, and converted back.
// our old friend factorial :: (x: int) -> int { if x <= 1 return 1; return x * factorial(x-1); } // function we're going to #run at compile-time comptime_modify :: (code: Code) -> Code { // covert Code to AST nodes root, expressions := compiler_get_nodes(code); // walk AST // multiply number literals by their factorial // 3 -> 3*factorial(3) -> 3*6 -> 18 for expr : expressions { if expr.kind == .LITERAL { literal := cast(*Code_Literal) expr; if literal.value_type == .NUMBER { // Compute factorial fac := factorial(literal._s64); // Modify node in place literal._s64 *= fac; } } } // convert modified nodes back to Code modified : Code = compiler_get_code(root); return modified; } // modify and duplicate code do_stuff :: (code: Code) #expand { // modify the code at compile-time new_code :: #run comptime_modify(code); #insert new_code; #insert new_code; #insert new_code; #insert new_code; } // same as before x := 3; c :: #code { x *= 3; }; do_stuff(c); // prints 3*18*18*18*18 = 314,928 print("%\n", x);
In this example we:
- Declare the code
x *= 3;
- Compile-time modify the compiler parsed AST to
x *= 18;
- Insert
x *= 18;
four times
We did all of this with code that looks and runs like regular vanilla Jai code. It isn't a new macro language using #define
string manipulation or complex macro_rules!
syntax. 🤯🤯🤯🤯
The possibilities for this are endless. Frighteningly endless even. Excessive compile-time code is more complex and harder to understand.

Spark of Joy: assert_eq
I want to make a small detour share a small moment where Jai really sparked my joy.
For Advent of Code I wrote a simple assert_eq
macro I was proud of. It prints both the values that failed to match and also the expression that produced the value.
assert_eq :: (a: Code, b: Code) #expand { sa := #run code_to_string(a); va := #insert a; sb := #run code_to_string(b); vb := #insert b; assert(va == vb, "(left == right)\n left: % expr: %\n right: % expr: %\n loc: %\n", va, sa, vb, sb, #location(a)); } assert_eq(42, factorial(3)); // stderr: // C:/aoc2022/main.jai:154,5: Assertion failed: (left == right) // left: 42 expr: 42 // right: 6 expr: factorial(3) // loc: {"C:/aoc2022/main.jai", 85, 15}
It prints the value and also the code that produced the value. That's neat. It made me happy. It was a fun moment.
Rust has convenient built-ins like #[derive(Hash)]
. The community has built ultra powerful libraries like serde
. Jai doesn't an ecosystem of similar libraries yet. I believe the powerful compile-time capabilities should make them possible. I'm very curious to see what folks come up with.
Medium Ideas
Now we're at the phase of what I'll call "medium impact ideas". These ideas are super important, but perhaps a little less unique.
Polymorphic Procedures
Jai does not have object oriented polymorphism ala virtual
functions. It does have "polymorphic procedures" which are Jai's version of templates
, generics
, etc. Naming things is hard. This name may change.
Here's a basic example:
// Jai square :: (x: $T) -> T { return x * x; } // C++ equivalent template<typename T> T square(T x) { return x * x; }
The $T
means the type will be deduced at compile-time. Like most compiled languages this function is compiled for all necessary types and there is no dynamic dispatch or run-time overhead.
Polymorphic procedure syntax has some subtle niceties to improve compiler errors.
// compile error. $T can only be defined once. foo :: (a: $T, b: $T); // deduce from array type array_add1 :: (arr: [..]$T, value: T); // deduce from value type (gross) array_add2 :: (arr: [..]T, value: $T); // dynamic array of ints nums: [..] int; // Error: Type mismatch. Type wanted: int; type given: string. array_add1(*nums, "not an int"); // Error: Type mismatch. Type wanted: *[..] string; type given: *[..] int. array_add2(*nums, "not an int");
Explicitly specifying which argument defines the type T
is intuitive. It also enables much better error messages for compiler errors.
Polymorphic Structs
structs
can also be declared polymorphically.
Vector :: struct($T: Type, $N: s64) { values : [N]T; } // a simple vector of 3 floats Vec3 :: Vector(float, 3); v1 : Vec3 = .{.[1,2,3]}; // a big vector of 1024 ints (for some reason) BigVec :: Vector(int, 1024); v2 : BigVec = .{.[/*omitted*/]};
You can use this for simple types, vectors
, hash_maps
, etc. It's an important example of how Jai is C+ and not C.
Build System
Build systems are a pain in the ass. Some languages require a configuration language just to define their build. Make, MSBuild, Ninja, etc. This build language may or may not be cross-platform.
Jai provides a built-in build system that uses vanilla Jai code. Here is a very simplified look.
build :: () { // workspace is a "build mode" like debug, release, shipping, etc w := compiler_create_workspace(); // get CLI args args := options.compile_time_command_line; // set some flags and stuff option := get_build_options(); for arg : args { if arg == "-release" { options.optimization_level = .RELEASE: } } set_build_options(options, w); // specifiy target files // compiler auto-starts in background add_build_string(TARGET_PROGRAM_TEXT, w); add_build_file("extra_file.jai", w); } // invoke at compile-time via #run #run build();
Jai ships default_metaprogram.jai
and minimal_metaprogram.jai
so users do not have to manually write this every time. Larger programs will inevitably have their own build systems to perform more complex operations.
Jai's approach to a build systems is interesting. Having a real programming language is great. It's better than cmake
hell.
My professional life is a polyglot of languages (C, C++, C#, Rust, Python, Matlab, Javascript) and operating systems (Windows, macOS, Linux, Android, embedded) and devices (PC, Quest, Hololens, embedded) and environments (Unity, Unreal, Matlab, Jupytr, web). Kill me. â˜
C++ is under-defined and doesn't define any build system. Modern languages have rectified this and it's much better.
Unfortunately I don't think anyone has solved "the build system problem" yet. I'm not sure it can be "solved". :(
Small Ideas
This post is getting too long. I'm going to punt "Small Ideas" to the end of the post as a "bonus section"
What do I think about Jai?
So, what do I think about Jai? Numerous folks have asked me this. It's incredibly hard to answer.
Q: Do I enjoy writing Jai?
A: Mostly. The language is good. Learning it is not difficult. The community is very helpful. The minimalistic standard library forces you to write lots of little helpers which is an annoying short-term hurdle.
Q:Would I use Jai in production?
A: No, of course not! It's an unfinished language. The compiler is not for distribution. It's not ready for production.
Q: Would I rather use Jai than C?
A: Yes, I think so. Jai being "C plus" is effectively a super set. I prefer C+ to C.
Q: Would I rather use Jai than C++?
A: That's a harder question. Sometimes maybe but not yet? There are numerous Jai features that I desperately want in C++. A sane build system, usable modules, reflection, advanced compile-time code, type-safe printf
, and more.
I'm intrigued by Jai's style of memory management. I'm not fully sold just yet. Jai is chipping away at the ergonomics but hasn't cracked it yet.
Ask me again in 2 years.
Q: Would I rather use Jai than Rust?
A: I think Rust is really good at a lot of things. For those things I would probably not prefer Jai.
I also think Rust still sucks for some things, like UI and games. There's cool projects trying to fix that. They're not there yet. Since Rust is not a good fit I would prefer Jai for such projects.
Q: Would I rather use Jai than Carbon, Cppfront, etc?
A: Yes. Lots of people are trying to replace C++. I understand the appeal of "replace C++ but maintain compatibility with C++ code". It's very practical. That path doesn't appeal to me. I don't want a shiny new thing to be tightly coupled to and held down by the old poopy thing. I want the new thing to be awesome!
Q: Would I rather use Jai than JavaScript?
A: I'd rather serve coffee than write JavaScript.
Q: How is the Jai community?
A: Small but very friendly and helpful. Every Discord question I ever asked got answered. They really hate Rust to a kinda weird degree. There will be growing pains.
Q: When will Jai be publicly available?
A: I have no idea. I feel like the language went dark for awhile and it's starting to wake up. I think it'll be awhile.
Q: Do I think Jai's closed development is wise?
A: No idea. I support anyone willing to do something a different way.
Q: Will Jai take off?
A: Who knows! Most languages don't. Betting against Jai is a safe bet. It's also a boring bet. Jai is ambitious. If every ambitious project succeeded then they weren't actually ambitious.
Jai will be popular with the Handmade community. I expect folks will produce cool things. Jai will get off the ground. It may or may not hit escape velocity and reach the stars. I'm rooting for it!
Q: Will Jai take over the games industry?
A: Nah. Unreal Engine, Unity, and custom game engines are too entrenched. Call of Duty and Fortnite aren't going to be re-written in Jai. I don't expect "Rewrite it in Jai" to be a big thing.
Building a game engine is hard. Not many studios are capable of it. Maybe Jai will allow some mid sized indies to more effectively build custom engines to make games that Unity/Unreal are a bad fit for? That'd be a very successful outcome I think.
Q: Will Jai develop a healthy niche?
A: Strong maybe! It could be cool for tools. If enough useful tools get built then it could be cool for more things.
Designers and artists don't care what language their tools were made in. Being 15% better isn't enough to overcome inertia. It needs to be at least 50% better. Maybe even 100% better. That's tough.
Q: Can you re-write parts of your game in Jai?
A: Ehhh I dunno. C ABI is the lingua franca of computer science. Jai can call C code and vice versa. You could write a Jai plugin for Unity or a custom engine. I'm not sure that provides enough value.
Q: Does Jai have papercuts, bugs, and missing features?
A: Absolutely. What are they? I don't think it's appropriate to publicly blast an unfinished language that invited me to a closed beta. I don't want to pretend it's all roses and sunshine. But I don't want to publicly air my grievences either. It's a tough balance.
Q:Will Jai be useful for non-games?
A: Good question. Probably yes? It doesn't exclude other use cases. But Jai is definitely optimizing for games and game problems.
Q: Will Jai make everyone happy?
A: No, of course not. Will it make some people happy? Yeah definitely.
Q: What should readers think about Jai?
A: That's up to you. I don't want to tell you what to think. I tried to paint a picture of Jai based on my experiences. Your interpretation of that is up to you.
Conclusion
Let's wrap this up.
Coming into all this I knew little to nothing about Jai. I got into the beta. Participated in Advent of Code. Read all the tutorials. And watched some videos.
I feel like I have decent grasp of Jai's basics. I can see some of the things it's trying to do. It would take working on a big project to see if its ideas payoff.
The Jai beta is still very small. If you've got a project you'd like to write or port to Jai you can probably get yourself in. If you just want to poke around for 30 minutes you'd be better off waiting.
I love the solution space that Jai is exploring. I'm onboard with its philosophy. It has some genuinely good ideas and features. It's a very ambitious project. I hope it achieves its lofty goals.
Thanks for reading.
(keep scrolling for more bonus content)
Bonus Content
Welcome to the bonus section! There's so many things I want to talk about. For the sake of semi-brevity and narrative I can't say them all. Here's some unsorted and less polished thoughts from the cutting room.
Language vs Standard Library vs Ecosystem
Sometimes I think about languages in 3 parts:
- Language. Syntax, keywords, "built-in" features, etc.
- Standard library. "User mode" code that you could have written yourself but ships with the compiler for convenience.
- Ecosystem. Libraries written by the community.
This post focused almost entirely on language. Jai is still in a small beta. I'll worry about the standard library and ecosystem later.
Jai ships with a very small standard library. It is NOT a "batteries included" language. It wants to avoid compiling tens of thousands of lines of code because you imported a single file.
Ecosystem can make or break language adoption. Python kinda sucks as a language, but the ecosystem is unrivaled. A key selling point of Rust is the vibrant and robust crate ecosystem. I don't know what approach Jai will ultimately take.
Compile Times
One of Jai's selling points is fast compile times. They're relatively fast?
Here's a comparison between my 2022 Jai code and my 2021 Rust code. It's not quite apples to apples. But it's pretty similar.
Advent of Code 2022 (Jai) Jai debug full 0.56 seconds Jai release full 1.36 seconds Jai debug incremental 0.56 seconds Jai release incremental 1.36 seconds Advent of Code 2021 (Rust) Rust debug full 13 seconds Rust release full 14 seconds (incremental) Rust debug incremental 0.74 seconds Rust release incremental 0.74 seconds
Jai doesn't do incremental builds. It always compiles everything from scratch. The long term goal is a million lines per second. The compiler is currently not fully multi-threaded.
My impression is that Jai doesn't have any magic secret sauce to fast compile times. How do you make compiling fast? Compile fewer lines of code! Makes sense. But maybe a little disappointing. I wanted magic!
Run-time Performance
I did not do extensive performance testing with Jai. It seems fast?
I ported one solution that was running quite slow to C++ to compare. My C++ version ran slower. This was because C++ std::unordered_map
was slower than Jai Hash_Table
.
I am NOT claiming that Jai is faster than your favorite language. However I am not concerned about Jai's performance long-term. It should be just as fast as other compiled languages. It's idioms may or may not be faster than another language's idioms.
Struct of Arrays
Once upon a time Jai talked about automagic struct-of-arrays compiler features. Those features have been cut. I believe the use cases were so specific that no generalized solution presented itself.
Assembly
I'll be honest, the only assembly I've ever written was in school. It's not my jam.
Jai might have some cool assembly features? I dunno!
result : s64 = 0; #asm { // compiler picks a general purpose register for you // that seems cool? foo : gpr; bar : gpr; // assign some 64-bit qwords mov.q foo, 42; mov.q bar, 13; // foo += bar add foo, bar; // result = foo mov result, foo; } print("%\n", result); // prints: 15
Jai will allocate registers for you. If it runs out of registers it's a compiler error. Seems nice?
Safety
Jai is NOT a memory safe language. Debug mode checks array bounds which is nice. But that's about it?
There's no compile-time borrow checker. There's no checks for memory leaks or use after free. It's the C/C++ wild west for better or worse.
There's also no special safety for multi-threading either. You're on your own just like in C/C++.
There's also no mechanisms for async code.
Modules
Jai imports modules. I think it follows typical module rules? It has some assorted macros.
#import "Foo" // import module #load "bar.jai" // similar-ish to #include #scope_file // code is NOT exposed via import/load #scope_export // code is exposed via import/load
Jai does not have namespaces. It solves the C name collision issue by letting module imports be assigned to a name.
// foo.jai do_stuff :: () { /* omitted */ } // bar.jai do_stuff :: () { /* omitted */ } // main.jai #import"Foo" Bar :: #import "Bar"; do_stuff(); // calls foo.jai do_stuff Bar.do_stuff(); // calls bar.jai do_stuff
Problem solved?
Small Ideas
As previously discussed, here are smaller language features I think are neat, valuable, or uncommon.
Initialized by Default
By default all values in structs are initialized. You can have uninitialized memory but it's strictly opt-in via ---
.
foo : float; // zero Vec3 :: struct { x, y, z: float }; bar : Vec3; // 0,0,0 baz : Vec3 = ---; // uninitialized
There are mechanics for specifying non-zero initial values. However there are no constructors.
No Printf Formatters
Printing in Jai is safe and simple. It doesn't use error prone printf specifier.
Foo :: struct { a : int; b : bool; c : string; d : Bar; } Bar :: struct { x : float; y : string; z : [..]int; } foo := Foo.{ 42, true, "hello", Bar.{ 13.37, "world", .[1,2,3]} }; print("foo: %\n", foo); // prints: {42, true, "hello", {13.37, "world", [1, 2, 3]}}
There are mechanics for fancy formatting. The important thing is that the simple case "just works".
Distinct
Jai has an amazing feature I want so badly in other languages. It's what I would call a "type safe typedef".
// cpp: using HandleA = u32; // rust: type HandleA = u32; HandleA :: u32; // cpp: no equivalent // rust: no equivalent HandleB :: #type,distinct u32; // Functions do_stuff_u :: (h: u32) { /* ... */ } do_stuff_a :: (h: HandleA) { /* ... */ } do_stuff_b :: (h: HandleB) { /* ... */ } // Variables u : u32 = 7; a : HandleA = 42; b : HandleB = 1776; // HandleA converts to u32 // HandleB does not // Assignment u = a; a = u; // a = b; // compile error // b = a; // compile error // u = b; // compile error // b = u; // compile error // procedure takes u32 do_stuff_u(u); do_stuff_u(a); //do_stuff_u(b); // compile error // procedure takes HandleA do_stuff_a(u); do_stuff_a(a); // do_stuff_a(b); // compile error // procedure takes HandleB // do_stuff_b(u); // compile error // do_stuff_b(a); // compile error do_stuff_b(b);
This is exceedingly valuable for handles. I've lost track of how many wrapper structs I've written.
Universal Declaration Syntax
Something you may have noticed is everything is declared the same way.
// compile-time constants use :: my_func :: (v: int) -> int { return v + 5; } my_enum :: enum { Foo; Bar; Baz; } my_struct :: struct { v: int; } my_const_float :: 13.37; my_const_string :: "hello world"; my_func_type :: #type (int) -> int; // variables use := // the type can be explicit or deduced a_func_ptr : my_func_type = my_func; a_float : float = my_const_float; an_int : int = 42; another_int := a_func_ptr(an_int); a_type : Type = string; another_type := type_of(an_int);
This standard syntax is surprisingly nice. It's simple and consistent.
Some languages are exceedingly difficult and ambigious to parse. Jai strives to be easy to parse and to provide access to the AST to users. This will eventually enable very robust and reliable IDE support.
Everyone has their own opinion on syntax. I work in enough languages I don't really care. Any C-ish syntax is easy to get used to. I'll gladly adapt to any rational syntax if it enables better tooling.
Relative Pointers
Jai has first-class relative pointers. They store an offset relative to the pointer's storage address.
Node :: struct { // relative pointer stored in s16 next: *~s16 Node; value: float; } // Declare two nodes a := Node.{null, 1337}; b := Node.{null, 42}; a.next = *b; // can directly dereference relative pointer value := a.next.value; print("rel: % value: %\n", a.next, value); // can convert to absolute pointer abs_ptr := cast(*Node)rel_ptr; print("abs: % value: %\n", abs_ptr, abs_ptr.value); // example output: // rel: r60 (5c_5d6f_ecc8) value: 42 // abs: 5c_5d6f_ecc8 value: 42
Yes you could store an integer offset. What makes it nice is the syntax for using a relative pointer is the same as a regular pointer. The language handles applying the offset for you.
Obvious use cases for this are packing data and deserializing binary blobs without having to run a fixup pass.
No References
Jai doesn't have references. Just value and pointers. It belives that references provide minimal value but significantly increase the complexity of the type system.
No Const
Jai doesn't have the concept of a const pointer or variable. Yes there are compile time constants. But there's no way to declare a run-time variable and say you won't change it.
Maybe by Reference
Arguments are passed to procedure "maybe by reference". This is kinda wild.
Types <= 8 bytes
are passed by value. Types >8 bytes
are passed maybe by reference. The compiler might pass it by a pointer. Or maybe not! Who knows.
However the code must be written as if it were passed by, in C++ parlance, const &
.
do_stuff :: (s: string) { // compiler error // Error: Can't assign to an immutable argument. s.count = 0; // you can make a copy and change that s2 := s; s2.count = 0; }
In Jai the compiler is free to decide whether to pass a pointer or by value. This allows Jai to actually treat args as const & restrict
and assume that no pointer arguments alias. If you want to modify the arg then simply pass an actual pointer.
Treating args as const & restrict
could be problematic. For example the data might be aliased by an evil global or a pointer. The Jai philosophy is don't do that and maybe try to detect it in debug mode by making a copy and comparing bytes at the end.
Data-only Pseudo-inheritance
Jai does not have virtual functions. However it does have some form what I shall call data-only pseudo-inheritance.
// Declare a base class Entity :: struct { id: int = 42; name: string = "bob"; } // Declare a "derived" class" BadGuy :: struct { // base: Entity is a data member #as using base: Entity; health: int = 9000; } // Helper to print an entity's name print_name :: (e: *Entity) { print("Entity: %\n", e.name); } // Declare a bad hombre baddie : BadGuy; // note: not baddie.base.id thanks to `using` print("Id: %\n", baddie.id); // *BadGuy casts to *Entity thanks to #as baddie_ptr : *BadGuy = *baddie; print_name(baddie_ptr); // Can also cast the other way // Don't get this wrong! entity_ptr := baddie_ptr; baddie_ptr2 := cast(*BadGuy)entity_ptr;
The #as
casting is maybe useful. The using
sugar is sometimes nice. It can be used to do some real wacky things that are probably a bad idea.
Custom Iterators
Imagine you have a custom data container and you want to write for value : myContainer
. Jai has a slick mechanism to make this super easy.
// Super dumb Vector with fixed size FixedVector :: struct(ValueType: Type, Size: int) { values : [Size]ValueType; count : int; } // Push a value! push :: (vec: *FixedVector, value: $T) { if vec.count < vec.Size { vec.values[vec.count] = value; vec.count += 1; } } // iterate all values for_expansion :: (vec: FixedVector, body: Code, flags: For_Flags) #expand { // Loop over inner array by count for i : 0..vec.count - 1 { // Must declare `it and `it_index `it := vec.values[i]; `it_index := i; // insert user code #insert body; } } // Declare a vector and push some values myVec : FixedVector(int, 10); push(*myVec, 5); push(*myVec, 1); push(*myVec, 5); push(*myVec, 2); push(*myVec, 5); push(*myVec, 3); // Loop and print for value : myVec print("% ", value); // prints: 5 1 5 2 5 3
It took roughly 5 lines of code to add write a custom for_expansion
for our custom container. We didn't have to declare a helper struct with a million little helper functions and operators.
Let's write a second version that skips values equal to 5
.
// declare a custom iterator skip_fives :: (vec: FixedVector, body: Code, flags: For_Flags) #expand { // perform normal iteration for value, index : vec { // we don't like 5, booo! if value == 5 continue; // declare required `it and `it_index `it := value; `it_index := index; // insert user code #insert body; } } // iterate using skip_fives for :skip_fives v: myVec { print("% ", v); } // prints: 1 2 3
Here we've made a second iteration function called skip_fives
. It iterates across the array but ignores any value equal to 5
. We called it by writing for :skip_fives
.
The End
Congrats for making it to the end. As a reward here is a picture of my dogs, Tank and Karma. They're adorable.
