Documentation pages
Type System
SultanC has one canonical semantic type system for Arabic, English and mixed source. This page describes the builtin and composite types, the conversion and arithmetic rules that belong to the type layer, the thread-capture check, and the division of responsibilities between the type layer and the rest of the compiler. It is written for programmers who need the exact rules and for contributors who touch type resolution.
Canonical identity
Surface spellings are normalized before any downstream consumer sees a type.
source spelling
|
v
token / AST type
|
v
type resolution
|
v
canonical Type ID
The English and Arabic spellings of a builtin integer resolve to the same Type ID. Backends, MSIR, safety analysis
and tooling never receive separate Arabic and English type systems. Builtin definitions are generated from the
canonical type registry, tools/generate/registry/type_registry.def, and the frontend and bootstrap mirrors are
derived from it.
Builtin types
| Family | English | Arabic |
|---|---|---|
| Any value | any |
أي |
| Boolean | bool |
منطقي |
| Text | string |
نص |
| Character | char |
حرف |
| Signed integer | int |
عدد |
| Unsigned integer | uint |
طبيعي |
| No value | void |
فراغ |
| Fixed signed | i8, i16, i32, i64 |
ص8, ص16, ص32, ص64 |
| Fixed unsigned | u8, u16, u32, u64 |
ط8, ط16, ط32, ط64 |
| Floating point | f32, f64 |
ع32, ع64 |
طبيعي is a natural number: an integer greater than or equal to zero, with zero included. The old Arabic
spellings غير_موقع and غ8…غ64 are deprecated aliases that compile as the new ones and are reported once per
use as SULTANC-SYNTAX-DEPRECATED-SPELLING. Arabic terminology lists
every renamed spelling and the reason for each.
Composite types
The semantic model represents these composite forms:
- tuples, written
(int, int); - boxes;
- option and result values;
- function types, written
fn(int): int; - pointers, and shared and mutable references (
&T,&mutable T); - slices and vectors;
- mutable wrappers (
mutable T); - named record, enum and alias types, which may take type parameters;
- named opaque types (
type T = opaque;, see Opaque types).
The identity of a named record or enum is its resolved declaration, not its spelling. Generic arguments nest, and
arc<mutex<i64>> closes both lists with one >> token, as Rust, C++11 and Java do; the parser takes it as two
closing angle brackets. A generic call infers a type parameter from the type of the value passed, so a mutable
local passed for a parameter T binds T to the value's type rather than to mutable T
(compiler/frontend/types/call_signature.sn). A generic instance whose result type is void may bind and return
that result, as in let n: R = f(); return n; with R = void: the binding holds no value.
type Pair<A, B> = { first: A, second: B };
def main(): int {
let pair: Pair<int, bool> = { first: 1, second: true };
let present: option int = option.some(3);
let nested: vector vector int = [[1], [2, 3]];
return pair.first + length(nested);
}
Option and result
option T holds a value or nothing. Its constructors are some(x) and Some(x), and none and None.
result T, E has ok / Ok and err / Err. These names are contextual: lexically they are ordinary
identifiers, and they name the builtin type only in a type or constructor position, so a local may still be called
option or result
(tools/generate/registry/type_registry.def; the constructor spellings are matched in
compiler/frontend/types/type_semantic_shape_queries.sn).
The old Arabic spellings اختيار for the type and بعض for the constructor are deprecated aliases. They name the
same type and constructor, and each use is warned once with SULTANC-SYNTAX-DEPRECATED-SPELLING.
Slices
A slice [] T is a non-owning view with the vector layout. A vector T value or place is accepted wherever [] T
is expected: as a call argument, or in a declaration or assignment of a slice. It is not accepted as a returned
value. The slice views the vector's buffer without a copy, and the vector keeps owning it
(هل_عرض_متجه_كشريحة, compiler/frontend/types/compatibility.sn). SSF treats the view as a shared borrow of the
vector's root local. A vector argument viewed as a slice is borrowed for the call, not moved, and a slice local
viewing a vector place holds a shared borrow, so the vector cannot be changed while the view is alive. MSIR retypes
the vector header with نقل_تسلسل_مملوك.
A slice declaration initialized from a slice literal (let s: [] T = [...]) or from a call result builds or
receives a vector, which an anonymous owner in the enclosing block destroys at the block's end, as Rust extends a
temporary's lifetime. The same holds for a call result passed directly as a slice argument. A slice assigned a
literal or call result later (s := [...]) keeps no owner for it, so the buffer leaks instead of letting the view
outlive it.
Vector literal expressions
A vector literal [a, b, c] is an expression. It can be a call argument, a returned value, a record field, an
element of another literal ([[1], [2, 3]]), an indexed temporary ([a, b][i]) or an operand of a reader primitive
(length([..])). let v: T = [..]; keeps the initializer statement (تهيئة_متجه), which also builds slices.
- Typing. Typing is bidirectional, as in Rust and Swift. In a
vector Econtext (a parameter, a declared or returned type, a field, an outer literal's element) each element is checked againstEand[]is avector E. Without such a context the first element givesE, so[1, 2]is avector int, and[]alone isSULTANC-TYPE-MISMATCH(ابن_نوع_حرفي_متجه_داخلي,compiler/frontend/types/semantic_pipeline.sn). In any other context the literal's own type is compared, sof([1])withf(v: [] int)is a mismatch rather than a slice view. - Ownership. The same as a
let-bound literal. Elements are evaluated left to right and moved in (SSFطبق_آثار_تعبير_لتدفق), the literal carries every borrow its elements hold (so returning[&local]is an escape), and when the literal is an argument, the borrows its elements take directly or as enum-constructor payloads last for the call and conflict with the call's other arguments (ابدأ_استعارة_حجة_مؤقتة). A borrow passed to an element's own function call ends with that call. - Lowering. The literal becomes one
تهيئة_متجهSSA value, a fresh owned temporary like a call result (هل_تعبير_طازج_للخفض,هل_أصل_مؤقت_للخفض). It is moved into an owned parameter or destination, and destroyed after an element or its length is read.tests/{ar,en}/language/surface/vector_literal_expressions.snandvector_literal_rss_flat.sncover it.
The C Stage0 seed does not parse vector literal expressions, so the compiler sources and the std modules the
compiler imports keep let-bound literals.
Opaque types
type T = opaque; (نوع T = معتم;) declares a named type with its own identity and no fields, no size and no
alignment. It stands for data that only other code (typically a C library) knows the layout of, such as
sqlite3 or sqlite3_stmt. A value of it never exists in a program; a raw pointer to it does.
public type Database = opaque;
public type Statement = opaque;
def same(a: *Database, b: *Database): bool { return a == b; }
def main(): int {
let none: *Database = 0 as *Database;
let handles: mutable vector *Database = [];
append(handles, none);
if (same(handles[0], none)) { return 0; };
return 1;
}
opaque / معتم is contextual. It declares an opaque type only as the whole right-hand side of a type declaration;
anywhere else it is an ordinary identifier, so a local, a function or even a type may still be called opaque
(هل_تعريف_معتم_حالي, compiler/frontend/parser/declarations.sn). Where a type named opaque / معتم is visible,
type X = opaque; would once have aliased it, so it is rejected (SULTANC-TYPE-INVALID-DEFINITION, reason
type.opaque-hides-visible-type) rather than silently declaring a new type; rename that type, or alias it with
alias X = opaque;. An opaque type takes no type parameters
(SULTANC-TYPE-INVALID-DEFINITION). It follows the visibility rules of any named type (public, imports), and an
alias of it (type Handle = Database;) is the same type.
Allowed:
*Tand*mutable T, and0 as *Tfor a null pointer;==and!=between pointers to the same opaque type;- such pointers as parameters, results, locals, record fields, enum payloads and vector elements;
- casts under the rules for every raw pointer (Pointer casts): to the target's address word (
u64;u32on targets with 32-bit pointers) safely, and insideunsafefrom an address word and to any other raw pointer,*voidand*mutable Tincluded.
Rejected at compile time:
| Use | Diagnostic |
|---|---|
T by value: a local, field, payload, parameter, result, global, cast target, &T, or an element of a vector, option, tuple or other container; a literal of T |
SULTANC-TYPE-OPAQUE-BY-VALUE |
a generic instance that needs the layout of T (see below) |
SULTANC-TYPE-OPAQUE-TYPE-ARGUMENT |
*p where p: *T, inside unsafe too |
SULTANC-TYPE-OPAQUE-DEREFERENCE |
p.f where p: *T |
SULTANC-TYPE-OPAQUE-FIELD-ACCESS |
p[i] where p: *T (it steps by an element size T lacks) |
SULTANC-TYPE-OPAQUE-POINTER-ARITHMETIC |
p + 1, as for every pointer |
SULTANC-TYPE-MISMATCH |
a *Statement where a *Database is expected |
SULTANC-CALL-ARGUMENT-TYPE-MISMATCH (or SULTANC-TYPE-MISMATCH) |
An opaque type may be a type argument, written (f<T>(...), G<T>) or inferred (a *A parameter given a
*T), as long as the instance uses it only through pointers: null tests, comparisons, vectors of *A, records whose
fields are *A. Each instance is decided on its own instantiated types:
- the call's instantiated signature may not pass or return
Tby value (the frontend, at the call, with the generic's name as related context); - a generic record or enum instance may hold
Tonly behind pointers in its members; - the instantiated body, as monomorphization specializes it (
أول_استعمال_معتم_في_مثيل,compiler/msir/lowering/monomorphization.sn), may not useTby value: no dereference or copy, no&Tforsize_of/align_of, no local, vector or other container ofTto build or destroy.
A violation is SULTANC-TYPE-OPAQUE-TYPE-ARGUMENT at the call that started the instantiation, even several generic
calls deep, with the use inside the generic as related context.
The canonical model is تعريف_نوع_مسمى.معتم with the named shape شكل_نوع_مسمى.معتم
(compiler/frontend/types/type_model.sn); representation consumers see صنف_شكل_نوع_دلالي.معتم. The rule that
an opaque type appears only directly behind * is استعمال_معتم_ممنوع_في_نوع
(compiler/frontend/types/type_registry.sn). Layout gives the opaque type itself no value representation; *T is an
ordinary pointer of the target's width, which the Native backends, LLVM, MSIR and the interpreter treat as *void.
Debug information describes *T as a pointer to an unspecified type (Native DWARF) or a void * (LLVM).
Compile-time evaluation makes no raw pointers, opaque or not (SULTANC-COMPTIME-PROHIBITED-EFFECT).
Tests: tests/{ar,en}/language/opaque/ and tests/{ar,en}/target/opaque_pointers_32.sn.
Numeric casts
x as T converts between scalar types. The type checker accepts a cast between two scalar types only
when a conversion joins them (تحويل_سلمي_مرفوض, compiler/frontend/types/compatibility.sn). Any other pair, such
as bool as int, is SULTANC-TYPE-MISMATCH at the cast, with the reason type.invalid-cast. Casts that involve
references, pointers or byte vectors are checked where they are lowered. A raw pointer converts to and from its
address word (u64, i64, int, uint, and u32 / i32 only on a target whose pointers are 32 bits wide) and to
any other raw pointer (p as *void, p as *mutable T), as one MSIR address-word operation (مرجع_إلى_مؤشر).
Pointer casts
| Cast | Outside unsafe |
|---|---|
the same type; a reference to a raw pointer; *mutable T to *T (dropping mutable) |
safe |
a raw pointer to its address word (p as u64; p as u32 on 32-bit targets) |
safe |
the integer literal 0 written directly before as (0 as *T, the null pointer) |
safe |
an address word to a raw pointer (w as *T), including (0) as *T and a named zero |
SULTANC-SAFETY-UNSAFE-OPERATION, reason safety.integer-to-raw-pointer |
a raw pointer to another pointee (*A as *B), to or from *void |
SULTANC-SAFETY-UNSAFE-OPERATION, reason safety.raw-pointer-other-pointee |
*T to *mutable T (adding mutable) |
SULTANC-SAFETY-UNSAFE-OPERATION, reason safety.raw-pointer-adds-mutable |
a raw pointer to a reference (p as &T) |
SULTANC-SAFETY-UNSAFE-OPERATION |
Dropping mutable is also implicit: a *mutable T is accepted wherever a *T of the same T is expected (an
argument, a typed let, a return, a record field), so a read-only parameter takes a writable handle without a cast.
The reverse (*T where *mutable T is expected) and any change of pointee stay type mismatches. Lowering emits the
same address-word reinterpretation as the explicit cast.
The classification is صنف_تحويل_بين (compiler/frontend/types/compatibility.sn); the unsafe gate and the null
exemption are in compiler/frontend/types/unsafe_operations.sn. Only the bare literal is exempt: parentheses leave
no syntax node, so the gate reads the byte after the literal, and (0) as *T needs unsafe like a named constant. The
gate generates no code; pointer widths and the C ABI are unchanged.
| From | To | Result |
|---|---|---|
| integer | integer | two's-complement truncation or extension; wraps and never traps |
u8 |
char |
the code point |
char |
an unsigned integer of 32 bits or more | the code point |
| float | integer | truncated toward zero, saturated at the type's bounds; NaN gives 0 |
| integer | float | the nearest float, ties to even |
f32 |
f64 |
exact |
f64 |
f32 |
the nearest f32, ties to even; out of range gives an infinity; a NaN stays a NaN |
The float conversions follow Rust's as (saturating float-to-integer, defined by rust-lang/rust#71269), which
is also the behavior of Java's d2l and WebAssembly's non-trapping iNN.trunc_sat_fMM. Go and C leave an
out-of-range float-to-integer result implementation-defined or undefined; SultanC does not. Integer-to-float
rounding is IEEE 754 roundTiesToEven, as in Rust, Go, Swift and C.
def main(): int {
let small: u8 = 200;
let wide: u32 = (small as u32) + 100;
let wrapped: u8 = (wide as u8); // 300 wraps to 44
let ratio: f64 = 7.9;
return (ratio as int) + (wrapped as int);
}
All float conversions are one MSIR operation, تحويل_عائم (compiler/msir/msir_model.sn), which carries the
integer side's signedness. Each backend lowers it as follows:
- x86_64 Native uses
CVTTSD2SI/CVTTSS2SIafter comparing with the bounds as floats (exact powers of two), so the instruction only sees in-range values. Au64above 2^63 subtracts 2^63 and sets bit 63. The reverse direction usesCVTSI2SD/CVTSI2SS, with the sticky-halving sequence for au64above 2^63, andCVTSS2SD/CVTSD2SSbetween float widths. - arm64 Native uses
FCVTZS/FCVTZU, which already saturate and map NaN to 0, at 32 and 64 bits. Results of 8 and 16 bits are clamped first withFMIN/FMAX, which keep a NaN. It usesSCVTF/UCVTFandFCVTfor the reverse and between widths. - wasm32 Native uses
iNN.trunc_sat_fMM_s/u,fMM.convert_iNN_s/u,f32.demote_f64andf64.promote_f32, with the samemin/maxclamp for 8 and 16 bits. - LLVM uses
llvm.fptosi.sat/llvm.fptoui.sat,sitofp/uitofp, andfpext/fptrunc. Targets with 32-bit registers would legalize 64-bit conversions into compiler-rt calls (__aeabi_d2lz,__fixdfdi), which no SultanC link provides, socompiler/backend/provider/llvm/wide_conversion.snbuilds them from 32-bit conversions and exact float steps, aswide_division.sndoes for 64-bit division. AVR and MSP430 have no FPU, so their objects call the usual soft-float routines for every float operation, conversions included, and the toolchain that links them supplies those. - The interpreter and compile-time evaluation use IEEE 754 software arithmetic
(
compiler/interpreter/operations/float_software.sn). A module constant such aslet k: int = 2.5 as int;is folded with the same code, and a float result is kept as its exact decimal text (نص_عشري_دقيق_من_بتات,compiler/msir/decimal_float_bits.sn).
The bit patterns of floats are available as f64_to_bits, f64_from_bits and the
f32 forms in std.math.float, over the compiler's bit-reinterpret intrinsics.
Wrapping arithmetic
+, -, * and << trap on overflow. Wrapping arithmetic, the result modulo 2^N, is provided by four MSIR
operations (جمع_ملتف, طرح_ملتف, ضرب_ملتف, إزاحة_يسرى_ملتفة) that lower to the plain machine instruction on
every backend. They are reached through the compiler intrinsics بدائي_جمع_ملتف, بدائي_طرح_ملتف,
بدائي_ضرب_ملتف, بدائي_إزاحة_يسرى_ملتفة and بدائي_إزاحة_يمنى_ملتفة, whose two operands share one integer
type. A shift takes its amount modulo the width, as Rust's wrapping_shl does.
std.math.wrapping gives every integer type a wrapping form wrapping_add_T (and sub, mul,
shl, shr), a checked form checked_add_T that returns an option, and a saturating form
saturating_add_T, after Rust's wrapping_*, checked_* and saturating_*.
Float operations
Beyond + - * / and the comparisons, the IEEE 754 operations that Rust exposes as f64 methods are compiler
primitives. Each is one هوية_بدائي_مترجم.عملية_عائمة carrying a عملية_عائمة_بدائية, and its operands and result
have the first operand's float type, f32 or f64.
| Operation | Intrinsic | std.math.float | Result |
|---|---|---|---|
| square root | بدائي_جذر_تربيعي_عائم |
sqrt_f64 |
correctly rounded (IEEE squareRoot); -0 gives -0, a negative operand NaN |
| absolute value | بدائي_قيمة_مطلقة_عائمة |
abs_f64 |
clears the sign bit, also of a NaN |
| floor, ceil, trunc | بدائي_أرضية_عائمة, بدائي_سقف_عائم, بدائي_بتر_عائم |
floor_f64, ceil_f64, trunc_f64 |
exact; a zero result keeps the operand's sign |
| round to nearest, ties to even | بدائي_تقريب_زوجي_عائم |
round_ties_even_f64 |
exact (roundToIntegralTiesToEven) |
| minimum, maximum | بدائي_أدنى_عائم, بدائي_أقصى_عائم |
min_f64, max_f64 |
IEEE 754-2019 minimum and maximum: a NaN operand gives a NaN, and -0 < +0 |
| copysign | بدائي_نسخ_إشارة_عائمة |
copysign_f64 |
the first operand's magnitude with the second's sign |
| fused multiply-add | بدائي_ضرب_جمع_مدمج_عائم |
mul_add_f64 |
a * b + c rounded once |
The f32 forms exist as well. Every backend produces the same bits: the native backends and LLVM lower each
operation to the target's instruction (see MSIR and code generation), and the
interpreter computes it exactly on the bits. Where a target's baseline instruction set has no integral rounding
(baseline x86-64 has no ROUNDSD) or no fused multiply-add, std.math selects an exact bit-level form by architecture
instead of the primitive, as Rust's libm does, so no libm call is ever needed. The payload and sign of a NaN result
are not specified, as in Rust; abs and copysign only move the sign bit.
Negation and constant arithmetic
Unary minus applies to signed integers and floats. On a float it is the IEEE negation, -0.0 - x, so -x of
+0.0 is -0.0. On a value of an unsigned type (uint, u8…u64) it is SULTANC-TYPE-MISMATCH with the reason
type.unsigned-negation, as Rust's E0600, in a function and in a module constant alike. An integer literal keeps
its own value check, so -0 fits an unsigned type and -1 is out of range. Subtract, or convert to a signed type
first.
A module constant is evaluated at compile time with the run-time semantics. An operation that would trap at run
time (integer overflow, division or remainder by zero, the signed minimum divided by -1, a shift by the width or
more) is SULTANC-COMPTIME-EVALUATION-FAILED with the reason comptime.execution-trapped, as Rust's E0080 is for a
const. A constant whose declared type is wider than its expression's, such as
let k: int = (250 as u8) + 3;, is a widening the type checker accepts, and the constant holds the same number in
the declared type.
Thread capture rules
A closure handed to another thread may capture only values that are safe to send there, the role Rust's
F: Send + 'static bound plays on std::thread::spawn. SultanC has no traits, so the frontend checks the closure
literal at the call (compiler/frontend/types/thread_capture.sn, run after the unsafe check and before SSF).
| Captured | Verdict | Diagnostic |
|---|---|---|
a value whose type holds a raw pointer (*T, *mutable T, anywhere inside records, enums, tuples, options, results, vectors or boxes) |
rejected, unless the call that hands the closure over sits inside unsafe (the programmer vouches for the pointee's synchronization, as with unsafe impl Send) |
SULTANC-THREAD-CAPTURE-RAW-POINTER |
a value whose type holds a borrowed reference or a slice (&T, &mutable T, [] T) |
rejected: the thread may outlive the borrowed storage ('static) |
SULTANC-THREAD-CAPTURE-REFERENCE |
| a writable module variable named in the closure body, outside an unsafe block of that body | rejected: every thread shares it and its reads and writes are not atomic (Rust's static mut) |
SULTANC-THREAD-CAPTURE-NON-ATOMIC-SHARED |
plain values (numbers, records of them), owned values (moved into the closure), function values and the std.sync handles built on them (arc, Rust's Arc, whose count is atomic), type parameters |
accepted | none |
Each diagnostic names the captured symbol and its type at its first use in the closure, in Arabic and English (iSpeak).
Which calls send a closure is derived, not listed. The function-typed parameters of a function that calls a
closure-address intrinsic (بدائي_عنوان_دالة, بدائي_مدخل_سي_لدالة, which is how std.thread hands a closure to a
new thread on every OS) are sending, and a parameter a function forwards to a sending parameter is sending too,
to a fixpoint over all units. When no unit names those intrinsics, as in the compiler itself and most programs, the
pass does nothing. See Concurrency for the user-facing side.
Responsibilities of the type layer
The type layer owns canonical type identity, builtin metadata, type construction and interning, named-type resolution, equality and compatibility, conversion classification, scalar and signedness queries, integer-literal fit checks, and the semantic type shape exposed to downstream consumers. Consumers ask these APIs for type facts instead of reconstructing them from AST syntax or source spelling.
Type identity is distinct from physical representation and machine calling convention. The type system does not decide source tokenization or grammar, module and name lookup, borrow and lifetime legality, target selection, physical field offsets or alignment, ABI argument and return locations, register allocation, object format or instruction encoding. Those facts belong to the frontend, layout, target, ABI and backend layers.
After type resolution, target-aware layers query the type shape and combine it with the selected target's data model:
semantic Type ID
|
+--> safety / semantic queries
|
+--> layout + target data model
|
+--> ABI
+--> native code generation
The meaning of a type stays target-neutral while its physical representation varies by target where required.
Explicit type arguments
f<A, B>(..) binds the callee's declared type parameters, in order, before inference from the arguments and the
expected result: empty<u8>() (a parameter only the result uses), same<u8>(30) (the argument is typed as u8).
After a callee name or qualified name, < starts type arguments only when a type list, > and ( follow;
otherwise it is the comparison. A wrong count is SULTANC-CALL-ARGUMENT-COUNT-MISMATCH. The parser wraps the callee
(تعبير.حجج_نوع, with the callee's own span) and the type layer seeds the call's substitutions
(استبدالات_حجج_نوع_صريحة_داخلي). tests/*/language/surface/explicit_type_arguments.sn covers it.
Embedded files
embed_file("path") (ضمن_ملف(«path»)) is the file's bytes as a string literal, read when the source is parsed;
a relative path is resolved from the source file's directory. An unreadable file is SULTANC-PARSE-SYNTAX
(parser.embed-file-unreadable). tests/*/language/surface/embed_file.sn covers it.
Limitations
- A slice assigned a literal or call result after its declaration (
s := [...]) owns nothing, so the buffer leaks. []with no element and no vector context is a type mismatch.- A closure passed through a variable instead of a literal is not inspected by the thread-capture check, a captured
closure is opaque (its own captures are not walked), and a writable module variable reached through a call inside
the closure is not seen. Rust tracks all three through auto traits and
staticrules. - A type whose physical layout is intentionally not fixed (text, vectors, slices, function values) has no size or alignment; asking for one at compile time is a diagnostic (Compile-time evaluation).