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.

Void is the zero-size type for functions with no meaningful returned data. Its only value is ().

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

Always use these surface forms 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

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

Ordinary variable declarations do not accept ref on the left. Create reference values explicitly on the right:

Int value = 1;
Int& borrowed = value$.ref();
Int copied = borrowed$.get();

Int mutable_value! = 2;
Int&! mutable_ref = mutable_value$.mut_ref();

Inside destructuring and patterns, ref versus ref! selects shared versus unique mutable borrow;

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

The type position after ref is optional: ref name means ref _ name, and ref! name means ref! _ name. This applies to destructuring, payload patterns, and lambda captures:

(ref left, _ right!) = pair;

if maybe is .some(ref item) {
    use(item);
}

Type patterns may infer only part of a type, as in Int[_] values or _[3] values.

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 { ... } to handle it.