Getting Started - Jai-Community/Jai-Community-Library GitHub Wiki
This is a guide to help you get started with Jai. It assumes that you've done at least some programming before, and will often state the differences between Jai and languages such as C or C++. This section covers basic things such as variables, types, flow control, and looping. More advanced topics such as polymorphism, inline assembly, and context are covered under the advanced section.
- Download and extract the Jai compiler to a custom directory.
- On Linux, you might consider
/opt/jai
- On Linux, you might consider
- Set your environment to know how to find the
jai
-binary- for Windows, add the path to
jai.exe
to your%PATH
. - On Linux, a symbolic link like
ln -s /path/to/jai/bin/jai /usr/bin/jai
will get you there.
- for Windows, add the path to
- Compile a Jai program via
jai main.jai
, given that there's amain.jai
file in your current path. - Run the generated executable.
If you are having installation problems, consider reading the Troubleshooting section.
This horrific cliché of an initial program does nothing to express the actual power of any language, but it does serve as a convenient crib to get an idea of what the language will look like and its baseline semantics.
#import "Basic";
main :: () {
print("Hello, World!\n");
}
Save this to hello.jai
and compile it with jai hello.jai
. Then you should be able to run: ./hello
or hello.exe
(depending on your OS).
Some basics, here:
-
#import
imports a module, in this case the bundled moduleBasic
. - Each and every Jai program must have a
main
function. - The
::
operator is used to define a constant. Top level functions are typically constant. - The syntax is pretty C-like in many regard. Statements are terminated with
;
. - The function
print
comes from theBasic
library. More on it later!
By the way, comments are useful:
// Single line comment; starts at the slashes, ends at the end of line.
/*
* Multiline comment, starts at the opening /* and ends at the */
* But hey, here's something odd! In C or C++, the closing bit ^-- here would have ended the comment.
* But in Jai, you can nest multiline comments.
*/
Modules are stored in the compiler directory under jai/modules/
.
Modules can be imported via #import "ModuleName";
.
You can create your own module by simply putting it in jai/modules/
.
This can also be done inside a folder jai/modules/YourModuleFolder/
. In this case, you need to have a module.jai
inside the folder.
Modules can be assigned to identifiers e.g. Math :: #import "Math";
. Then everything inside the module will be namespaced with the given name, e.g. Math.sqrt(...)
.
Additional modules from other directories can be imported via the -import_dir "Path/To/Module"
flag, e.g. to load a module.jai
in the same folder:
jai hello_world.jai -import_dir "./"
You can load any jai
-file via #load
, e.g. #load "my_file.jai";
. The path is relative to the calling file.
Think of loading a file as pasting the code directly into the calling file: If you load two files
#load "file_a.jai";
#load "file_b.jai";
then all (exported) functions/structs/globals of file_a
will be available in file_b
and vice versa.
You can name modules that are imported. A named #import
allows you to namespace function. This allows you to resolve namespace collusions in code.
Math :: #import "Math";
y := Math.sqrt(2.0);
You can import a specific file
, dir
, and string
based on what you prefer.
module :: #import, file "themodule/module.jai";
This loads a file with a known filename, without searching the import_path
.
module :: #import, dir "files/directory";
This loads a directory-style module from a specific path.
#import, string "factorial :: (x: int) -> int { if x <= 1 return 1; return x*factorial(x-1);}";
This loads a specific string into your program. This string must be known at parse time, meaning it cannot import runtime code created by #run
directives.
Variables are defined using the :=
operator, which allows you to specify a type and a value for your new variable. Specifically:
name : type = value;
For example:
a : int = 3;
b : string = "hello";
We'll get into the details of the available types in a bit, but first, a bit more about this declaration syntax.
While name
obviously can't be skipped, either the type or the value can be. If you skip the type, it will be inferred from the provided value. If you skip the value, it will be initialized to whatever is the zero-equivalent for the type. For example:
a := 3; // Inferred as int.
b := 3.1415; // Inferred as float.
c := "hello"; // Inferred as string.
d : string; // Defaults to empty string, "".
e : *u32; // Defaults to a null pointer to a u32.
f := #char "1"; // Inferred as s64. f has the value of the ASCII character '1'
Earlier, we mentioned the ::
syntax, which is used instead of :=
when defining constants. But much like :=
, you can separate the two colons to specify the type if you want to be precise. For example:
PI :: 3.141592;
my_word : u16 : 65535;
You'll notice later that we typically define functions, structs and various other things as constants most of the time.
Jai comes with a number of basic types, along with some mechanisms to create custom types.
The basic types are:
-
bool
- a boolean, that can take valuestrue
orfalse
. This value takes up 8 bits of memory. -
s8
,u8
- signed and unsigned 8 bit integers. -
s16
,u16
- signed and unsigned 16 bit integers. -
s32
,u32
- signed and unsigned 32 bit integers. -
s64
,u64
- signed and unsigned 64 bit integers. -
float32
- 32 bit floating point number. -
float64
- 64 bit floating point number. -
string
- a string, an array view of u8. Jai strings are NOT zero-terminated
Additionally, there are int
which defaults to s64
and float
which defaults to float32
. The type void
also exists, but you'll probably use it less than in some other languages that have that type.
Although you declare variables using the :=
format, you assign values just using the =
-part, such as:
mynumber := 5; // Declare variable
mynumber = 17; // Assign 17 to variable.
Arithmetic is performed using the regular +
, -
, *
, /
and %
operators for addition, subtraction, multiplication, division and modulus respectively.
As in many programming languages, there are convenient variations on the assignment operator available for arithmetic manipulation as needed, namely +=
, -=
, *=
, /=
and %=
, corresponding to the appropriate arithmetic operators. Note that unlike some other languages, there are no increment or decrement operators.
If the user does not explicitly initialize a variable, the variable is set to zero. You can have explicitly uninitialized variables by typing a: int = ---
. Uninitialized variables have undefined behavior until a value is written to.
a, b = b, a
will swap elements. This also works for longer sequences with arbitrary permutations. All right hand side values are evaluated in the first pass, and then assignments are done to the left hand side values on the second pass.
In addition to a normal range of arithmetic operators, Jai has a number of boolean operators, namely:
-
!
- boolean NOT -
||
- boolean OR -
&&
- boolean AND
Additionally, it has a number of bitwise operators:
-
|
- bitwise OR -
&
- bitwise AND -
^
- bitwise XOR -
<<
- shift left -
<<<
- rotate left -
>>
- shift right -
>>>
- rotate right -
~
- bitwise NOT (one's complement) (unary)
The bitwise operators perform an arithmetic shift, following C's rules regarding bitwise operators.
Bitwise AND compares the respective bits between two numbers together. If both respective bits are 1, then the output is 1. If either respective bits are 0, then the output is zero.
11001100
& 10001000
----------
= 10001000
Bitwise OR compares the respective bits between two numbers together. If either respective bits are 1, then the output is 1. If both respective bits are 0, then the output is zero.
11001100
| 10000011
----------
= 11001111
You can write number literals in a number of ways. First off, you have typical decimal format, but additionally there are prefixes for hexadecimal and binary numbers. Unlike many languages, there is no special format for octal literals. You can optionally use an underscore to separate digit groups as desired. For example:
A :: 10; // here's a 10
C :: 0b10; // this is 2 in binary
B :: 0x10; // and this is 16 in hexadecimal
D :: 0b1010_0010_0101_1111;
E :: 0xFFFF_FF_FF; // This is inconsistent and weird, but legal.
F :: 16_777_216;
Strings are a datatype representing a sequence of characters, where each character is a byte, or u8
. Here is the definition for a string:
string :: struct {
count: int;
data: *u8;
}
The count
represents the length of the string, while data
is the pointer to the data. String has the same definition as array view.
Multi-line strings can be declared first by typing in the identifier, followed by an assign statement, #string
, following by a token indicator for the end of the string, followed by the multi-line string.
multi_line_string := #string END_STRING
This
is
a
multi-line
string.
END_STRING;
print(multi_line_string);
In the example above, multi_line_string
prints out:
This
is
a
multi-line
string.
Strings implicitly cast to boolean values that can be then used in if statements. If str
has characters in it, then the automatic cast to boolean
is true
. Otherwise if str
is an empty string, it is false
.
str : string = "Hello.";
if str {
} else {
}
Here are some common string operations people may use that are available in the String
and Basic
module:
// string comparing functions
equal :: (a: string, b: string) -> bool;
compare :: (a: string, b: string) -> int;
contains :: (str: string, substring: string) -> bool;
equal
checks if two strings are equal. Returns true if both strings are equal. Returns false if not equal. compare
compares two strings a
and b
. Returns -1 if a
is less than b
, 1 if a
is greater than b
, and 0 if they are equal. Similar to strcmp
in C. contains
returns true
if the string contains substring
.
// string begins with and ends with functions
begins_with :: (str: string, prefix: string) -> bool;
ends_with :: (str: string, suffix: string) -> bool;
begins_with
checks if a given string begins with a specified prefix. ends_with
checks if a given string ends with a specified suffix.
// concatenating and spliting strings
join :: (inputs: .. string, separator := "", before_first := false, after_last := false) -> string;
split :: (str: string, separator: string) -> [] string;
Joins all the string inputs together to form a larger string. For example: join("a","b","c","d")
outputs "abcd".
Splits an input string according to a separator.
// string to int/float and parsing functions
string_to_int :: (str: string) -> int, bool;
string_to_float :: (str: string) -> float, bool;
parse_float :: (line: *string) -> float, bool;
parse_int :: (line: *string) -> int, bool;
parse_token :: (line: *string) -> string, bool;
Attempts to parse a string into a float, token, or int.
// c string manipulation functions
to_c_string :: (str: string) -> *u8; // NOTE: This function heap allocates memory
c_style_strlen :: (ptr: *u8) -> int;
These functions manipulate c-strings received from c-libraries
The Any
Type is a type that is matches against all other types in the language. Structs, primitives, strings, arrays, array views, and dynamic arrays are all auto castable to Any
Type. Take for example the print
function in the Basic module:
print :: (fmt: string, param: ..Any) {
//..
}
The print function takes in a variable number of type Any
parameters at compile time, and the type information is available at runtime.
The Any
Type is a struct of two values: the type info and the pointer to the value.
Any :: struct {
type: *Type_Info;
value_pointer: *void;
}
When you assign a variable to Any
, it translates to the following:
number: int = 8;
any: Any;
any = number;
// these two expressions are the same.
any.type = type_of(number);
any.value_pointer = *number;
The type system in Jai is fairly strict. Types, though similar, may need explicit casting if there is any risk of loss of information. Therefore, you can cast a u16
to a u32
implicitly, but you can't cast a u32
to a u16
without being explicit about it, and the compiler will give you an error if you attempt to.
Instead, you can use cast()
, like so:
a : u32 = 50000;
b : u16;
b = cast(u16) a;
Now, in this case the value of a
in this case fit into a u16
, so this was no problem. But what happens if you had a bigger value, such as a = 16000000;
?
In this case, by default, you'll get a runtime error:
Cast bounds check failed. Number must be in [0, 65535]; it was 16000000. Site is example.jai:8.
This is to say, you cannot just throw away information without being explicit about your intent. This may seem annoying, but it's actually going to save you in a lot of cases. If you do decide to throw away unwanted information, you can do so in two ways - but note that they are actually identical: trunc
and no_check
. They both exist so that you can document your intent ─ are you trying to explicitly throw away information (trunc
), or are you just trying to make it run fast by skipping runtime boundary checks (no_check
)?
Either way, if you do b = cast,trunc(u16) a;
or b = cast,no_check(u16) a;
, the new value of b
is 9216, thereby truncating the bits you don't care about. This will speed up your code, so it can be good if you have a way to be certain that you'll not exceed the bounds, but if you're not careful something strange might happen.
It's worth noting that you can turn off bounds checking at run time (see Metaprogramming section) if you want your build to run at maximum speed, with the penalty of possible accidental truncation.
In many cases, you want to just cast but you don't really care what the receiving type is. For these situations there is an autocast operator, xx
, which you can use to cast while glossing over the details:
b = xx a;
A pointer is an address data type. It is used to store the address of another variable. In Jai, just like C, pointers are defined using the *
marker; but beware, the syntax is a bit different from C or C++.
a : int = 42; // Just a normal integer
b : *int; // Here's a pointer to a integer.
b = *a; // Point b at a.
print("Value of a is %, address of a is %\n", a, *a);
print("Value of b is %, but b points at address %\n", <<b, b);
print("Value of b is %, but b points at address %\n", b.*, b);
As you can see there, we use unary *
to get the address of a variable, and unary <<
to dereference a pointer. The .*
can also be used to dereference a pointer.
A pointer is a data address, which is a numeric value. You can perform arithmetic operations such as +
, -
, *
, and /
on a pointer just as you can on a numeric value. You can check pointers for equality using ==
, !=
, <
, <=
, >
, and >=
.
array: [10] int
a: *int = array.data; // set a to point to array.data
a += 1;
In the example above, incrementing pointer a
by 1 adds 8 to the pointer, since a 64-bit integer consists of 8 bytes. A pointer increments depending on the size of the data the pointer points to.
variable: *void;
is a void pointer, or a pointer with no associated data type. A void pointer can hold the address of any type and can be typcasted to any type. void pointer has the same functionality as in C.
Just like in C++, pointers can also point to other pointers, allowing multiple indirection.
a: int = 3;
b: *int = *a;
c: **int = *b;
d: ***int = *c;
print("%\n", << << << d); // prints out the value of a, which is 3
Function pointers can be declared almost in the same way as regular functions.
function :: (a: int, b: int)->int { // declare function
return a + b;
}
f_ptr: (int,int)->int = function;
// call the function that the function pointer is pointing to
c := f_ptr(1,2);
This is a list of keywords and identifiers available in the Jai Programming Language. This is not a comprehensive list, and is subject to change while the programming language is still inside the closed beta.
Keywords | Purpose |
---|---|
bool , true , false
|
boolean keywords |
int , s8 , u8 , s16 , u16 , s32 , u32 , s64 , u64
|
integers |
float , float32 , float64
|
float point numbers |
void |
Just like C, it means nothing, and when used in void* , means a pointer to anything |
enum , enum_flags
|
enums and enum_flags keyword |
size_of |
used to get the size of a type. To use it on a variable, do size_of(type_of(variable)) . |
struct , using , union
|
Keywords denoting a record with multiple data members |
string |
Denotes a string of characters such as "John Newton"
|
type_of |
used to get the type of something. |
cast |
used to cast a variable to a different type. For example, b := cast(int)a . |
if , ifx , then , else , case
|
if statement and branching keywords |
for , while
|
Looping and control flow statements |
break , continue , remove
|
Used to for control flow within a loop |
return |
Returns from a function |
defer |
Similar to the Go Language. This statement is executed at the closing of a code block. |
inline |
Forced inlining of a particular function |
In addition to scalar variables, Jai supports both static and dynamic arrays, and array views.
a : [4]u32; // An array of 4 u32 integers.
b : [30]float64; // An array of 30 float64's.
You access array members similarly to in other languages, using the []
subscript syntax.
a: [4]float;
a[0] = 10.0;
a[1] = 20.0;
a[2] = 1.4;
a[3] = 10.0;
print("a = %\n", a);
Note: The
%
to indicate insertion points for variables. Unlike the C languageprintf
, you don't need to specify what kind of thing is being printed, and it handles complex types too. However, if you want any special formatting of the variable to be printed, you must handle that separately.
You can initialize arrays using the following syntax:
array : [4]float = float.[10.0, 20.0, 1.4, 10.0];
Unlike C, Jai stores array length information. You can find out the array length by using array.count
.
print("array has % number of elements\n", array.count);
Arrays implicitly convert to boolean values, and can be used in if statements to check if the array has elements. If an array has at least one element in the array, the boolean value of the array is true
. If the array has zero elements, the boolean value of the array is false
.
array: [..] int;
if array {
}
Jai supports multi-dimensional static arrays. Multi-dimensional static arrays can be declared like this:
a: [4][4] float; // 2D static array
b: [4][4][4] float; // 3D static array
Multi-dimenional arrays can be initialized using the following syntax:
array: [2][2] int = .[int.[1,0], int.[0,3]];
A simple way of generating heap-allocated arrays is by #import "Basic";
and using NewArray
:
a := NewArray(4, float); // will heap-allocate an array of 4 floats.
You can also pass a custom allocator and more, see Basic/module.jai
(line 477 in beta 0.0.102)
Make sure to free memory via array_free(a);
.
Sometimes you don't know how many you will need in your list. Dynamic arrays are the most basic data structure at your disposal for arbitrary-length data. You can of course build much more powerful data structures if you need them, but you'll be surprised at how often a dynamic array is just what you need. Here's the declaration syntax:
a : [..]int; // A dynamic array of integers.
b : [..]string; // A dynamic array of strings.
A few things to note about dynamic arrays:
- They allocate memory as needed.
- When you add things, memory is reallocated as needed.
- They will use your context's default allocator; this will be explained later.
Here is the struct definition and member fields of the resizable array found in Preload.jai
:
Resizable_Array :: struct {
count: s64; // number of elements in the array
data : *void; // array data
allocated: s64; // total space used by the resizable array
allocator: Allocator; // the allocator in use by the resizable array
}
The resizable array functions similar to C++ std::vector
, but with the added bonus that they use your current context's allocator. Contexts are explained in a different section ─ for now, just know that this is pretty great.
With those caveats out of the way, here's how you work with them:
array_add(*myarray, 5); // Add 5 to the end of myarray
array_add(*myarray, 9); // Add 9 to the end of myarray
array_reset(*myarray); // Reset myarray
array_find(myarray, 5); // look for 5 in myarray
array_copy(*anotherarray, myarray); // copy array into anotherarray
In many cases, you'll be adding a number of entries to dynamic arrays at once, and you might even know how many there are. For this situation, it's worth considering that allocating once is almost always better than allocating as needed. Here we're going to demonstrate this using a loop ─ if these are unfamiliar to you, check ahead to the chapter on for
loops before reading on.
myarray : [..]int;
N :: 50;
for 1..N array_add(*myarray, it);
The above example works just fine, but involves many additional allocations for no reason, since we already knew we were going to add 50 items. So it's better to do:
myarray : [..]int;
N :: 50;
array_reserve(*myarray, N); // Reserve 50 items!
for 1..N array_add(*myarray, it);
This will only perform one allocation as opposed to guessing and adjusting every time array_add
is called.
Note that array_reserve
wants the total number of items to reserve, not the number to additionally reserve. So you may want to use myarray.count
or myarray.allocated
to get the number of items currently in the array, or the number currently reserved in the array.
Alternatively, you can write a small helper function as in NewResizableArray
.
When you're done with a dynamic array, it's good to array_free
the array. Consider using defer
for this!
The array view data structure represents a view into the data that is contained in an array or a subsection of an array. Here is how the array view is declared.
arr: []int = int.[1,2,3,4,5];
This is the array view struct declaration and data fields as found in Preload.jai
:
Array_View_64 :: struct {
count: s64; // number of elements
data : *u8; // pointer to element array data
}
Both Static Arrays and Dynamic Arrays are autocasted to Array Views if the array view is a parameter. Because strings are array views with u8
, both share the same definition.
The basic conditional statement in Jai is similar to other languages:
if a == b
print("They're equal!\n");
else
print("They're not equal!\n");
Unlike many other languages, the condition does not require parenthesis. Optionally, you can put a then
after the condition. This can be convenient for visual separation in some cases. Like so:
if a == b then print("They're equal!\n");
The comparison operators are:
-
==
- logical equivalence -
!=
- logical inequivalence -
<
- less than -
>
- greater than -
<=
- less than or equal -
>=
- greater than or equal
An if
statement can be followed by multiple else if
statements to test various other conditions.
grade := 100;
if grade >= 97 {
print("Your grade is an A+\n");
} else if grade >= 90 {
print("Your grade is an A\n");
} else if grade >= 80 {
print("Your grade is a B\n");
} else if grade >= 70 {
print("Your grade is a C\n");
} else if grade >= 60 {
print("Your grade is a D\n");
} else {
print("You grade is a F\n");
}
Here is the static compile-time version of #if
statements.
CONSTANT :: 3;
#if CONSTANT == 0 {
} else #if CONSTANT == 1 {
} else #if CONSTANT == 2 {
}
Just like C++, Jai has its own ternary operator statement. ifx
allows a programmer to condense a simple if
statement down to a single line statement. The syntax of ifx
is ifx
followed by a condition statement, the value assigned if the condition is true, else
, and finally the value assign if the condition is false.
a := 0;
b := 100;
c := ifx a > b 10 else 1000;
d := ifx a > b then 10 else 1000;
The if-case
statement in Jai allows a variable to be tested for equality against a list of values. Each value is called a case, and the variable is checked for each case. This if-case
statement is similar to a switch
statement in C, with a few exceptions. Unlike C, there is no need to put a break
statement after each case to prevent fallthrough, if there is a break
statement in the if-case
, the statement will attempt to break out of a loop the statement is nested in. Also unlike C, there is no need to add brackets to segregate the cases. case;
will assign to the default value.
a := 0;
if a == {
case 0;
print("case 0\n"); // because a=0, this if-case statement will print out "case 0".
case 1;
print("case 1\n"); // because a=0, this will be ignored
case;
print("default case\n"); // because a=0, this print will be ignored.
}
Fallthrough switch behavior like in C can be obtained by adding a #through;
at the end of a case statement.
a := 0;
if a == {
case 0;
print("case 0\n"); // because a=0, this if-case statement will print out "case 0".
#through;
case 1;
// because of the #through statement, this if-case statement will print out "case 1"
// in addition to "case 0".
print("case 1\n");
case;
print("default case\n"); // because there is no #through statement, this print will be ignored
}
if-case
statements work on integers, strings, enums, bools, arrays, and floats. Be careful when using if-case
statements with floats since floating point numbers approximate values.
The #complete
compiler directive requires you to fill out all the case possibilities when using enum. This is useful when adding additional enum members to an enum. #complete
only works when applied to enums or enum_flag datatypes.
Val :: enum { A; B; C; }
a := Val.A;
if #complete a == {
case Val.A;
print("This is Val.A case\n");
case Val.B;
print("This is Val.B case\n");
case Val.C;
print("This is Val.C case\n");
}
While loops simply loop until the loop condition is met. Their syntax is while condition action;
, where condition
is some expression that can be evaluated as true or false, and action is a statement or a block of statements. For instance:
n := 0;
while n < 10 {
n += 1;
}
This while
loop keeps on incrementing the n
variable when n
is less than 10. When n
is no longer less than 10, the program will terminate the loop.
If you have a type that can be iterated over, such as an array of some sort, you can use a for
loop to iterate through it. For-loops in Jai are actually deceptively powerful, for a few reasons.
The simple format for for loops is for set action
, where the set
is something that supports iteration and action
is a statement or a block.
To iterate over a sequence of numbers, say from 1 to 10, simply do:
for number:1..10 print("Number %\n", number);
Here, number
is the iterator variable name, but Jai allows you to skip it, in which case it will be called it
by default:
for 1..10 print("Number %\n", it);
It's worth noting that this will iterate from 1 to 10 inclusive, or, as mathematicians might put it, [1, 10].
Sometimes, you want the index to be a non s64
type. In the following example, i
is casted to a s8
type:
for i: 0..cast(u8)255 {
// casts i to s8.
print("%\n", i);
}
Often, rather than iterating over a sequence of numbers, you'll want to iterate over an array. Then you simply state the array name as the set. For instance:
my_array := u8.[5, 10, 15, 20, 25, 30];
for my_array {
print("We got a %\n", it);
}
In addition to it
, Jai also defines it_index
by default, which contains the index of the item.
foods := string.["Burek", "Pho", "Khachapuri", "Empanadas", "Jjajangmyeon"];
print("Top five dishes:\n");
for foods {
print(" %. %\n", it_index, it);
}
The break statement terminates the current loop immediately after the break statement is executed. The break
statement works in both for
and while
loops.
for i: 0..5 { // This for loop prints out 0, 1, 2 then breaks out of the loop
if i == 3
break;
print("%, ", i);
}
In the example above, the for loop loops three times, printing out 0, 1, 2, then the break statement stops the iteration.
The break
statement can also be used to break out of an outer loop through the syntax: break var
, where var
is the variable
name in the for loop. Here is the syntax for using break
to break
from an outer for
loop.
for i: 0..5 {
for j: 0..5 {
if i == 3
break i; // breaks out of the outer loop for i: 0..5
print("(%, %)", i, j);
}
}
The example above prints out (0,0), (0,1), (0,2), (0,3)... (2,5), then when it reaches i==3, the break statement stops the outer loop.
The continue statement is used to skip all the statements in the current loop after the continue statement is executed. The continue
statement works in both for
and while
loops.
for i: 0..5 { // This for loop prints out 0, 1, 2, 4, 5
if i == 3
continue;
print("%, ", i);
}
In the example above, the for loop loops six times, printing out 0, 1, 2, 4, 5. The value of 3 is not printed since the continue statement causes the program to skip the rest of the loop.
The continue
statement can also be used to skip to the outer loop through the syntax: continue var
, continue var
is the variable
name in the for loop.
for i: 0..5 {
for j: 0..5 {
if i == 3
continue i; // breaks out of the outer loop for i: 0..5
print("(%, %)", i, j);
}
}
The example above prints out (0,0), (0,1), (0,2), (0,3)... (2,5), (4,0), (4,1), (4,2)...(5,5). The for-loop skips all the value pairs starting with a 3, since the continue skips all the instructions after the continue statement.
break
and continue
can also be used to modify the control flow of a while
loop, not just a for
loop. As you can see in the example below, condition
is used to label a while
loop. There is a nested loop inside of the outer loop. break condition
is being used to break out of an outer while
loop when the control flow is in the inner loop.
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;
}
The remove statement is used to remove an element from a dynamic array [..] without needing to rewrite the entire for loop into a while loop. The remove statement assumes an unordered remove, the remove swaps the current element that is being iterated on with the last element, and then removes the last element. The remove statement happens in constant time O(1).
arr: [..] int;
for i: 0..10 {
array_add(*arr, i);
}
for a: arr {
if a == 2 {
remove a;
}
}
To do a for loop in reverse, add a <
in front of the for
loop. The for
loop will start at the beginning number and countdown to the ending number. In this example, the for
loop will iterate from 5 down to 0 inclusive.
for < i: 5..0 { // This for loop prints out 5 4 3 2 1 0 in that order
print("%\n", i);
}
To iterate an array by pointer, add a *
in front of the for
loop. Because you are taking a pointer to the array, you can modify the array elements. In this example, we take all the values in the array and square the elements. The resulting array should be int.[1, 4, 9, 16, 25]
;
array := int.[1, 2, 3, 4, 5];
for * ele : array {
val := <<ele;
<<ele = val * val; // take all the values in an array and square the elements
}
Jai allows you to use the for
loop to iterate over custom data structures. for
loops are designated through a macro as follows:
LinkedList :: struct {
data: int;
next: *LinkedList;
}
for_expansion :: (list: *LinkedList, body: Code, flags: For_Flags) #expand {
iter := list;
i := 0;
while iter != null {
`it := iter.data;
`it_index := i;
#insert body;
iter = iter.next;
i += 1;
}
}
In the example above, we define a custom LinkedList, a very common computer science data structure. We define using the for loop over that data structure by using a for_expansion
. for_expansion
takes in three parameters: a pointer to the data structure one wants to use the for loop on, a Code
datatype, and a For_Flags
flags. The #insert body;
inserts body of the for loop at that portion of the macro.
You need to backtick an it
and it_index
to get the for_expansion
working. Else, this is an error.
The For_Flags
enum_flags is found in Preload.jai
with the following definition:
For_Flags :: enum_flags u32 {
POINTER :: 0x1; // this for-loop is done by pointer.
REVERSE :: 0x2; // this for-loop is a reverse for loop.
}
In the #insert
directive, break
, continue
, and remove
can be redefined and the default behavior can be overwritten to do custom things.
#insert (break=do_something(), continue=do_something()) code;
There can be multiple ways to iterate a data structure that do not fit into the narrow descriptions of the basic For_Flags
enums such as reverse iteration or by pointer. For example, someone might want to make a Tree
struct with a breath-first search and a depth-first search iteration of a Tree
. This can be accomplished by writing a breath first search macro and a depth first search macro with the following parameter arguments:
macro :: (o: *Object, body: Code, flag: For_Flags) #expand;
Writing a macro with that function signature allows the macro to be used to label a for loop expansion. In the following example below, we create a bfs
and dfs
for expansion macro that allows the for loop to traverse either in breath first search or depth first search respectively.
tree: Tree;
for :bfs node: tree {
// breath first search the tree
print("%\n", node);
}
for :dfs node: tree {
// depth first search the tree
print("%\n", node);
}
Tree :: struct {
data: int;
left: *Tree;
right: *Tree;
}
bfs :: (t: *Tree, body: Code, flags: For_Flags) #expand {
// define breath first search here..
}
dfs :: (t: *Tree, body: Code, flags: For_Flags) #expand {
// define depth first search here..
}
A function is a group of statements that together perform a task. Every program has at least one function, which is main(), and all but the most trivial programs can define additional functions.
This is an example of how to declare a function with the name function
:
function :: (arg1: int, arg2: int, arg3: int) -> int {
// write function code here.
}
Here is how you call the function:
function(1, 2, 3);
function(arg1=1, arg2=2, arg3=3);
Functions can take multiple arguments and return multiple values. Unlike languages such as Rust or Go, functions do not return tuple object values, but rather return the values in registers. The idea of creating some kind of tuple type and then optimizing away the tuple type so it becomes a normal function is just adding unnecessary loads of work to the compiler optimizer.
function :: (arg1: int, arg2: int, arg3: int) -> ret1: int, ret2: int {
// write function code here.
}
ret1, ret2 := function(arg1=1, arg2=2, arg3=3);
You can ignore some or all of the return values of a function with multiple return values, unless the return value is tagged with a #must
directive. You can only get the return values in the order that you return the values, meaning in order to get the second returned value, you need to get the first value.
function :: () -> int, int, int {
}
a := function(); // get only the first value in the function
a, b := function(); // get the first and second value in the function
a, b, c := function(); // get all the return values.
It is sometimes useful to ignore some of the return values from a function. The _
can be used to ignore a particular return value. If you want to ignore a
and b
and use c
only, you can do:
// ignore a, b.
_, _, c := function();
In the case where one wants to declare several variables, but one already exists, one can add a modifiers '=' or ':' to each comma-separated argument to indicate what should happen if one wants it to be different from the rest of the statement. If a statement is a declaration of multiple variables, you can add '=' if one variable already exists.
b := 5;
a, b=, c := 1, 2, 3;
In this case, b=
is just assign to 2, the b
ignores the :=
operator, and b
is not being redeclared.
This syntax can be useful especially when writing parsing code. For example:
token: string = "1 2 3 4";
num1, success := parse_int(*token);
num2, success= := parse_int(*token);
num3, success= := parse_int(*token);
In the following example above, we reuse the success
boolean value for every parse_int
function rather than having multiple success values (e.g. success1
, success2
, success3
, etc.).
A named return value is merely a comment for a programmer. The name of the return value is not a variable, and is NOT a variable declaration. In the example below, the -> a: int, b: int {
part of the function signature does not declare a variable. The named return values serve as comments for the programmer to remind the programmer of what the return values mean. As one can see, you need to declare a: int = 100;
and b: int = 200;
later on in the function body.
function :: () -> a: int, b: int {
a: int = 100;
b: int = 200;
return a, b;
}
Jai functions can have default return values. A default return value, similar to a default function argument, is a value provided in a function that is automatically assigned by the compiler if the function doesn’t provide a value.
function :: (var: bool) -> a: int = 100, b: int = 200 {
if var then
return; // 100, 200 are automatically returned by default
else
return 1_000_000; //
}
a, b := function(true);
print("(%, %)\n", a, b); // prints out '(100, 200)'
a, b := function(false);
print("(%, %)\n", a, b); // prints out '(1000000, 200)'
Just like any other imperative programming language, you can have recursive functions:
factorial :: (a: int) -> int {
if a <= 1
return a;
return a * factorial(a - 1);
}
#this
refers to the current function/struct in the current scope. This is the same factorial function that performs in the same exact way as the recursive definition, except using #this
instead of calling factorial
directly.
factorial :: (a: int) -> int {
if a <= 1
return a;
return a * #this(a - 1);
}
Functions can be overloaded with several definitions for the same function name. The functions must differ from each other by the types and/or the number of arguments passed into it.
function :: (x: int) {
print("function overload 1\n"); // first overloaded function
}
function :: (x: int, y: int) {
print("function overload 2\n"); // second overloaded function
}
function(1); // prints "function overload 1"
function(1, 2); // prints "function overload 2"
In this example, the first overloaded function is called, printing out "function overload 1", then the second overloaded function is called, printing out "function overload 2".
You can tag a return value with a #must
directive to require the caller of the function to receive the return values of the function.
funct :: (x: int) -> int #must {
}
Just like C++, Jai functions can have default arguments. A default argument is a value provided in a function that is automatically assigned by the compiler if the caller of the function doesn’t provide a value.
// a = 0 by default
funct :: (a: int = 0) {
}
funct(); // a is passed 0
If a default parameter is used, parameters following a
need to be explicitly passed to a
.
funct :: (a: int = 0, b: int, c: int) {
}
funct(b=8, c=0);
A function can take a variadic number of arguments, or a variable number of arguments into a function. Consider the print
function inside the Basic module. Notice that it can take in either 1 argument, 2 arguments, 4 arguments, or indeed any number of arguments:
#import "Basic";
x, y, z, w := 0, 1, 2, 3;
print("Hello!\n"); //
print("x=%\n", x);
print("x=%, y=%, z=%, w=%\n", x, y, z, w);
You can create your own variadic function using the following syntax:
#import "Basic";
var_args :: (args: ..int) {
print("args=%\n", args);
}
var_args(1,2,3,4,5,6,7);
args := int.[1,2,3,4,5,6,7];
var_args(..args); // same as doing var_args(1,2,3,4,5,6,7);
var_args(args=..args); // same as doing var_args(1,2,3,4,5,6,7);
In variadic functions, the variadic arguments are passed to the function as an array, and arrays can be passed to variadic functions by adding a ..
to the array identifier.
Functions can be defined inside the scope of other functions. The function defined inside another function cannot access the local variables of the outer function.
function :: () {
x := 1;
inner_function();
inner_function();
inner_function :: () {
print("This is an inner function\n");
// x = 42; // this does not work! cannot access variable of inner_function scope!
}
}
If you want, however, to define "inner functions" that do access the outer scope variables, than one way of doing this is to use #expand
, see also the section below:
function :: () {
inner_function :: () #expand {
`x = 42;
}
x := 1;
inner_function();
}
Functions can be inlined through adding inline
to the function declaration. inline
replaces the function call with the actual body of the function. Unlike C or C++, inline
in Jai is not just a suggestion to inline a function, but forces the compiler to attempt to inline a function.
From beta 0.1.032
onwards the compiler does inline functions by default.
A function can be inlined from the function definition as follows:
function :: inline (a: int, b: int)->int {
//... function body
}
A function can also be inlined from the place the function is called:
answer := inline function(10, 20);
Lambda Expressions, i.e. simple small one-line functions, can be declared as follows:
funct :: (a, b) => a + b;
The following lambda expression takes in two parameters a
and b
, adds them together, and outputs a+b
.
Let's create an anonymous lambda expression to map a bunch of values from array_a
to array_b
. In this example, we add 100 to all the values in array_a
, and place those values in array_b
.
map :: (array_a: [$N] $T, f: (T)->T)-> [N]T {
array_b: [N] T;
for i: 0..N-1 {
array_b[i] = f(array_a[i]);
}
return array_b;
}
array_a := int.[1, 2, 3, 4, 5, 6, 7, 8];
array_b := map(array_a, (x)=>x+100); // array_b := int.[101, 102, 103, 104, 105, 106, 107, 108];
Unlike C++ or Rust, closures and capture blocks are not supported! The best way to get the desired functionality of closures would be to write a macro.
#bake_arguments
is a directive that takes an existing function and creates a new function with the existing function's arguments partially evaluated as constants at compile-time. This has similar functionality to currying in functional programming languages, except any random function argument parameter can be evaluated rather than depending on the order the parameters come in. The #bake_arguments
directive, unlike currying values in functional programming languages, cannot be done at runtime, and is only limited to compile-time evaluation.
add :: (a,b) => a+b;
add10 :: #bake_arguments add(a=10); // create an add10 function that adds 10 to a given number
b := 20;
c := add10(b); // prints out 30
Arguments can be baked into functions by adding a $
in front of the identifier.
funct :: ($a: int) -> int {return a + 100;} // $a is baked into the function due to `$` in front of a
$$
will attempt to bake arguments into the parameter list if the parameter value is constant. If not, it will be a regular function without the baking. You can use static #if
's to determine which version of the function is being called.
funct :: ($$a: int) -> int {
#if is_constant(a) {
print("a is constant\n");
return a + 100;
} else {
print("a is not constant\n");
return a + 100;
}
}
#bake_arguments
can also be used on a parameterized struct to partially evaluate the constants at compile-time.
A :: struct (M: int, N: int) {
array: [M][N] int;
}
AA :: #bake_arguments A(M=10);
a: AA(N=2);
print("a=(%,%)\n", a.M, a.N); //prints out "a=(10,2)".
Jai allows you to defer some execution until just before a function returns. For this, you use the defer
keyword. Simply put it before a function call to make it execute right at the end of the scope. Defer statements in loops are executed at the end of the loop, not the end of the function. (This is different from what you may be used to in Go).
print("1, ");
defer print("5, ");
print("2, ");
defer print("4, ");
print("3, ");
This will print out the text "1, 2, 3, 4, 5, ". Note that deferred statements execute in reverse. Things deferred first will be executed last.
The Jai Programming Language implements hygienic macros. Hygienic macros do not cause any accidental captures of identifiers. Hygienic macros modify variables only when explicitly allowed. Unlike the C programming language in which a macro is completely arbitrary, hygienic macros are more controlled, better supported by the compiler, and come with much better typechecking.
Macros can be created by adding the #expand
directive to the end of the function declaration before the curly brackets.
macro :: () #expand {
// This is a macro
}
Macros are similar to inline functions in that the compiler inlines the code with the macro functionality. Anything with a backtick is something the macro refers to in the outer scope. In this language, macros work like hygienic macros in Lisp: local variables are available locally, and if the macro refers to something in the outer scope, mark the variable with a backtick `
.
a := 0;
macro(); //call the macro
macro :: () #expand {
a := "No backtick"; // local variable, does not pollute the outer scope.
`a += 10; // add 10 to the "a" variable found in the outer scope.
}
Macros can take in Code
as an argument and #insert
directives can be used inside the macros to insert the code into the body of the macro.
macro :: (c: Code) #expand {
#insert c;
#insert c;
#insert c; // In this macro, we insert the code "c" into the macro three times
}
Just like regular functions, you can return values from macros.
max :: (a: int, b: int) -> int #expand {
if a > b then return a;
return b;
}
function :: () -> string {
c := max(2,3); // c = 3
return "done";
}
Variables are not the only piece of code that can be backticked "`". You can also backtick return values and defer statements. You cannot backtick continue
, break
, or remove
statements. Backticking return values means the macro returns from the outer scope. In this example, the backticked return from the macro return a string value from the outer function
.
function :: () -> string {
macro :: () -> int #expand {
`defer print("Defer inside macro\n");
if `a < `b {
`return "Backtick return macro"; // return a value from the "function"
}
return 1;
}
a := 0;
b := 100;
c := macro();
return "none";
}
s := function();
print("%\n", s); // s = "Backtick return macro"
Registers can be passed through macro arguments, giving you the power of macros while using inline assembly.
add_regs :: (c: __reg, d: __reg) #expand {
#asm {
add c, d;
}
}
main :: () {
#asm {
mov a:, 10;
mov b:, 7;
}
add_regs(b, a);
}
Macros can be nested. You call a macro within another macro. There is a macro limit, meaning there is a limit to how many macro calls you can generate. If you call a macro recursively (e.g. creating a fibonacci macro to call fibonacci recursively), this results in a compiler error that you hit a macro limit. The macro limit is by default 1000.
macro :: () #expand {
print("This is a macro\n");
nested_macro();
nested_macro :: () #expand {
print("This is a nested macro\n");
}
}
The following recursive fibonacci macro calls results in a compiler error, saying you hit the macro limit.
fibonacci :: () #expand {
fibonacci();
}
fibonacci();
Here's the error generated:
Error: Too many nested macro expansions. (The limit is 1000.)
If you want to make a recursive macro, compute the if
at compile-time with a compile-time #if
.
/*
// This version of the macro fails to compile since the 'if' is a runtime 'if'
factorial :: (n: int) -> int #expand {
if n <= 1 return 1;
else {
return n * factorial(n-1);
}
}
*/
// This code works and compiles.
factorial :: (n: int) -> int #expand {
#if n <= 1 return 1;
else {
return n * factorial(n-1);
}
}
x := factorial(5);
print("factorial of 5 = %\n", x);
Frequently, you will need to represent something more complicated than a single number or text string, or a list of the same. Things in reality will tend to have multiple dimensions of different types, such as a person who has a name, an age, a location, and a favorite animal. Using structures, we might define such a person like this:
Person :: struct {
name : string;
age : int;
location : Vector2;
favorite_animal : Animal;
}
Let's leave the definition of Animal
for now as a bit of foreshadowing, and instead focus on what the structure represents. It is a complex data type that allows you to keep track of multiple properties. You can use this new struct as a type for variables, and use the .
operator to reach into it for assignment and to read from it:
bob : Person;
bob.name = "Bob";
bob.age = 42;
bob.location.x = 64.14;
bob.location.y = -21.92;
print("% is aged % and is currently at %\n", bob.name, bob.age, bob.location);
You'll notice here that location, which is of type Vector2
, is also a structure. Vector2
is a vector that is part of the Jai Math
module.
It's worth noting that unlike C/C++, when you have a pointer to a structure, you don't need special syntax to dereference properties. The same .
notation just works:
move_person :: (person: *Person, newlocation: Vector2) {
person.location = newlocation;
}
As you can see there, assignment of entire structures also works; it will copy the values.
This section will go through several different ways to initialize a struct. All the different ways are different aesthetic ways to write the same code.
Vec3 :: struct {
x: float;
y: float;
z: float;
}
The first way we initialize a struct is the most obvious way that we have already seen in previous sections:
vec3: Vec3;
vec3.x = 1.0;
vec3.y = 2.0;
vec3.z = 3.0;
structs
can be initialized using struct
initializers.
vec3 := Vec3.{1, 2, 3};
struct
initializers can take in named parameters.
vec3 := Vec3.{x=1, y=2, z=3};
struct
initializers work during runtime.
x := 1.0;
y := 2.0;
z := 3.0;
vec3 := Vec3.{x=x, y=y, z=z};
Unions are a data type that can only hold one of its non-static fields at a time. Unions have the same exactly functionality like in the C programming language.
T :: union {
a: s64 = 0;
b: float64 = 5.0;
c: Type;
}
t: T;
t.a = 100;
print("t.a = %\n", t.a); // prints out 100
t.b = 3.0;
print("t.b = %\n", t.b); // prints out 3.0
print("t.a = %\n", t.a); // prints out gibberish, since b has been assigned
t.c = s64;
print("t.c = %\n", t.c); // prints out s64
print("t.a = %\n", t.a); // prints out gibberish, since b has been assigned
You can obtain the same union
functionality using #place
directives.
T :: struct {
a: s64;
#place a;
b: float64;
#place a;
c: Type;
}
In unions
or #place
directive, when you initialize multiple values to some piece of memory, they get overwritten in that order.
structs
, unions
, and enums
can be declared anonymously, without a type name attached to it.
struct {
// This is an anonymous struct.
x: int;
y: int;
z: int;
}
Structs of Arrays is a way of rearranging the layout of the data fields of a struct. Specifically, Structs of Arrays (SoA) is a data layout separating elements of a struct into one parallel array per field. Take the following example:
// this is just a normal struct, no SOA
Vec3 :: struct {
x: float;
y: float;
z: float;
}
// this is a SOA Vec3 struct
SOA_Vec3 :: struct {
x: [100] float;
y: [100] float;
z: [100] float;
}
Using an #insert
directive and generating code at compile time, we can generalize the concept of SOA to any structs using the following:
Vec3 :: struct {
x: float;
y: float;
z: float;
}
Person :: struct {
age: int;
is_cool: bool;
}
SOA :: struct(T: Type, count: int) {
#insert -> string {
t_info := type_info(T);
builder: String_Builder;
defer free_buffers(*builder);
for fields: t_info.members {
print_to_builder(*builder, " %1: [%2] type_of(T.%1);\n", fields.name, count);
}
result := builder_to_string(*builder);
return result;
}
}
// create an soa_vec3
soa_vec: SOA(Vec3, 10);
for i: 0..soa_vec.count-1 {
print("soa_vec.x[i]=%, soa_vec.y[i]=%, soa_vec.z[i]=%\n", soa_vec.x[i], soa_vec.y[i], soa_vec.z[i]);
}
// create an soa_person
soa_person: SOA(Person, 10);
for i: 0..soa_person.count-1 {
print("soa_person.age[i]=%, soa_person.is_cool[i]=%\n", soa_person.age[i], soa_person.is_cool[i]);
}
An enumerator is a user defined datatype to assign names to integer constants. This is how to declare an enum:
my_enum :: enum {
A;
B;
C;
D;
}
By default, my_enum.A
is the value of 0, and the subsequent values for B
, C
, and D
are 1, 2, 3 respectively.
my_enum :: enum {
A :: 100;
B;
C;
D;
}
By default, an enum variable is 64 bits wide. To change the default enumerator value, add a specifying integer type in front of the variable.
my_enum :: enum s16 { // This enum is a signed s16
A;
B;
C;
D;
}
An enum flag is an enumerator where each individual bit is an individual flag value of true or false. The next value in an enum flag is bit shifted to the left rather than incremented as in enums.
flags :: enum_flags u32 {
A; // A = 0b00_01
B; // B = 0b00_10
C; // C = 0b01_00
D; // D = 0b10_00
}
Enum flags can either be assigned to or use bit manipulation to set certain values to either true or false. The compiler recognizes when flags are set/unset, and will print out the flags accordingly.
using flags;
print("%\n", A|B); // will print out "A|B"
print("%\n", A|B|C); // will print out "A|B|C"
var := A|B|C|D;
print("%\n", var); // wil print out "A|B|C|D"
You can assign enum flags in the following ways:
f: flags = flags.A | .B;
f: flags = .A;
f: flags = 1; // numbers
f: flags = flags.A + 1;
Jai allows using
keyword on structs
, unions
, and enums
, importing the members into that particular scope.
We use struct inclusion on a
to access x
, y
, and z
directly without needing to do something such as a.x
. Same example works with unions too.
Vec3 :: struct {
x: float; y: float; z: float;
}
a: Vec3 = Vec3.{1,2,3};
using a;
print("a.x=%\n", x); // no need to do a.x, just access x directly
Putting a using
on an enum imports the enum into the scope.
Enum :: enum {
A; B; C; D;
}
using Enum;
e := A;
if e == {
case A; print("e=A\n");
case B; print("e=B\n");
case C; print("e=C\n");
case D; print("e=D\n");
}
using
can contain modifiers to allow for more fine grained control over the using
. This is useful especially when programs get larger and there are more naming conflicts. Here are some modifiers that can be applied to using
:
,only
,except
,map
,except
means the using
will import all identifiers found, except the identifiers found in the name list. The identifiers in the named list will be excluded from the using
.
Obj :: struct {
x: float;
y: float;
z: float;
}
function :: (using, except(x) obj: Obj) {
print("obj.x = %\n", obj.x); // x is excluded by the 'except', so need to access it by obj.x
print("obj.y = %\n", y); // y is included in the using
print("obj.z = %\n", z);
}
,only
means the using
will only apply to the names listed in the name list, and ignore everything else. The identifiers not found in the named list will be excluded from the using
.
Obj :: struct {
x: float;
y: float;
z: float;
}
function :: (using, only(x) obj: Obj) {
print("obj.x = %\n", x); // x is included in the only, so it can be access by only typing 'x'
print("obj.y = %\n", obj.y);
print("obj.z = %\n", obj.z);
}
,map
takes in a function that modifies an array of strings, and maps all the function names to a different set of names of your choosing.
Obj :: struct {
x: float;
y: float;
z: float;
}
add_a_char :: (array: [] string) {
character := "0";
for *identifier: array {
// str = str + "0"
<<identifier = join(<<identifier, character);
}
}
function :: (using, map(add_a_char) obj: Obj) {
print("obj.x0 = %\n", x0); // x
print("obj.y0 = %\n", y0); // y
print("obj.z0 = %\n", z0);
}
#import "Basic";
#import "String";
In the following example, we change all the identifiers in the struct from x
, y
, z
to x0
, y0
, and z0
.
#as
indicates that a struct can implicitly cast to one of its members. It is similar to using, except #as does not also import the names. #as works on non-struct-typed members. For example, you can make a struct with a int member, mark that #as, and pass that struct implicitly to any procedure taking a int argument.
num: Number;
// pass 'num' to the function as an int
function(num);
Number :: struct {
#as a: int;
}
function :: (a: int) {
// do something here...
}
Types are a first class type in Jai. You can do the things with a type that you would with any other variable. You can get the type of some variable using type_of
. The Type
from type_of
is a Type
.
var: Type = int;
print("%\n", var); // prints "s64". since var=int and int is a synonym for s64.
var = Vec3;
print("%\n", var); // print "Vec3", since var=Vec3.
You can compare types for equality. The type matches another type in cases when int == int
, float == float
, etc. int != u8
.
var: Type = int;
if var == int then {
print("var = %\n", var); prints "var = s64"
}
Type_Info
is a struct containing all sorts of type information, such as the name of the struct members, the offset of the member in bytes, the type of the members, notes attached to the members, etc.
Vec3 :: struct { x, y, z: float; }
info := type_info(Vec3);
for member : info.members {
print("%\n", member.name); // prints out x, y, z
}
To check if a type is any version of a polymorphic struct with a particular name, you can do:
is_complex_number :: (T: *Type_Info) -> bool {
if T.type != .STRUCT then return false;
S := cast(*Type_Info_Struct)T;
if S.name == "Complex" then return true;
return false;
}
In this example, if the struct is of the name Complex
, where Complex
is any type of struct with the name Complex
, the function will return true
.
You can add a backslash followed by multiple spaces in between identifier names. The ability to add backslashes followed by multiple spaces is for purely aesthetic purposes.
helloworld := 0;
hello\ world += 1; // add 1 to "helloworld".