Type System Basics
Jiang types are read left to right, from the inner value outward. Each suffix wraps the type before it.
Primitive Types
Section titled “Primitive Types”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[]&);Type Suffixes
Section titled “Type Suffixes”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 syntax | Meaning |
|---|---|
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.
Type Inference
Section titled “Type Inference”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 pattern | Meaning |
|---|---|
_ name | bind a new immutable value with the RHS type |
_ name! | bind a new mutable value with the RHS type |
ref _ name | borrow the RHS place and bind an immutable inferred reference |
ref! _ name | unique 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 IntUse 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);}Binding Mutability
Section titled “Binding Mutability”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 Boolflag = false;
Int[3] values! = [1, 2, 3]; // writable array bindingvalues[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 fieldArrays and Slices
Section titled “Arrays and Slices”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.
Pointers and References
Section titled “Pointers and References”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;Optional
Section titled “Optional”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;}Errorable Returns
Section titled “Errorable Returns”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.