Skip to content
Paolo Angeli edited this page Oct 13, 2019 · 16 revisions

The documents in this page come from the original BSVino's files but now are obsolete and need heavy rearrangement. ASAP this text will be updated and moved from here.


Navigate


Jai Language Features

Data-Oriented Structures

SOA AND AOS

Modern processors and memory models are much faster when spatial locality is adhered to. This means that grouping together data that is modified at the same time is advantageous for performance. So changing a struct from an array of structures (AoS) style:

struct Entity {
    Vector3 position;
    Quaternion orientation;
    // ... many other members here
};

Entity all_entities[1024]; // An array of structures

for (int k = 0; k < 1024; k++)
    update_position(&all_entities[k].position);

for (int k = 0; k < 1024; k++)
    update_orientation(&all_entities[k].orientation);

to a structure of arrays (SoA) style:

struct Entity {
    Vector3 positions[1024];
    Quaternion orientations[1024];
    // ... many other members here
};

Entity all_entities; // A structure of arrays

for (int k = 0; k < 1024; k++)
    update_position(&all_entities.positions[k]);

for (int k = 0; k < 1024; k++)
    update_orientation(&all_entities.orientations[k]);

can improve performance a great deal because of fewer cache misses.

However, as programs get larger, it becomes much more difficult to reorganize the data. Testing whether a single, simple change has any effect on performance can take the developer a long time, because once the data structures must change, all of the code that acts on that data structure breaks. So Jai provides mechanisms for automatically transitioning between SoA and AoS without breaking the supporting code. For example:

Vector3 :: struct {
    x: float = 1;
    y: float = 4;
    z: float = 9;
}

v1 : [4] Vector3;     // Memory will contain: 1 4 9 1 4 9 1 4 9 1 4 9

Vector3SOA :: struct SOA {
    x: float = 1;
    y: float = 4;
    z: float = 9;
}

v2 : [4] Vector3SOA;  // Memory will contain: 1 1 1 1 4 4 4 4 9 9 9 9

Getting back to our previous example, in Jai:

Entity :: struct SOA {
    position : Vector3;
    orientation : Quaternion
    // .. many other members here
}

all_entities : [4] Entity;

for k : 0..all_entities.count-1
    update_position(&all_entities[k].position);

for k : 0..all_entities.count-1
    update_orientation(&all_entities[k].orientation);

Now the only thing that needs to be changed to convert between SoA and AoS is to insert or remove the SOA keyword at the struct definition site, and Jai will work behind the scenes to make everything else work as expected.

Polymorphic Procedures

FUNCTION POLYMORPHISM

Jai’s primary polymorphism mechanism is at the function level, and is best described with an example.

sum :: (a: $T, b: T) -> T {
    return a + b;
}

f1: float = 1;
f2: float = 2;
f3 := sum(f1, f2);

i1: int = 1;
i2: int = 2;
i3 := sum(i1, i2);

x := sum(f1, i1);

print("% % %\n", f3, i3, x); // Output is "3.000000 3 2.000000"

When sum() is called, the type is determined by the T which is preceded by the $ symbol. In this case, $ precedes the a variable, and so the type T is determined by the first parameter. So, the first call to sum() is float + float, and the second call is int + int. In the third call, since the first parameter is float, both parameters and the return value become float. The second parameter is converted from int to float, and the variable x is deduced to be float as well.

THE ANY TYPE

Jai has a type called Any, which any other type can be implicitly casted to. Example:

print_any :: (a: Any) {
    if a.type.type == Type_Info_Tag.FLOAT
        print("a is a float\n");
    else if a.type.type == Type_Info_Tag.INT
        print("a is an int\n");
}

BAKING

… this section is not written yet! Sorry. (The #bake directive emits a function with a combination of arguments baked in. For example, #bake sum(a, 1) becomes equivalent to a += 1.)

Memory Management

Jai does not and will never feature garbage collection or any kind of automatic memory management.

STRUCT POINTER OWNERSHIP

Marking a pointer member of a struct with ! indicates that the object pointed to is owned by the struct and should be deleted when the struct is deallocated. Example:

node :: struct {
    owned_a : !* node = null;
    owned_b : !* node = null;
}

example: node = new node;
example.owned_a = new node;
example.owned_b = new node;

delete example; // owned_a and owned_b are also deleted.

Here, owned_a and owned_b are marked as being owned by node, and will be automatically deleted when the node is deleted. In C++ this is accomplished through a unique_ptr<T>, but Jai considers this the wrong way to do it, because the template approach now masks the true type of the object. A unique_ptr<node> is no longer a node—it’s a unique_ptr masquerading as a node. It’s preferable to retain the type of node*, and retain the properties of node*-ness that go along with it, because we don’t really actually care about unique_ptr for its own sake.

LIBRARY ALLOCATORS

… this section is not written yet! Sorry. (Jai provides mechanisms for managing the allocations of an imported library without requiring work from the library writers.)

INITIALIZATION

Member variables of a class are automatically initialized.

Vector3 :: struct {
    x: float;
    y: float;
    z: float;
}

v : Vector3;
print("% % %\n", v.x, v.y, v.z); // Always prints "0 0 0"

You can replace these with default initializations:

Vector3 :: struct {
    x: float = 1;
    y: float = 4;
    z: float = 9;
}

v : Vector3;
print("% % %\n", v.x, v.y, v.z); // Always prints "1 4 9"

va : [100] Vector3;                                 // An array of 100 Vector3
print("% % %\n", va[50].x, va[50].y, va[50].z); // Always prints "1 4 9"

Or you can block the default initialization:

Vector3 :: struct {
    x: float = ---;
    y: float = ---;
    z: float = ---;
}

v : Vector3;
print("% % %\n", v.x, v.y, v.z); // Undefined behavior, could print anything

You can also block default initialization at the variable declaration site:

Vector3 :: struct {
    x: float = 1;
    y: float = 4;
    z: float = 9;
}

v : Vector3 = ---;
print("% % %\n", v.x, v.y, v.z); // Undefined behavior, could print anything

va : [100] Vector3 = ---;
print("% % %\n", va[50].x, va[50].y, va[50].z); // Undefined behavior, could print anything

By explicitly uninitializing variables rather than explicitly initializing variables, Jai hopes to reduce cognitive load while retaining the potential for optimization.

INLINING

test_a :: () { /* ... */ }
test_b :: () inline { /* ... */ }
test_c :: () no_inline { /* ... */ }

test_a(); // Compiler decides whether to inline this
test_b(); // Always inlined due to "inline" above
test_c(); // Never inlined due to "no_inline" above

inline test_a(); // Always inlined
inline test_b(); // Always inlined
inline test_c(); // Always inlined

no_inline test_a(); // Never inlined
no_inline test_b(); // Never inlined
no_inline test_c(); // Never inlined

Additionally, there exist directives to always or never inline certain procedures, to make it easier to inline or avoid inline conditionally depending on the platform.

test_d :: () { /* ... */ }
test_e :: () { /* ... */ }

#inline test_d    // Directive to always inline test_d
#no_inline test_e // Directive to never inline test_e

Other Cool Stuff

Things that C/C++ should have had a long time ago:

  • Multi-line block comments
  • Nested block comments
  • Specific data types for 8, 16, and 32 bit integers
  • No implicit type conversions
  • No header files
  • . operator for both struct membership and pointer dereference access—no more ->
  • A defer statement, similar to that of Go

Planned

Here’s a short list of planned features to be added to Jai.

  • Automatic build management—the program specifies how to build it
  • Captures
  • LLVM integration
  • Automatic versioning (see below)
  • A better concurrency model
  • Named argument passing
  • A permissive license

Not Planned

Jai will not have:

  • Smart pointers
  • Garbage collection
  • Automatic memory management of any kind
  • Templates or template metaprogramming
  • RAII
  • Subtype polymorphism
  • Exceptions
  • References
  • A virtual machine (at least, not usually—see below)
  • A preprocessor (at least, not one resembling C’s—see below)
  • Header files

If it sounds odd to you that Jai is a modern high-level language but does not have some of the above features, then consider that Jai is not trying to be as high-level as Java or C#. It is better described as trying to be a better C. It wants to allow programmers to get as low-level as they desire. Features like garbage collection and exceptions stand as obstacles to low-level programming.


Proposed Features

These are a few proposed features that have not been implemented yet. To my knowledge they’re not yet in the language. Syntax is preliminary and likely to change.

  • Joint Allocations:
Mesh :: struct {
    name: string;
    filename: string;
    flags: uint;

    positions: [] Vector3;
    indices: [] int;        @joint positions
    uvs: [] Vector2;        @joint positions
}

example_mesh: Mesh;
example_mesh.positions.reserve(positions: num_positions,
                               indices: num_indices,
                               uvs: num_uvs);

Here we want to avoid multiple memory allocations, so we have the compiler do a joint allocation between positions, indices, and uvs, and divide the memory up accordingly. Currently this is done manually in C, and is prone to errors.

  • Optional Types:
do_something :: (a: Entity?) {
    a.x = 0; // ERROR!
    if (a) {
        a.x = 0; // OK
    }
}

The idea here is to prevent one of the most common causes of crashes, null pointer dereferencing. The ? in the above code means that we don’t know whether or not the pointer is null. Trying to dereference the variable without testing to see if it is null would result in a compile-time error.

  • Automatic Versioning:
Entity_Substance :: struct { @version 37
    flags: int;
    scale_color: Vector4;   @15
    spike_flags: int;       @[10, 36]
}

Here we are providing markup for our data structures that indicate to the compiler what version of the struct each member was present in. These versioning schemes would be used as part of an automatic serialization/introspection interface, but there are no details on that, other than that the language should have some capability of introspection.


Compilation keyword #bake to specialize polymorphic functions. A polymorphic function can contain also 'internal' polymorphic type but they can only be used after 'baking' them.

Compilation keyword #modify is used to make constraints/transformations on polymorphic types. For example, it allows to define default output polymorphic type, make only one implementation functions for different type u8..32-->u64, complex mapping to reduce the number of generated functions NdT: Powerful, but can be used to build function difficult to understand. It uses parameters names as input.

Compilation keyword #body_text takes the types of the parameters, then return a string containing the body of the function It's planned to have another keyword #body_if for 'inline' compile-time code generation.

#bake_values: a compile time specialization of some function with some (or all) parameters hardcoded. Useful to control inlining, somewhat duplicate (force) inline, but easier to use with functions with lots of arguments.

$$: an autobaker for function parameters prefixed by $$ and called with literals (currently no analysis to check if the parameter is known at compile time).

End of new features, demonstration of map which shows 'high-levelness' of Jay, but requires to free the allocated memory or there is a memory leak (no GC). And of map taking an allocator param which allows usage of the stack (faster than heap) for 'inline map'.

In the Q&A video, Blow think that 'libraries' will be pre-parsed code but not binary, to allow these features working.

04/08/2015 Demo: Bounds check, here strings, overloading. [renox, I didn't have the patience to listen to the full Q&A]

  • Array Bound Checking (ABC): By default arrays(both SoA and AoS) and strings are bound checked, for optimization there is a 'no array bound checking' directive: #no_abc (which works either on a statement or on a block), you can also disable globally ABC, or enforce always ABC (ignoring the #no_abc directive).

SoA pointers are also bound checked as they're 'fat pointers' aka arrays.

  • Here strings:
# string <end keyword>
...
<end keyword>

Currently no indentation support and the must be at the beginning of the line. Strings are Unicode strings. Can be empty.

  • overloaded function: Overloading works as usual (and also with 'inheritance': Jai 'using' feature). One main improvement: with integer literals, the smaller int type which can support the literal is called. Works with multiple parameters. If there are several possible overload with the same level of conversion, it's a compilation error. Integer conversion are preferred to float conversion.

There are 'surprising' features related to overloading on which JB ask feedback:

  1. overloading is scoped: if you can find a match in the local scope, it is selected, even if there is another better match in the outside scope baz :: (a: u8) { ... } { baz :: (a: u16) { ... } baz(100) // this use the u16 version. }

  2. overloading only work on constant function definition (not on functions pointers, lambda).

  3. overloading induce a 'file scope'? [renox. I didn't catch this point]

In the Q&A: parametrized type cannot be overloaded currently.

Types, constants and variables

  • Variables and assignments
  • Language data types
  • Simple user-defined data types
  • Expressions and operators
  • Type-casting
  • Pointers

Flow control

Procedures and functions

  • Declarations
  • Arguments / Parameters
  • Return values
  • Overloading / Polymorhism
  • Advanced features
  • Lambdas

Aggregated data types

  • Arrays
  • Strings
  • Composition of Structs

Advanced features

Clone this wiki locally