Skip to content

Type System Basics

Jiang types are read left to right, from the inner value outward. Each suffix wraps the type before it.

Bool flag = true;
Int count = -123;
UInt size = 123;
UInt8 byte = 255;
Int16 small = -45;
Float value = 12.3;
Double precise = 132.54;
Char ch = 'a';

Int and UInt are pointer-sized integers. Use fixed-width types such as Int32 or UInt64 when ABI, file, or protocol layout needs an exact width.

Enum discriminants are stored as Int32 by default unless the enum declares another integer representation.

() is the Unit type. It is a zero-size value and is also used for functions with no meaningful returned data.

String literals are UTF-8 byte sequences. Their default borrowed view type is UInt8[:0]&, so the compiler can expose null-terminated data for C-style boundaries while keeping the slice length separate from the sentinel byte:

UInt8[_] bytes = "jiang";
UInt8[:0]& view = "jiang";

String literals can still be assigned directly where the expected type is a plain UInt8[]&. For non-literal sentinel slices, convert explicitly with slice() or a cast to UInt8[]&:

UInt8[:0]& c_view = "jiang";
UInt8[]& plain = c_view.slice();
UInt8[]& forced = c_view$.as(UInt8[]&);

Suffixes farther to the right wrap a wider layer:

Int[2][3] matrix;
Int?[2][3] nullable_items;
Int?[2]?[3] nullable_rows;

Common suffix forms:

Surface syntaxMeaning
T?optional value
T^owning pointer
T& / T&!shared reference / unique mutable reference
T* / T*!readonly / writable raw pointer
T[N]fixed-size array
T[N:S]fixed-size array with sentinel value S
T[]unsized array type
T[]&borrowed slice
T[]^owning slice
T[:S]&borrowed sentinel slice reference

Compiler-owned nominal names behind these forms are not available to user source. Always use the surface syntax in declarations, extend targets, and @where type patterns. A user-defined type named Option, Result, Ref, Box, or Slice is an ordinary nominal type and receives no built-in behavior.

Type-level ! is deliberately narrow: it is valid only in T&! and T*!. Forms such as Int!, T^!, T[]!, T?!, and T!& are invalid. Mutability for a variable, parameter, global, or field is written after its binding name instead.

Use _ as a placeholder for the right-hand side type. The compiler infers the initializer’s natural type; add ! after the new name when that binding must be reassignable:

_ answer = 42;
_ name = "Jiang";
_ values = [1, 2, 3];

The declaration forms below keep type inference and binding mutability separate:

Binding patternMeaning
_ namebind a new immutable value with the RHS type
_ name!bind a new mutable value with the RHS type
ref _ nameborrow the RHS place and bind an immutable inferred reference
ref! _ nameunique mutable borrow with an immutable reference binding
ref _ name!shared borrow with a rebindable reference binding
ref! _ name!unique mutable borrow with a rebindable reference binding

Put ! after the name when the new binding itself must be reassignable:

_ count! = 0;
count = count + 1;

Inference preserves the expression’s natural ownership/reference shape:

Int^ make_value();
_ owner = make_value(); // owner: Int^
Int copied = make_value()$.get();

Binding mutability is not part of the value type, so copying from a mutable binding does not make the new binding mutable:

Int source! = 1;
_ copied = source; // immutable binding, value type Int
_ mutable! = source; // mutable binding, value type Int

Use ref _ to infer a borrowed reference binding from a place expression:

Int value = 1;
ref _ borrowed = value; // borrowed: Int&
Int copied = borrowed$.get();
Int mutable_value! = 2;
ref! _ mutable_ref = mutable_value; // mutable_ref: Int&!

ref _ is a binding mode. It actively borrows the right-hand side place and infers the referenced type. The source must be a place that can be borrowed; if the expression is already a reference, use _ instead of ref _:

Int& existing = value$.ref();
_ same_ref = existing; // same_ref: Int&
// ref _ nested = existing; // error: `ref _` does not borrow an existing reference again.

Inside destructuring and patterns, ref versus ref! selects shared versus unique mutable borrow; the ! after the name only makes the new binding reassignable:

Int first! = 1;
Int second! = 2;
(ref! _ current) = (first);
(ref! _ rebindable!) = (second);

The same binding forms are used inside destructuring. Use ref T name when borrowing a payload pattern:

(ref _ left, _ right!) = pair;
if maybe is .some(ref Int item) {
use(item);
}

Put ! after a binding name to allow assignments to that storage location. This is metadata about the binding, not part of the stored type:

Bool flag! = true; // mutable binding containing Bool
flag = false;
Int[3] values! = [1, 2, 3]; // writable array binding
values[0] = 10;
values = [4, 5, 6];

Binding mutability does not enter a function signature. foo(Int value!) and foo(Int value) have the same parameter type; the first form merely lets the function body reassign its local parameter. By contrast, foo(Int&! value) requires a unique mutable reference, so that capability is part of the signature.

There is no unique modifier. unique is an ordinary identifier; T&! alone expresses the unique mutable capability.

For aggregate values, a member is writable only when both the outer access place and the member declaration provide write capability. A readonly outer view recursively freezes reachable fields.

struct User {
Int id;
Int age!;
}
User user! = User(id: 1, age: 18)
user.age = 19;
// user.id = 2; // error
User& readonly = user$.ref();
// readonly.age = 20; // error: the shared outer borrow freezes the field

Array length is part of the type:

Int[3] values = [1, 2, 3];
Int[_] inferred = [1, 2, 3];

Nested arrays use repeated suffixes:

Int[2][3] matrix = [[1, 2], [3, 4], [5, 6]];

T[] is an unsized array type whose length is known at runtime. Bare T[] is not a normal by-value type. Use T[]& for a borrowed slice reference:

Int[_] values = [1, 2, 3];
Int[]& view = values[..];

T[]^ is the owned handle for an unsized array. It owns the initialized buffer, drops the elements, and frees the allocation.

T[:S] is a sentinel unsized array type. Sentinel slice views such as T[:S]& carry an additional type-level sentinel guarantee. A non-literal sentinel slice does not implicitly lose that guarantee when assigned to T[]&; write value.slice() or value$.as(T[]&) to request a plain slice view.

Int^ owner = new Int(42);
Int value = owner$.get();
unsafe {
owner$.dealloc();
}

T^ can auto-dereference when the expected type is T; use $.get() for an explicit pointee read. T& and T* require explicit reads. Member access can pass through T^:

Int value = 41;
Int& ref = value$.ref();
Int copied = ref$.get();
struct Box {
Int value;
}
Box^ box = new Box(value: 42);
Int n = box.value;

T? means the value may be null:

Int? maybe = 42;
Int? none = null;

Use optional chaining, ??, or .some(...):

_ field = user?.name;
Int value = maybe ?? 0;
if maybe is .some(payload) {
return payload;
} else {
return 0;
}

T@E is used in function and method return position for errorable results:

enum Err {
bad = 1,
}
Int@Err ok() {
return 42;
}
Int main() {
return try ok() catch () => 0;
}

Use throw expr; to return the error side and try expr catch (...) => fallback to handle it.