logo

JAI Programming Language

Posted on: 2022-12-04

Conclusion

I've come away from reading Jai documentation, with mixed feelings. It's really more of a C replacement. There is a lot of similarity with Odin, but I think Odin is more thought through and logical as a language. This feels like a very practical language designed to solve the problems for it's developers, as those problems occured. It definately isn't a "big idea" language, and is described as such in Philosophy of Jai. Unfortunately that makes it feel more like a jumble of ideas.

Theres quite a lot to like here. I don't feel it's likely this will become a language in wide spread use though. Both Zig and Odin are out there, are readily available, in a similar space and seem more solid and consistent.

Good

Interesting

Not sure

Bad

Ugly

Exclusion Summary

Discussion

Just like C, Jai has pointers with the same exact functionality as C, just using different operators for getting the address of and de-referencing a pointer. You can do pointer arithmetic on pointers as well as compare pointers against each other. Unlike C however, arrays do not cast to pointers, but rather arrays in Jai are objects with a data pointer and a count that counts the number of elements in an array. source

Rust allows raw pointers, but only in unsafe blocks or only when using smart pointers or reference counted pointers. Jai follows the C pointer model which allows pointers all the time. source

Although Jai can do function pointers, virtual functions are not supported in the compiler at all. source

Has struct inclusion with using. Has function pointers.

Lot's of hot takes

Syntax around constant and variables is very similar to Odin

x : int = 42;       // Variable 
x : int : 42;       // Constant

unsafe, JAI doesn't have anything similar. The argument seems a little weak.

Types are what you'd expect. Uses s prefix for int. int is s64.

Zero initializes by default. Uses --- to set as uninitialized.

Typical number literal styles, supporting 0x and 0b and allowing _ to split.

No character type. Strings are list of bytes.

autocast operator is xx(?). This is similar to the idea around coerce, but the name xx doesn't seem right.

Pointers uses * for reference and << to dereference.

Uses the "prefix type style". So it's *int.

Has built in support for relative pointers

Initializing an array...

array : [4]float = float.[10.0, 20.0, 1.4, 10.0];

Arrays. Oh I have to use C style syntax to manipulate the array(!), for example...

array_add(*myarray, 5); // Add 5 to the end of myarray

Uses # for built in and attribute like behavior, such as with #complete.

Uses it to name the value, and it_index to name the index.

Has an unusual syntax for labeling break/continue.

x := 0;
while condition := x < 10 {
  y := 0;
  while y < 3 {
    print("x=%, y=%\n", x, y);
    y += 1;
    if x > 3
      break condition; // break out of an outer while loop
  }

  x += 1;
}

Is that better than the more common C++ label style? Hmm probably not. It looks too much like it's setting up a variable. The ideal of having the "label" part of the statement (as opposed to prefixing it) perhaps has something. Actually the typical label syntax might have problems here because name : already has a meaning?

Has a remove statement. That is helpful in so far as that is a fairly common idiom. Having a keyword for that seems a little surprising. Not clear if it only works on arrays, or has some other mechanism to work with other types.

Allows more custom iteration by using for_expansion. This all seems kind of wacky stuff.

Function declaration...

function :: (arg1: int, arg2: int, arg3: int) -> int {
  // write function code here.
}

Has named returned variables, and named parameters it seems.

function :: (arg1: int, arg2: int, arg3: int) -> ret1: int, ret2: int {
  // write function code here.
}

ret1, ret2 := function(arg1=1, arg2=2, arg3=3);

Oh actually no, the names are just "reminders".

Has #this as the name of the current scope.

Has overloading. No closures.

Lambda is defined with =>, so...

funct :: (a, b) => a + b;

Has SOA through macro expansion. Not sure this really works in general, and seems like pretty weak support.

Like that lower snake is used for built in keywords, like size_of, type_of etc.

The #as concept is interesting, it allows a type to be used as a member type.

Interesting idea of using void as a struct marker. In JAI void is always 0 sized. It can be returned, and have 'variables'.

Has operator overloading. Can use #symmetric if the parameter order can be inverted.

In section on polymorphism describes a variety of ways to limit a $T. The syntax seems a little odd x : Foo($) or x : $T/Foo. The first says T is of the kind Foo, the second allows T to incorporate Foo, T doesn't have to be a Foo.

The Helper example shows how to calculate a return type.

foo :: (a: $A, b: $B) -> Helper(A,B).T {...}

Here the return type isn't directly a type, it in effect invokes Helper that takes the type of A and B, and produces a result. That style via #run is more powerful, seems kind of akin to some C++ meta programming style, inserting information into structs. The trick here is that Helper is a type, and it's evaluation (which uses compile time #run determines the final result, accessed as `.T).

JAI uses () for specialization. With a struct

Foo :: struct(x: $T) {...}
a : Foo(42); 

Hmm to my eyes that looks a bit to close to calling a function.

Foo :: struct(T: Type) {
    some_data : T;
}

f : Foo(int);

Because types are being passed in, is not so bad, but still a little worrying.

It is possible to get the specialization types (and perhaps values) for a specialized generic, by just accessing by their parameter name. This is helpful in later examples, where parameters are dependent on the type of one of the parameters.

Hash_Table :: struct (K: Type, V: Type, N: int) {
  keys: [N] K;
  values: [N] V;
}

function1 :: (table: Hash_Table($K, $V, $N), key: K, value: V) {
  // do stuff
}

function2 :: (table: $T/Hash_Table, key: T.K, value: T.V) {
  // do stuff
}

function3 :: (table: $T, key: T.K, value: T.V) {
  // do stuff
}

Perhaps it's of note that lower camel is used for built in functions, that for a type Type_Info is the name used.

The use of #modify allows for compile time checking.

I like that there is support for context. This is a more powerful context system than is seen in Odin. It is used for memory, logging, assert callback and other information can be added. Not clear how this would work across shared library boundaries.

Enum is interesting in that it doesn't use =, but the :: constant syntax.

The #align behavior seems a bit odd, in that it doesn't align the struct. That does allow usage of bytes after the struct I suppose, but that seems like a more unusual scenario, such that it seems like the default should align the struct, and there are other mechanisms for the special cases.

Meta programming

Has a lot of meta programming power. It's more like Zig than C++. The idea of using "regular programming" to do meta programming is compelling.

Handles compilation with a game or windows-like message loop.

Has "preload" module is akin to a "prelude".

Is a discussion about implementing (interface or trait](https://github.com/Jai-Community/Jai-Community-Library/wiki/Snippets-and--Benchmarks#interfacetrait). It embeds the 'vtbl' directly in the struct. Doesn't have any common interface features such as inheritance.

The EventBus, uses the common C idiom of having a function pointer, and void*.

Read

Other links

To do: