Studying Jai by way of Creation of Code
Final 12 months I obtained into the Jai beta. One in every of my favourite methods to study a brand new language is by way of Advent of Code
The Jai beta guidelines are that I’m free to speak in regards to the language and share my code. Nevertheless the compiler is to not be shared.
That is the kind of subject the place people have, ahem, sturdy opinions. Be form y’all.
- What is Jai
- Advent of Code
- Memory Management
- Big Ideas
- Medium Ideas
- What do I think about Jai?
- Conclusion
- Bonus Content
Right here is my try and outline Jai. That is unofficial and displays my very own biases.
Jai is a methods language that exists in-between C and C++. It’s compiled and statically typed. It strives to be “easy by default”. It focuses on issues confronted by video games.
It is value declaring a number of that Jai does NOT have or do.
- NO destructors or RAII
- NO rubbish assortment
- NO runtime
- NO digital capabilities
- NOT a “reminiscence secure” language
The Philosophy of Jai shouldn’t be explicitly written down. Nevertheless the tutorials are filled with philosophy and perspective. Based mostly on tutorials, shows, Q&As, and neighborhood I feel the Philosophy of Jai’s appears to be like very vaguely like this:
- C++ is tremendous overcomplicated, however does some issues proper
- Jai is what C++ ought to have been
- Deal with video games and actual issues they face
- More and more high-level languages have not elevated productiveness
- Design to unravel onerous issues
- Keep away from small options that solely remedy simple issues
- Sport logic bugs are more durable and extra frequent than reminiscence bugs
- Trendy “reminiscence secure” methods are too inflexible for video games
- Debug-mode run-time checks can detect most reminiscence bugs
- Experiment with new concepts in a small, closed beta
I feel it is also essential to notice that Jai:
- is NOT a completed language
- has experimental concepts which will transform dangerous
- has papercuts, tough edges, and incomplete options
Please take all that with a grain of salt. These are my early impressions.
I at all times attempt to write with an specific viewers in thoughts. Generally it is a deeply technical article for skilled programmers. Different occasions it is for the “dev curious” avid gamers who need a peak beneath the hood.
This submit has been very troublesome to border. Who is that this weblog submit for? What do I need them to remove? Ought to this be an goal description of the language or my subjective opinion? So many selections!
This is what I settled on:
- write for recreation devs who have not seen Jai code
- give attention to language ideas I feel are attention-grabbing
The listing of issues this submit is NOT is for much longer:
- NOT for Jai beta customers
- NOT a Jai tutorial
- NOT a complete information to Jai
- NOT an argument for or towards C/C++/Rust/and many others
- NOT a evaluate and distinction to different languages
- NOT an argument that you need to convert to Jai
This submit is extremely subjective and biased so I feel it is vital to know the place I am coming from.
I’ve 15+ years skilled expertise working in video games and VR. I’ve shipped video games in Unity, Unreal, and a number of customized engines. Most of my profession has been writing C++ or Unity C#. I’ve written some Python and Lua. I <3 Rust. In video games I labored on initiatives with 15 to 40 individuals. I labored on methods resembling gameplay, pathfinding, networking, and many others. I at present work in BigTech Analysis.
I am not a language programmer. I do not know something about webdev. I feel C++ is horrible, but it surely’s shipped every part I’ve ever labored on. JavaScript is an abomination. I’ve ideas on Python and global variables. I do not know something about backends.
This submit is simply, like, my opinion, man.
Jai may be very simple to run. The beta is distributed as a vanilla zip file. It comprises:
jai.exe
compilerlld.exe
linker- default modules aka “commonplace library”
- 65 “” tutorials
- a handful of examples
That is mainly it. Compiling and operating a Jai program is as simple as:
- Run
jai.exe foo.jai
- Run
foo.exe
On Home windows the compiler may also produce `foo.pdb` which permits for full breakpoint and step debugging in Visible Studio or RemedyBG. I am a card carrying member of #TeamDebugger and hate when new languages solely provide printf
for debugging.
I’m happy to say that this 12 months I efficiently solved all 25 days of Creation of Code completely with Jai. This ended up being 4821 traces of code. You possibly can view full supply on GitHub.
My options are somewhat sloppy, inefficient, and non-idiomatic. One cool factor about studying a language by way of Creation of Code is studying from different individuals’s options. Sadly with Jai being a closed beta I wasn’t in a position to do this this 12 months!
This is my answer to day 01. It is an exceedingly easy program. Nevertheless I am assuming most readers have by no means seen Jai code earlier than.
// [..]u32 is much like std::vector<u32> // [..][..] is much like std::vector<std::vector<u32>> day01_part1 :: (elves: [..][..]u32) -> s64 { // := is declare and initialize // var_name : Kind = Worth; // Kind could be deduced, however is statically recognized at compile-time max_weight := 0; // iterate elves for elf : elves { // sum weight of things carried by elf elf_weight := 0; for weight : elf { elf_weight += weight; } // discover elf carrying probably the most weight max_weight = max(elf_weight, max_weight); } return max_weight; }
This code needs to be simple to know. The syntax could also be completely different than you are used to. It is good, simply roll with it for now.
This is most of my answer to day 4.
day04_solve :: () { // Learn a file to a string enter, success := read_entire_file("information/day04.txt"); assert(success); // Remedy puzzle and print outcomes part1, part2 := day04_bothparts(enter); print("day04 part1: %n", part1); print("day04 part2: %n", part2); } // Word a number of return values day04_bothparts :: (enter : string) -> u64, u64 { part1 : u64 = 0; part2 : u64 = 0; whereas enter.rely > 0 { // next_line is a helper perform I wrote // this does NOT allocate. it "slices" enter. line : string = next_line(*enter); // cut up is a part of the "commonplace library" // splits "###-###,###-###" in two "###-###" components elves := cut up(line,","); assert(elves.rely == 2); // declare a helper perform inside my perform // converts string "###-###" to 2 ints get_nums :: (s : string) -> int, int { vary := cut up(s, "-"); assert(vary.rely == 2); return string_to_int(vary[0]), string_to_int(vary[1]); } a,b := get_nums(elves[0]); x,y := get_nums(elves[1]); // extra helpers comprises :: (a:int, b:int, x:int, y: int) -> bool overlaps :: (a:int, b:int, x:int, y: int) -> bool { return a <= y && x <= b; } // Jai loves terse syntax // This model is inspired if comprises(a,b,x,y) part1 += 1; if overlaps(a,b,x,y) part2 += 1; } return part1, part2; }
This code also needs to be simple to know. In comparison with C there are just a few new options resembling nested capabilities and a number of return values.
Creation of Code is fairly easy. The options are properly outlined. I did not write a 5000 line Jai program. I wrote 25 Jai tiny packages which might be at most just a few hundred traces.
I used to be going to share extra AoC snippets however they’re “extra of the identical”. It is all on GitHub if you would like to look.
Would I like to recommend AoC to study Jai? Completely! It is an awesome and enjoyable option to study the fundamentals of any language, imho.
Would I like to recommend Jai for aggressive programming? Positively not. Jai is not designed for leet code. This 12 months’s AoC winner invented their very own language noulith which is pure code golf sugar. I kinda adore it. However that ain’t Jai.
If you wish to remedy puzzles quick use Python. If you need puzzle options that run quick use Rust. If you wish to study a brand new language then use something.
I need to talk about cool language options. I feel it is necessary to go over reminiscence administration first.
Jai doesn’t have rubbish assortment. Reminiscence is managed manually by the programmer.
Jai has no destructors. There isn’t a RAII. Customers should manually name free on reminiscence or launch on handles. Jai does have defer
which makes this somewhat simpler.
// Create a brand new Foo that we implicitly personal and should free foo : *Foo = generate_new_foo(); // free the Foo when this scope ends defer free(foo);
No destructors may be very very similar to C and by no means like C++. If you happen to’re a C++ programmer you might be recoiling in horror. I urge you to maintain an open thoughts.
Jai documentation spends over 7000 phrases describing its philosophy on reminiscence administration. That is longer than my total weblog submit! I can not presumably summarize it right here.
One subsection resonated with me. Jai theorizes that there are roughly 4 classes of lifetimes.
- Extraordinarily brief lived. Might be thrown away by finish of perform.
- Quick lived + properly outlined lifetime. Reminiscence allotted “per body”.
- Lengthy lived + properly outlined proprietor. Uniquely owned by a subsystem.
- Lengthy lived + unclear proprietor. Closely shared, unknown when it might be accessed or freed.
Jai thinks that almost all program allocations are class 1. That class 4 allocations needs to be uncommon in properly written packages. And that C++ allocators and destructors are centered on supporting cat 4.
Classes 2 and three are greatest served by area allocators. Contemplate a node primarily based tree or graph. One implementation is likely to be to manually `malloc` every node. Then `free` every node individually throughout shut down. A less complicated, quicker various could be to make use of an area and `free` the entire thing in a single easy name.
These classes are NOT onerous baked into the Jai language. Nevertheless the concepts are closely mirrored.
Jai gives commonplace entry to each a “common” heap allocator and an excellent quick momentary allocator for class 1 allocations.
The temp allocator is a straightforward linear allocator / bump allocator. An allocation is a straightforward increment right into a block of reminiscence. Objects can’t be freed individually. As a substitute your complete block is reset by merely resetting the offset to zero.
silly_temp_print :: (foo: Foo) -> string { // tprint allocates a brand new buffer // makes use of the temp allocator return tprint("hi there world from %", foo) } // no destructor means we now have to take care of reminiscence msg : string = silly_temp_print(my_foo); // awhile later (perhaps as soon as per body) reset_temporary_storage();
On this instance we printed a string
utilizing the temp
allocator by way of tprint
. The perform silly_temp_print
would not personal or preserve the string
. The caller would not have to manually name free
as a result of it is aware of that reset_temporary_storage()
shall be referred to as in some unspecified time in the future.
The idea of a brief allocator is baked into Jai. Which suggests each library authors and customers realize it exists and may depend on it.
Most allocations in most packages are very brief lived. The purpose of the momentary alloactor is to make these allocations tremendous low-cost in each efficiency and psychological effort.
At this level we have seen some easy Jai code and realized somewhat bit about its reminiscence administration guidelines. Now I need to share Jai options and concepts that I feel are attention-grabbing.
That is explicitly NOT in tutorial order. Can Jai do all the fundamental issues any language can do? Sure. Am I going to inform you how? No.
As a substitute I will share extra superior concepts. These are the forms of issues you do not study on day 1 however could present probably the most worth on day 1000.
In Jai each process has an implicit context
pointer. That is much like how C++ member capabilities have an implicit this
pointer.
The context comprises just a few issues:
- default reminiscence allocator
- momentary allocator
- logging capabilities and elegance
- assertion handler
- cross-platform stack hint
- thread index
This enables helpful issues like altering the reminiscence allocator or logger when calling right into a library. contexts
may also be pushed.
do_stuff :: () { // copy the implicit context new_context := context; // change the default allocator to area 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 handed to subroutine // subroutine now makes use of our allocator and logger call_subroutine(); } }
The context
struct could be prolonged with extra consumer information. Nevertheless this does not play good with .dlls
so there are nonetheless design issues to unravel.
I hate globals with a fiery ardour. I want all languages had a context
struct. It is fairly elegant.
One of the vital highly effective concepts I’ve seen in Jai is the rampant use of compiler directives.
This is an instance of #full
which forces an enum change to be exhausive at compile-time.
// Declare enum Stuff :: enum { Foo; Bar; Baz; }; // Create variable stuff := Stuff.Baz; // Compile Error: This 'if' was marked #full... // ... however the next enum worth was lacking: Baz if #full stuff == { case .Foo; print("discovered Foo"); case .Bar; print("discovered Bar"); }
This can be a easy however genuinely helpful instance. Languages spend a lot of time bikeshedding syntax, key phrases, and many others. In the meantime Jai has dozens of compiler directives and so they’re seemingly added willy nilly.
Listed here are a number of the directives at present accessible.
#add_context inject information into context #as struct can forged to member #asm inline meeting #bake_arguments bake argument into perform #bytes inline binary information #caller_location location of calling code #c_call c calling conference #code assertion is code #full exhaustive enum test #compiler interfaces with compiler internals #compile_time compile-time true/false #cpp_method C++ calling conference #deprecated induces compiler warning #dump dumps bytecode #increase perform is a macro #filepath present filepath as a string #international international process #library file for international capabilities #system_library system file for international capabilities #import import module #insert inject code #intrinsic perform dealt with by compiler #load consists of goal file #module_parameters declare module "argument" #should return worth have to be used #no_abc disable array bounds checking #no_context perform doesn't take context #no_padding specify no struct padding #no_reset international information persists from compile-time #place specify struct member location #placeholder image shall be generated at compile-time #procedure_name purchase comp-time process identify #run execute at compile-time #scope_export perform accessible to complete program #scope_file perform solely accessible to file #specified enum values have to be specific #string multi-line string #symmetric 2-arg perform could be referred to as both approach #this return proc, struct, or information sort #via if-case fall via #sort subsequent assertion is a sort
They do lots of issues. Some easy issues. Some massive issues we’ll study extra about.
What I feel is rad is how recreation altering they’re given how simple they’re so as to add. They do not require bikeshed committees. They are often sprinkled in with out declaring a brand new key phrase that breaks present packages. They seem to make it radically simpler to elegantly add new capabilities.
Jai has sturdy help for run-time reflection. Here’s a quite simple instance.
Foo :: struct { a: s64; b: float; c: string; } Runtime_Walk :: (ti: Type_Info_Struct) { print("Kind: %n", ti.identify); for *ti.members { member : *Type_Info_Struct_Member = it; member_ti : *Type_Info = member.sort; print(" %", member.identify); print(" % bytes", member_ti.runtime_size); mem_type := "unknown"; if member_ti.sort == { case .INTEGER; mem_type = "int"; case .FLOAT; mem_type = "float"; case .STRING; mem_type = "string"; case .STRUCT; mem_type = (forged(*Type_Info_Struct)member_ti).identify; } print(" %n", mem_type); } } Runtime_Walk(type_info(Foo));
At run-time this can print:
Kind: Foo a 8 bytes int b 4 bytes float c 16 bytes string
That is an exceedingly highly effective device. It is constructed proper into the language with full help for all sorts – primitives, enums, structs, procedures, and many others.
Jai has extraordinarily highly effective compile time capabilities. Like, frighteningly highly effective.
First, here’s a small syntax tutorial.
// that is compile-time fixed as a result of :: // you might have observed that structs, enums, // and procedures have all used :: foo :: 5; // that is variable as a result of := bar := 5;
This is an excellent fundamental instance of compile-time capabilities.
factorial :: (x: int) -> int { if x <= 1 return 1; return x * factorial(x-1); } // :: means compile-time fixed // word the usage of #run x :: #run factorial(5); print("%n", x); // compile error as a result of factorial(5) shouldn't be fixed // y :: factorial(5); // executes at runtime z := factorial(5);
Any code that has #run
shall be carried out at compile-time. It will probably name any code in your program. Together with code that allocates, reads information from disk, and many others. 🤯
The #insert
directive enables you to insert code. This is a toy instance.
// runtime variable x := 3; // insert string as code #insert "x *= 3;"; // runtime worth of x is 9 print("%n", x);
This inserts the string "x *=3;"
as code. That is foolish as a result of we might have simply written that code like a traditional particular person. Nevertheless we are able to mix #insert
with #run
and do issues which might be beginning to grow to be attention-grabbing.
// 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 supply 6 // insert code x *= 6 #insert #run gen_code(factorial(3)); print("%n", x); // print 54
We are able to insert arbitrary strings
as code. At compile-time we are able to execute arbitrary Jai code that generates and inserts strings
. 🤯🤯
Up to now we have been working on strings
. Jai also can function with sort security.
// runtime variable x := 3; // #increase makes this a "macro" so it may // entry variables in its surrounding scope do_stuff :: (c: Code) #increase { // splat the code 4 occasions #insert c; #insert c; #insert c; #insert c; } // generate a snippet of code c : Code : #code { x *= 3; }; // at compile-time: increase do_stuff macro // at run-time: execute code 4 occasions do_stuff(c); // prints 243 print("%n", x);
Right here we wrote x *= 3;
and saved it in a variable of sort Code
. Then we wrote the macro do_stuff
which copy pastes our snippet 4 occasions. And we did this with “correct” information sorts relatively than strings
. 🤯🤯🤯
At compile-time the Code
sort could be transformed to Summary Syntax Tree nodes, manipulated, and transformed again.
// our outdated buddy factorial :: (x: int) -> int { if x <= 1 return 1; return x * factorial(x-1); } // perform we will #run at compile-time comptime_modify :: (code: Code) -> Code { // covert Code to AST nodes root, expressions := compiler_get_nodes(code); // stroll AST // multiply quantity literals by their factorial // 3 -> 3*factorial(3) -> 3*6 -> 18 for expr : expressions { if expr.form == .LITERAL { literal := forged(*Code_Literal) expr; if literal.value_type == .NUMBER { // Compute factorial fac := factorial(literal._s64); // Modify node in place literal._s64 *= fac; } } } // convert modified nodes again to Code modified : Code = compiler_get_code(root); return modified; } // modify and duplicate code do_stuff :: (code: Code) #increase { // modify the code at compile-time new_code :: #run comptime_modify(code); #insert new_code; #insert new_code; #insert new_code; #insert new_code; } // similar as earlier than x := 3; c :: #code { x *= 3; }; do_stuff(c); // prints 3*18*18*18*18 = 314,928 print("%n", x);
On this instance we:
- Declare the code
x *= 3;
- Compile-time modify the compiler parsed AST to
x *= 18;
- Insert
x *= 18;
4 occasions
We did all of this with code that appears and runs like common vanilla Jai code. It is not a brand new macro language utilizing #outline
string manipulation or complicated macro_rules!
syntax. 🤯🤯🤯🤯
The probabilities for this are countless. Frighteningly countless even. Extreme compile-time code is extra complicated and more durable to know.
I need to make a small detour share a small second the place Jai actually sparked my pleasure.
For Creation of Code I wrote a easy assert_eq
macro I used to be pleased with. It prints each the values that didn’t match and likewise the expression that produced the worth.
assert_eq :: (a: Code, b: Code) #increase { sa := #run code_to_string(a); va := #insert a; sb := #run code_to_string(b); vb := #insert b; assert(va == vb, "(left == proper)n left: % expr: %n proper: % expr: %n loc: %n", va, sa, vb, sb, #location(a)); } assert_eq(42, factorial(3)); // stderr: // C:/aoc2022/predominant.jai:154,5: Assertion failed: (left == proper) // left: 42 expr: 42 // proper: 6 expr: factorial(3) // loc: {"C:/aoc2022/predominant.jai", 85, 15}
It prints the worth and likewise the code that produced the worth. That is neat. It made me pleased. It was a enjoyable second.
Rust has handy built-ins like #[derive(Hash)]
. The neighborhood has constructed extremely highly effective libraries like serde
. Jai would not an ecosystem of comparable libraries but. I imagine the highly effective compile-time capabilities ought to make them doable. I am very curious to see what people give you.
Now we’re on the part of what I will name “medium influence concepts”. These concepts are tremendous vital, however maybe rather less distinctive.
Jai doesn’t have object oriented polymorphism ala digital
capabilities. It does have “polymorphic procedures” that are Jai’s model of templates
, generics
, and many others. Naming issues is tough. This identify could change.
This is a fundamental instance:
// Jai sq. :: (x: $T) -> T { return x * x; } // C++ equal template<typename T> T sq.(T x) { return x * x; }
The $T
means the kind shall be deduced at compile-time. Like most compiled languages this perform is compiled for all essential sorts and there’s no dynamic dispatch or run-time overhead.
Polymorphic process syntax has some delicate niceties to enhance compiler errors.
// compile error. $T can solely be outlined as soon as. foo :: (a: $T, b: $T); // deduce from array sort array_add1 :: (arr: [..]$T, worth: T); // deduce from worth sort (gross) array_add2 :: (arr: [..]T, worth: $T); // dynamic array of ints nums: [..] int; // Error: Kind mismatch. Kind needed: int; sort given: string. array_add1(*nums, "not an int"); // Error: Kind mismatch. Kind needed: *[..] string; sort given: *[..] int. array_add2(*nums, "not an int");
Explicitly specifying which argument defines the kind T
is intuitive. It additionally allows a lot better error messages for compiler errors.
structs
may also be declared polymorphically.
Vector :: struct($T: Kind, $N: s64) { values : [N]T; } // a easy vector of three floats Vec3 :: Vector(float, 3); v1 : Vec3 = .{.[1,2,3]}; // a giant vector of 1024 ints (for some purpose) BigVec :: Vector(int, 1024); v2 : BigVec = .{.[/*omitted*/]};
You should use this for easy sorts, vectors
, hash_maps
, and many others. It is an vital instance of how Jai is C+ and never C.
Construct methods are a ache within the ass. Some languages require a configuration language simply to outline their construct. Make, MSBuild, Ninja, and many others. This construct language could or will not be cross-platform.
Jai gives a built-in construct system that makes use of vanilla Jai code. Here’s a very simplified look.
construct :: () { // workspace is a "construct mode" like debug, launch, delivery, and many others w := compiler_create_workspace(); // get CLI args args := choices.compile_time_command_line; // set some flags and stuff choice := get_build_options(); for arg : args { if arg == "-release" { choices.optimization_level = .RELEASE: } } set_build_options(choices, w); // specifiy goal information // compiler auto-starts in background add_build_string(TARGET_PROGRAM_TEXT, w); add_build_file("extra_file.jai", w); } // invoke at compile-time by way of #run #run construct();
Jai ships default_metaprogram.jai
and minimal_metaprogram.jai
so customers would not have to manually write this each time. Bigger packages will inevitably have their very own construct methods to carry out extra complicated operations.
Jai’s method to a construct methods is attention-grabbing. Having an actual programming language is nice. It is higher than cmake
hell.
My skilled life is a polyglot of languages (C, C++, C#, Rust, Python, Matlab, Javascript) and working methods (Home windows, macOS, Linux, Android, embedded) and units (PC, Quest, Hololens, embedded) and environments (Unity, Unreal, Matlab, Jupytr, net). Kill me. ☠
C++ is under-defined and would not outline any construct system. Trendy languages have rectified this and it is a lot better.
Sadly I do not suppose anybody has solved “the construct system drawback” but. I am unsure it may be “solved”. 🙁
This submit is getting too lengthy. I will punt “Small Concepts” to the tip of the submit as a “bonus part”
So, what do I take into consideration Jai? Quite a few people have requested me this. It is extremely onerous to reply.
Q: Do I get pleasure from writing Jai?
A: Principally. The language is nice. Studying it’s not troublesome. The neighborhood may be very useful. The minimalistic commonplace library forces you to jot down a number of little helpers which is an annoying short-term hurdle.
Q:Would I exploit Jai in manufacturing?
A: No, in fact not! It is an unfinished language. The compiler shouldn’t be for distribution. It is not prepared for manufacturing.
Q: Would I relatively use Jai than C?
A: Sure, I feel so. Jai being “C plus” is successfully an excellent set. I desire C+ to C.
Q: Would I relatively use Jai than C++?
A: That is a more durable query. Generally perhaps however not but? There are quite a few Jai options that I desperately need in C++. A sane construct system, usable modules, reflection, superior compile-time code, type-safe printf
, and extra.
I am intrigued by Jai’s model of reminiscence administration. I am not totally offered simply but. Jai is chipping away on the ergonomics however hasn’t cracked it but.
Ask me once more in 2 years.
Q: Would I relatively use Jai than Rust?
A: I feel Rust is admittedly good at lots of issues. For these issues I’d in all probability not desire Jai.
I additionally suppose Rust nonetheless sucks for some issues, like UI and video games. There’s cool initiatives attempting to repair that. They are not there but. Since Rust shouldn’t be a great match I would like Jai for such initiatives.
Q: Would I relatively use Jai than Carbon, Cppfront, and many others?
A: Sure. A lot of persons are attempting to switch C++. I perceive the enchantment of “substitute C++ however preserve compatibility with C++ code”. It’s totally sensible. That path would not enchantment to me. I do not need a shiny new factor to be tightly coupled to and held down by the outdated poopy factor. I need the brand new factor to be superior!
Q: Would I relatively use Jai than JavaScript?
A: I would relatively serve espresso than write JavaScript.
Q: How is the Jai neighborhood?
A: Small however very pleasant and useful. Each Discord query I ever requested obtained answered. They actually hate Rust to a kinda bizarre diploma. There shall be rising pains.
Q: When will Jai be publicly accessible?
A: I do not know. I really feel just like the language went darkish for awhile and it is beginning to get up. I feel it’s going to be awhile.
Q: Do I feel Jai’s closed growth is sensible?
A: No concept. I help anybody prepared to do one thing a distinct approach.
Q: Will Jai take off?
A: Who is aware of! Most languages do not. Betting towards Jai is a secure guess. It is also a boring guess. Jai is bold. If each bold venture succeeded then they weren’t truly bold.
Jai shall be well-liked with the Handmade neighborhood. I count on people will produce cool issues. Jai will get off the bottom. It might or could not hit escape velocity and attain the celebrities. I am rooting for it!
Q: Will Jai take over the video games trade?
A: Nah. Unreal Engine, Unity, and customized recreation engines are too entrenched. Name of Responsibility and Fortnite aren’t going to be re-written in Jai. I do not count on “Rewrite it in Jai” to be a giant factor.
Constructing a recreation engine is tough. Not many studios are able to it. Possibly Jai will enable some mid sized indies to extra successfully construct customized engines to make video games that Unity/Unreal are a foul match for? That’d be a really profitable final result I feel.
Q: Will Jai develop a wholesome area of interest?
A: Robust perhaps! It could possibly be cool for instruments. If sufficient helpful instruments get constructed then it could possibly be cool for extra issues.
Designers and artists do not care what language their instruments had been made in. Being 15% higher is not sufficient to beat inertia. It must be a minimum of 50% higher. Possibly even 100% higher. That is powerful.
Q: Are you able to re-write components of your recreation in Jai?
A: Ehhh I dunno. C ABI is the lingua franca of pc science. Jai can name C code and vice versa. You could possibly write a Jai plugin for Unity or a customized engine. I am unsure that gives sufficient worth.
Q: Does Jai have papercuts, bugs, and lacking options?
A: Completely. What are they? I do not suppose it is applicable to publicly blast an unfinished language that invited me to a closed beta. I do not need to fake it is all roses and sunshine. However I do not need to publicly air my grievences both. It is a powerful stability.
Q:Will Jai be helpful for non-games?
A: Good query. In all probability sure? It would not exclude different use circumstances. However Jai is unquestionably optimizing for video games and recreation issues.
Q: Will Jai make everybody pleased?
A: No, in fact not. Will it make some individuals pleased? Yeah undoubtedly.
Q: What ought to readers take into consideration Jai?
A: That is as much as you. I do not need to inform you what to suppose. I attempted to color an image of Jai primarily based on my experiences. Your interpretation of that’s as much as you.
Let’s wrap this up.
Coming into all this I knew little to nothing about Jai. I obtained into the beta. Participated in Creation of Code. Learn all of the tutorials. And watched some movies.
I really feel like I’ve first rate grasp of Jai’s fundamentals. I can see a number of the issues it is attempting to do. It will take engaged on a giant venture to see if its concepts payoff.
The Jai beta remains to be very small. If you happen to’ve obtained a venture you would like to jot down or port to Jai you may in all probability get your self in. If you happen to simply need to poke round for half-hour you would be higher off ready.
I like the answer area that Jai is exploring. I am onboard with its philosophy. It has some genuinely good concepts and options. It is a very bold venture. I hope it achieves its lofty targets.
Thanks for studying.
(maintain scrolling for extra bonus content material)
Welcome to the bonus part! There’s so many issues I need to discuss. For the sake of semi-brevity and narrative I can not say all of them. This is some unsorted and fewer polished ideas from the chopping room.
Generally I take into consideration languages in 3 components:
- Language. Syntax, key phrases, “built-in” options, and many others.
- Normal library. “Consumer mode” code that you would have written your self however ships with the compiler for comfort.
- Ecosystem. Libraries written by the neighborhood.
This submit centered nearly fully on language. Jai remains to be in a small beta. I will fear about the usual library and ecosystem later.
Jai ships with a really small commonplace library. It’s NOT a “batteries included” language. It desires to keep away from compiling tens of 1000’s of traces of code since you imported a single file.
Ecosystem could make or break language adoption. Python kinda sucks as a language, however the ecosystem is unequalled. A key promoting level of Rust is the colourful and sturdy crate ecosystem. I do not know what method Jai will in the end take.
One in every of Jai’s promoting factors is quick compile occasions. They’re comparatively quick?
This is a comparability between my 2022 Jai code and my 2021 Rust code. It is not fairly apples to apples. However it’s fairly related.
Creation of Code 2022 (Jai) Jai debug full 0.56 seconds Jai launch full 1.36 seconds Jai debug incremental 0.56 seconds Jai launch incremental 1.36 seconds Creation of Code 2021 (Rust) Rust debug full 13 seconds Rust launch full 14 seconds (incremental) Rust debug incremental 0.74 seconds Rust launch incremental 0.74 seconds
Jai would not do incremental builds. It at all times compiles every part from scratch. The long run purpose is one million traces per second. The compiler is at present not totally multi-threaded.
My impression is that Jai would not have any magic secret sauce to quick compile occasions. How do you make compiling quick? Compile fewer traces of code! Is sensible. However perhaps somewhat disappointing. I needed magic!
I didn’t do in depth efficiency testing with Jai. It appears quick?
I ported one answer that was operating fairly sluggish to C++ to check. My C++ model ran slower. This was as a result of C++ std::unordered_map
was slower than Jai Hash_Table
.
I’m NOT claiming that Jai is quicker than your favourite language. Nevertheless I’m not involved about Jai’s efficiency long-term. It needs to be simply as quick as different compiled languages. It is idioms could or will not be quicker than one other language’s idioms.
As soon as upon a time Jai talked about automagic struct-of-arrays compiler options. These options have been minimize. I imagine the use circumstances had been so particular that no generalized answer offered itself.
I will be trustworthy, the one meeting I’ve ever written was in class. It is not my jam.
Jai might need some cool meeting options? I dunno!
outcome : s64 = 0; #asm { // compiler picks a normal function register for you // that appears cool? foo : gpr; bar : gpr; // assign some 64-bit qwords mov.q foo, 42; mov.q bar, 13; // foo += bar add foo, bar; // outcome = foo mov outcome, foo; } print("%n", outcome); // prints: 15
Jai will allocate registers for you. If it runs out of registers it is a compiler error. Appears good?
Jai is NOT a reminiscence secure language. Debug mode checks array bounds which is sweet. However that is about it?
There isn’t any compile-time borrow checker. There isn’t any checks for reminiscence leaks or use after free. It is the C/C++ wild west for higher or worse.
There’s additionally no particular security for multi-threading both. You are by yourself similar to in C/C++.
There’s additionally no mechanisms for async code.
Jai imports modules. I feel it follows typical module guidelines? It has some assorted macros.
#import "Foo" // import module #load "bar.jai" // similar-ish to #embody #scope_file // code is NOT uncovered by way of import/load #scope_export // code is uncovered by way of import/load
Jai doesn’t have namespaces. It solves the C identify collision subject by letting module imports be assigned to a reputation.
// foo.jai do_stuff :: () { /* omitted */ } // bar.jai do_stuff :: () { /* omitted */ } // predominant.jai #import"Foo" Bar :: #import "Bar"; do_stuff(); // calls foo.jai do_stuff Bar.do_stuff(); // calls bar.jai do_stuff
Drawback solved?
As beforehand mentioned, listed below are smaller language options I feel are neat, useful, or unusual.
By default all values in structs are initialized. You possibly can have uninitialized reminiscence but it surely’s strictly opt-in by way of ---
.
foo : float; // zero Vec3 :: struct { x, y, z: float }; bar : Vec3; // 0,0,0 baz : Vec3 = ---; // uninitialized
There are mechanics for specifying non-zero preliminary values. Nevertheless there are not any constructors.
Printing in Jai is secure and easy. It would not use error inclined printf specifier.
Foo :: struct { a : int; b : bool; c : string; d : Bar; } Bar :: struct { x : float; y : string; z : [..]int; } foo := Foo.{ 42, true, "hi there", Bar.{ 13.37, "world", .[1,2,3]} }; print("foo: %n", foo); // prints: {42, true, "hi there", {13.37, "world", [1, 2, 3]}}
There are mechanics for fancy formatting. The vital factor is that the straightforward case “simply works”.
Jai has a tremendous characteristic I need so badly in different languages. It is what I’d name a “sort secure typedef”.
// cpp: utilizing HandleA = u32; // rust: sort HandleA = u32; HandleA :: u32; // cpp: no equal // rust: no equal HandleB :: #sort,distinct u32; // Features 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 doesn't // Task u = a; a = u; // a = b; // compile error // b = a; // compile error // u = b; // compile error // b = u; // compile error // process takes u32 do_stuff_u(u); do_stuff_u(a); //do_stuff_u(b); // compile error // process takes HandleA do_stuff_a(u); do_stuff_a(a); // do_stuff_a(b); // compile error // process takes HandleB // do_stuff_b(u); // compile error // do_stuff_b(a); // compile error do_stuff_b(b);
That is exceedingly useful for handles. I’ve misplaced monitor of what number of wrapper structs I’ve written.
One thing you might have observed is every part is asserted the identical approach.
// 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 :: "hi there world"; my_func_type :: #sort (int) -> int; // variables use := // the kind could be specific 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 : Kind = string; another_type := type_of(an_int);
This commonplace syntax is surprisingly good. It is easy and constant.
Some languages are exceedingly troublesome and ambigious to parse. Jai strives to be simple to parse and to offer entry to the AST to customers. This may ultimately allow very sturdy and dependable IDE help.
Everybody has their very own opinion on syntax. I work in sufficient languages I do not actually care. Any C-ish syntax is simple to get used to. I will gladly adapt to any rational syntax if it allows higher tooling.
Jai has first-class relative pointers. They retailer an offset relative to the pointer’s storage handle.
Node :: struct { // relative pointer saved in s16 subsequent: *~s16 Node; worth: float; } // Declare two nodes a := Node.{null, 1337}; b := Node.{null, 42}; a.subsequent = *b; // can instantly dereference relative pointer worth := a.subsequent.worth; print("rel: % worth: %n", a.subsequent, worth); // can convert to absolute pointer abs_ptr := forged(*Node)rel_ptr; print("abs: % worth: %n", abs_ptr, abs_ptr.worth); // instance output: // rel: r60 (5c_5d6f_ecc8) worth: 42 // abs: 5c_5d6f_ecc8 worth: 42
Sure you would retailer an integer offset. What makes it good is the syntax for utilizing a relative pointer is identical as an everyday pointer. The language handles making use of the offset for you.
Apparent use circumstances for this are packing information and deserializing binary blobs with out having to run a fixup go.
Jai would not have references. Simply worth and pointers. It belives that references present minimal worth however considerably enhance the complexity of the kind system.
Jai would not have the idea of a const pointer or variable. Sure there are compile time constants. However there is not any option to declare a run-time variable and say you will not change it.
Arguments are handed to process “perhaps by reference”. That is kinda wild.
Sorts <= 8 bytes
are handed by worth. Sorts >8 bytes
are handed perhaps by reference. The compiler may go it by a pointer. Or perhaps not! Who is aware of.
Nevertheless the code have to be written as if it had been handed by, in C++ parlance, const &
.
do_stuff :: (s: string) { // compiler error // Error: Cannot assign to an immutable argument. s.rely = 0; // you can also make a replica and alter that s2 := s; s2.rely = 0; }
In Jai the compiler is free to resolve whether or not to go a pointer or by worth. This enables Jai to truly deal with args as const & prohibit
and assume that no pointer arguments alias. If you wish to modify the arg then merely go an precise pointer.
Treating args as const & prohibit
could possibly be problematic. For instance the information is likely to be aliased by an evil international or a pointer. The Jai philosophy is do not try this and perhaps attempt to detect it in debug mode by making a replica and evaluating bytes on the finish.
Jai doesn't have digital capabilities. Nevertheless it does have some type what I shall name data-only pseudo-inheritance.
// Declare a base class Entity :: struct { id: int = 42; identify: string = "bob"; } // Declare a "derived" class" BadGuy :: struct { // base: Entity is a knowledge member #as utilizing base: Entity; well being: int = 9000; } // Helper to print an entity's identify print_name :: (e: *Entity) { print("Entity: %n", e.identify); } // Declare a foul hombre baddie : BadGuy; // word: not baddie.base.id due to `utilizing` print("Id: %n", baddie.id); // *BadGuy casts to *Entity due to #as baddie_ptr : *BadGuy = *baddie; print_name(baddie_ptr); // Can even forged the opposite approach // Do not get this fallacious! entity_ptr := baddie_ptr; baddie_ptr2 := forged(*BadGuy)entity_ptr;
The #as
casting is perhaps helpful. The utilizing
sugar is usually good. It may be used to do some actual wacky issues which might be in all probability a foul concept.
Think about you have got a customized information container and also you need to write for worth : myContainer
. Jai has a slick mechanism to make this tremendous simple.
// Tremendous dumb Vector with fastened measurement FixedVector :: struct(ValueType: Kind, Dimension: int) { values : [Size]ValueType; rely : int; } // Push a price! push :: (vec: *FixedVector, worth: $T) { if vec.rely < vec.Dimension { vec.values[vec.count] = worth; vec.rely += 1; } } // iterate all values for_expansion :: (vec: FixedVector, physique: Code, flags: For_Flags) #increase { // Loop over inside array by rely for i : 0..vec.rely - 1 { // Should declare `it and `it_index `it := vec.values[i]; `it_index := i; // insert consumer code #insert physique; } } // 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 worth : myVec print("% ", worth); // prints: 5 1 5 2 5 3
It took roughly 5 traces of code so as to add write a customized for_expansion
for our customized container. We did not should declare a helper struct with one million little helper capabilities and operators.
Let’s write a second model that skips values equal to 5
.
// declare a customized iterator skip_fives :: (vec: FixedVector, physique: Code, flags: For_Flags) #increase { // carry out regular iteration for worth, index : vec { // we do not like 5, booo! if worth == 5 proceed; // declare required `it and `it_index `it := worth; `it_index := index; // insert consumer code #insert physique; } } // iterate utilizing skip_fives for :skip_fives v: myVec { print("% ", v); } // prints: 1 2 3
Right here we have made a second iteration perform referred to as skip_fives
. It iterates throughout the array however ignores any worth equal to 5
. We referred to as it by writing for :skip_fives
.
Congrats for making it to the tip. As a reward here’s a image of my canine, Tank and Karma. They’re cute.