20 KiB
Jai Language Reference (for plugin implementors)
Provenance: Everything here was derived from the local Jai distribution at
~/.local/jai — primarily modules/Jai_Lexer/module.jai (the compiler's own
lexer, authoritative for tokens/keywords), plus how_to/ and modules/.
No online sources were used; online Jai material is outdated.
Anything marked ⚠️ is inferred rather than directly confirmed in the lexer.
Re-verify against: ~/.local/jai/modules/Jai_Lexer/module.jai
1. Source layout
- Extension:
.jai - Encoding: UTF-8 assumed; the language imposes no encoding on
string. - Line endings:
\nor\r\n(here-strings normalize to\n). - No zero-terminated strings; no preprocessor in the C sense.
2. Comments
// line comment to end of line
/* block comment
/* THESE NEST — track depth, do not stop at the first */
still inside */
Nested block comments are real. Jai_Lexer/module.jai:1436 maintains
comment_depth, incrementing on /* and decrementing on */.
A lexer that stops at the first */ is wrong.
Note the source comment at line 1433: the lexer will treat /////* as a nested
open-comment (the author flags this as possibly undesirable). Match the simple
depth-counting behavior.
3. Keywords
Exact list, from check_for_keyword (module.jai:758). These are the only
identifiers promoted to keyword tokens.
| Len | Keywords |
|---|---|
| 2 | if xx |
| 3 | ifx for |
| 4 | then else null case enum true cast |
| 5 | while break using defer false union |
| 6 | return struct remove inline |
| 7 | size_of type_of code_of context |
| 8 | continue operator |
| 9 | type_info no_inline interface |
| 10 | enum_flags |
| 11 | is_constant |
| 12 | push_context |
| 14 | initializer_of |
Flat list:
if xx ifx for then else null case enum true cast while break using defer false
union return struct remove inline size_of type_of code_of context continue
operator type_info no_inline interface enum_flags is_constant push_context
initializer_of
Notes:
xxis the auto-cast operator (KEYWORD_AUTO_CAST), not an identifier.removeis a loop-body statement for removing the current element.thenis used byifx(ifx cond then a else b).interfaceappears in type restrictions:(x: $T/interface Matchable).struct,union,enum,enum_flagsare all type-constructor keywords.
Not keywords (contrary to what one might assume): int, float, bool,
string, s8..s64, u8..u64, float32, float64, void, Type, Any,
Code. These are ordinary identifiers resolving to built-in types. Highlight
them as built-in types, in a distinct style from keywords, and do not let a
parser depend on them being reserved.
Built-in type names (identifiers, highlight separately)
s8 s16 s32 s64 u8 u16 u32 u64 int
float float32 float64 bool string void
Type Any Code
int is an alias for s64; float is an alias for float32.
4. Operators and punctuation
From Token_Type (module.jai:17). Multi-character tokens must be matched
longest-first.
Arithmetic / assignment
+ - * / %
+= -= *= /= %=
Comparison / logical
== != < > <= >= && || !
&&= ||=
ISEQUAL_FOR_SWITCH_STATEMENT is a distinct token: == immediately followed by
a { block introduces the switch form (if x == { case ...; }). The lexer
distinguishes it; a highlighter can ignore the distinction, a parser cannot.
Bitwise
& | ^ ~
&= |= ^=
<< >> (SHIFT_LEFT, SHIFT_RIGHT)
<<= >>=
<<< >>> (ROTATE_LEFT, ROTATE_RIGHT — disambiguated by the parser, not the lexer)
<<<= >>>=
Distinctive Jai tokens
| Token | Name | Meaning |
|---|---|---|
-> |
RIGHT_ARROW |
procedure return type |
.. |
DOUBLE_DOT |
inclusive range, for 0..7 |
$ |
polymorphic type/value capture | |
$$ |
DOUBLE_DOLLAR |
optionally-constant parameter |
--- |
TRIPLE_MINUS |
"do not initialize", x: T = ---; |
-- |
DOUBLE_MINUS |
distinct token (⚠️ not a decrement operator in normal code) |
,, |
DOUBLE_COMMA |
inline context modification, join(a,, allocator=temp) |
.{ |
BEGIN_STRUCT_LITERAL |
struct literal |
.[ |
BEGIN_ARRAY_LITERAL |
array literal |
=> |
QUICK_LAMBDA |
quick lambda, x => x.count |
.* |
POSTFIX_DEREFERENCE |
pointer dereference, ptr.* |
* |
POINTER_DEREFERENCE |
prefix: address-of AND pointer-type marker |
=== |
TRIPLE_EQUALS |
#asm register pinning |
` |
backtick | see §9 |
: :: := |
declaration forms, see §6 |
Critical * semantics — inverted vs C. *T is "pointer to T" and *value
is "address of value". Dereference is postfix .*. There is no prefix *
dereference. This matters for any expression parser.
- lexing (module.jai:1680): -> then --- then -- then -= then -.
5. Literals
Integers
123
1_000_000 // underscores allowed anywhere as separators
0xfade_deaf // hex
0b101101101 // binary
Default type of an integer literal is s64, but literals implicitly convert to
any type they fit in.
Floats
37.0
1.111
0h7fbf_ffff // IEEE-754 bits in hex ("hexfloat"), 16 hex digits max
0h8000_0000_0000_0000 // 64-bit negative zero
Default is float32 unless precision demands float64.
Value_Flags tracks HEX, BINARY, FLOAT, REQUIRES_FLOAT64, OVERFLOWED.
Strings
"Hello, Sailor!" // escapes: \n \t \" \\ etc.
Strings are {count: s64, data: *u8} views — not zero-terminated, and
subscripting yields u8 (there is no character type).
Here-strings (#string)
THE_STRING :: #string DONE
Anything at all, including "quotes" and \n literally.
DONE
Syntax: #string <IDENT> then a newline, then raw text, terminated by a line
that starts with <IDENT>. The terminator identifier is arbitrary (DONE
is only convention). Flagged as Value_Flags.HERE_STRING.
There is a #string variant used with imports: #import,string #string DONE.
For an IntelliJ lexer this needs a dedicated state with the terminator captured, much like heredocs in shell/Perl.
Character literals
#char "a" // yields a u8
#char is a directive, not a 'x' literal form. There is no single-quote
character literal in Jai. Do not lex ' as a string delimiter.
Notes (@)
foo :: () { } @PrintLike @Deprecated
x: int; @Cleanup
@Identifier is a NOTE token. Common in the wild: @Cleanup, @Incomplete,
@Speed, @Robustness, @Temporary, @ToDo, @Hack, @Copypasta, @test,
@PrintLike, @NoProfile. Treat as metadata/annotation for highlighting.
Note_Flags.IS_SYSTEM_LEVEL distinguishes compiler-known notes.
6. Declarations
The universal declaration form is name : type = value, with parts omissible:
a : float = 37.0; // explicit type + value
b : float; // explicit type, default-initialized to zero
c := 111.0; // type inferred (: and = merge into :=)
d : float : -123.45; // CONSTANT (second colon instead of =)
e :: 42; // constant, type inferred
f : T = ---; // explicitly UNINITIALIZED (no zeroing)
x, y, z: float; // compound declaration
Everything is zero-initialized by default unless = --- is used.
:: (constant) is how procedures, structs, and enums are declared — they are
just constant values:
main :: () { }
Person :: struct { }
Fruit :: enum u32 { }
This is the single most important structural fact for a parser: there is no
func/fn/class keyword. A top-level declaration is
IDENT :: <struct|enum|enum_flags|union|(params)...|expr>.
7. Procedures
name :: (a: int, b: float) -> ReturnType { ... }
name :: (a: int) -> named: ReturnType { ... } // named return value
name :: (a: int) -> (out: [] T) { ... } // parenthesized
name :: () -> A, B { ... } // multiple returns
name :: (x: int, y := 10) { ... } // default argument
name :: (fmt: string, args: ..Any) { ... } // variadic
Calls support named arguments: f(flavor = "chocolate", num_scoops = 3).
Quick lambdas
quick_sort(to_sort, x => x.count);
quick_sort(to_sort, (a, b) => ifx a.count != b.count then b.count-a.count else 0);
count1 :: x => x.count;
Operator overloading
operator + :: (x: Complex, y: Complex) -> Complex { ... }
operator == :: (a: T, b: T) -> bool #symmetric { ... }
operator [] :: (a: Bit_Array, index: int) -> bool { ... }
operator is a keyword; the token following it is the operator symbol, then
:: and the procedure. OPERATOR_ARRAY_SUBSCRIPT / OPERATOR_ASSIGNMENT_TO_ARRAY_SUBSCRIPT
exist as parser-internal token types for [].
Polymorphism
square :: (x: $T) -> T { return x*x; } // capture T from argument type
array_add :: (array: *[..] $T, item: T) { } // capture inside a compound type
multiplier :: ($T: Type) { } // $ = must be compile-time constant
divider :: (x: int, $$ y: int) -> int { } // $$ = optionally constant
discuss :: (x: $T/interface Matchable) { } // type restriction via interface
proc :: (x: $T/SomeStruct) { } // ⚠️ restriction by type
Holder :: struct ($T: Type, $N: s64) { } // polymorphic struct
$ on a parameter type captures it; $ on a parameter name requires the
value be compile-time constant. / after a polymorphic capture introduces a
restriction.
8. Types
[8] int // fixed-size array
[] float // array view {count, data}
[..] int // resizable/dynamic array
*T // pointer to T
[9] u8 // e.g. Phone_Number :: [9] u8;
Structs
Rectangle :: struct {
x0, y0: float;
color_name: string;
temperature := -10.0; // default value
info: Ice_Cream_Info;
info.flavor = "chocolate"; // override a sub-struct default, inside the struct body
}
Struct bodies may contain assignment statements that set nested defaults.
Layout is declaration order, always, with no compiler reordering.
Video_File :: struct {
#as using base: Document; // #as enables implicit cast to Document
// using imports Document's names
}
Enums
Fruits :: enum u32 {
BANANA :: 5; // explicit value
APPLE; // auto-increments
}
Fruits :: enum u32 #specified { BANANA :: 1; } // values locked for serialization
Flags :: enum_flags u8 { A :: 0x1; B :: 0x2; }
Unary dot: enum values can be written .APRICOT when the type is inferred.
This means a leading . followed by an identifier is a valid expression, and
must not be confused with member access. Same for .{ and .[.
Unions
union { a: int; b: float; }
9. Control flow
if cond { } else if cond { } else { }
if cond single_statement;
ifx cond then a else b // expression form
if value == { // switch form
case .BANANA; print("...");
case .APPLE; x := 1; print("..."); // each case is its own scope
case; print("default"); // valueless case = default
}
Cases do not fall through. To fall through, end the block with #through.
#complete on the if asserts all enum cases are handled.
while cond { }
break; continue;
For loops — many forms
for 1..7 { print("%", it); } // range; implicit `it`
for i: 1..7 { } // named index
for a..b { } // runtime bounds
for < b..a { } // REVERSED (note the `<`)
for array { print("%", it); } // iterate values; `it` and `it_index`
for < numbers { } // reversed iteration
for value, i: numbers { } // named value and index
for * teas { } // iterate BY POINTER (`it` is a pointer)
for outer: numbers { for inner: numbers { } } // named for nesting
for numbers if (it & 1) == 0 remove it; // `remove` current element
Implicit loop variables are it (value) and it_index (index). They are
ordinary identifiers, but worth special highlighting.
for < and for * are modifier tokens between for and the iteration
expression — a parser must accept for, then optional < and/or *.
Custom iteration (for_expansion)
for_expansion :: (holder: Holder, body: Code, flags: For_Flags) #expand {
for slot_index: 0..holder.count-1 {
`it_index := slot_index; // backtick exports the name to the caller
`it := holder.values[slot_index];
#insert body;
}
}
for :positive_vibes_only holder { } // ⚠️ select a named expansion
Backticked identifiers
`name marks an identifier that a macro exports into the caller's scope.
ident_is_backticked is a flag on the token. Only defer, return,
push_context, and overloaded operator functions may be backticked among
keywords (module.jai:1537).
defer
defer x += 1; // runs on ANY exit from the enclosing scope, incl. break
defer free(ptr);
Context
new_context := context;
new_context.allocator = temp;
push_context new_context { some_function(); }
join(strings,, allocator=temp); // ,, = inline context change for one call
10. Casting
cast(u8) b
cast,no_check(u8) c // modifier on cast, suppresses range check
xx value // auto-cast (infer target type)
xx,no_check value // ⚠️ modifier form
cast takes a parenthesized type and an optional ,modifier between the
keyword and the paren.
11. Compiler directives (#)
Directives are # immediately followed by an identifier. Full list observed in
the distribution, ordered by frequency (counts from modules/):
Very common
#foreign #c_call #type #cpp_method #if #import #char #overlay
#elsewhere #as #run #through #scope_file #load #no_context
#library #type_info_none #expand #align #asm #assert #scope_module
#scope_export #caller_location #string
Common
#define #cpp_return_type_is_non_pod #no_padding #no_aoc
#bake_arguments #insert #complete #compiler #no_abc #include
#Context #code #module_parameters #endif #add_context #modify
#location #deprecated #ifdef #no_debug #symmetric #filepath
#compile_time #intrinsic
Occasional
#type_info_procedures_are_void_pointers #program_export #placeholder
#procedure_of_call #pragma #version #this #bytes
#type_info_no_size_complaint #discard #file #else #run_and_insert
#undef #place #bake #bake_constants #no_reset #poke_name
#specified #no_alias #dump #placeholders
⚠️ Some of these (
#define,#include,#ifdef,#endif,#undef,#pragma,#elsewhere) appear predominantly inside C-binding files generated byBindings_Generator, which embed C preprocessor text. Do not assume they are core language directives.⚠️
#mustdoes not exist in this distribution — a full-tree grep of~/.local/jaireturns zero hits. Older online documentation mentions it. This is exactly the kind of drift the "don't search online" instruction guards against. Do not add it to the keyword list.
Semantics of the important ones
| Directive | Meaning |
|---|---|
#import "Basic" |
import a module; also #import,string, #import,file, #import,dir |
#load "file.jai" |
textually include another file into this scope |
#run expr |
execute at compile time |
#if cond { } / #else |
static (compile-time) conditional; can appear at top level |
#assert cond |
compile-time assertion |
#scope_file / #scope_module / #scope_export |
change visibility of everything below, until the next scope directive |
#expand |
mark a procedure as a macro |
#insert code |
splice a Code value in |
#code expr |
produce a Code value |
#through |
fall through to next case |
#complete |
require exhaustive case coverage |
#specified |
lock enum values for forward compatibility |
#as |
allow implicit cast from this struct member |
#char "a" |
character byte literal |
#string ID |
here-string |
#asm { } |
inline x86-64 assembly block |
#foreign / #library / #c_call |
FFI |
#caller_location / #location() / #file / #filepath |
source location introspection |
#no_abc / #no_aoc |
disable array-bounds-check / arithmetic-overflow-check |
#place |
overlay a struct member at another member's offset |
#module_parameters |
parameterize a module |
#modify |
compile-time hook to inspect/alter polymorph resolution |
#deprecated "msg" |
deprecation warning |
#body_text / #poke_name / #dump |
metaprogramming utilities |
#scope_file etc. are statement-position directives that affect everything
following them in the file — relevant if the plugin does symbol visibility.
12. Modules and imports
#import "Basic";
#import "Math";
Sort :: #import "Sort"; // bind a module to a name
#load "other_file.jai";
#import "Foo"(PARAM = 3); // ⚠️ module parameters
Module search path: ~/.local/jai/modules/. A module is either
Name.jai or a directory Name/module.jai. Both forms exist in the
distribution (e.g. Bit_Array.jai vs Basic/module.jai).
This is the resolution rule a "go to definition" / import-completion feature must implement.
13. Highlighting recommendations (IntelliJ token groups)
| Group | Contents |
|---|---|
| Keyword | §3 list |
| Built-in type | §3 built-in type names |
| Directive | #ident |
| Note / annotation | @Ident |
| Number | int/hex/binary/float/hexfloat, incl. _ separators |
| String | "..." |
| Here-string | #string ID ... ID (own token, own lexer state) |
| Line comment | //... |
| Block comment | /*...*/ nesting |
| Operator | §4 |
| Loop variable | it, it_index |
| Polymorph | $T, $$x |
| Backtick ident | `name |
| Uninitialized | --- |
14. Gotchas that break naive implementations
- Nested block comments. Must count depth.
- No
'c'char literal.'is not a string delimiter. Use#char "c". *is address-of (prefix) and pointer-type;.*is dereference. Inverted from C..{,.[,.IDENT— a leading.is not always member access.- Here-strings need a lexer state carrying the terminator identifier.
--vs---vs->vs-=— longest-match order matters.,,is one token, not two commas.==before{is a distinct token (switch form).- Primitive type names are not keywords — don't reserve them.
#mustis not in this version. Don't trust old online docs.fortakes modifiers (<,*) before the iterable.- Declarations have no introducer keyword.
Foo :: struct {}andfoo :: () {}andfoo :: 3;are all the same syntactic shape. removeis a keyword, valid only in loop bodies.thenis a keyword, used only byifx.- Struct bodies can contain assignments, not just declarations.
15. Primary sources to re-read
| Topic | Path (under ~/.local/jai) |
|---|---|
| Tokens, keywords, lexing | modules/Jai_Lexer/module.jai ← authoritative |
| Basics, declarations | how_to/001_first.jai, 002_number_types.jai |
| Arrays / strings | how_to/004_arrays.jai, 005_strings.jai |
| Structs / literals | how_to/006_structs.jai, 007_struct_literals.jai |
| Types as values | how_to/008_types.jai |
| Enums | how_to/013_enums.jai, 014_enum_unary_dot.jai |
| Loops | how_to/019_looping.jai, 730_for_expansions.jai |
| if / ifx / switch | how_to/022_if.jai, 025_ifx.jai, 027_if_case.jai |
| Imports / scopes | how_to/040_import_and_load/, 080_scopes.jai, 151_file_and_global_scopes/ |
| using | how_to/042_using.jai, 044_using_advanced/ |
| Operator overloading | how_to/093_operator_overloading.jai, 094_array_operators.jai |
| Polymorphism | how_to/100_, 110_, 115_, 120_, 160_type_restrictions.jai |
Context / ,, |
how_to/011_context.jai, 225_comma_comma.jai |
| Metaprogramming | how_to/450_basic_metaprogram/, 600_insert.jai, 630_compiler_get_nodes.jai |
| Inline asm | how_to/900_inline_assembly.jai |
| Compiler API | modules/Compiler/ |