Types
This document specifies the Silk type system used by the compiler front-end and type checker.
- Supported end-to-end: primitives, nominal
structtypes, optionals (T?),&Structreferences (in function parameter types and as local values produced bynew/ calls that return&Struct), and array/slice types (T[N],T[]) over element types that lower to a fixed scalar-slot sequence In Silk (including array literals, indexing reads, and iterableforloops). Indexed assignment targets (xs[i] = v) are supported for these element types; compound index ops require numeric scalar element types in the Supported forms. - Parameterized nominal types (monomorphized): generic
structandinterfacedeclarations with type parameters, plus applied types in type positions (Name(u8),Name(string)) for those declarations. - Reserved intrinsics: the compiler currently exposes reserved, stdlib
bring-up intrinsics for working with the
stringABI (__silk_string_ptr,__silk_string_len, and__silk_string_from_ptr_len). User code should generally prefer the language sugar: s as raw u64(extract the underlying byte pointer), andsizeof(s)(extract the string byte length asusize), andp as raw u64(extract the underlying address forp: &T), andptr as string(len)(construct astringview from a raw pointer plus an explicit byte length) over calling these helpers directly. The intrinsic names remain reserved and are not a stable user API.- Special-case: the nominal optional form
Option(T)is accepted and desugared toT?in type annotations (it is not a general generics feature). - Parsed but rejected by the current checker: const parameters and integer
literal type arguments (
Foo(N: int),Foo(u8, 1024)) (diagnostics,E2016). - Removed builtin map type form:
map(K, V)(E2017; usestd::map::{HashMap, TreeMap}instead). - Defined in the native backend subset: 128-bit scalar primitives
(
i128,u128,f128). - In the current scalar-slot model (structs impls layout),
these primitives lower to two 8-byte slots (
lo: u64,hi: u64). f128uses the IEEE‑754 binary128 bit pattern stored across those slots.- In the current backend implementation,
f128arithmetic and someascasts lower to bundled runtime helper calls: - on
linux/x86_64, the helpers use__float128and rely on libgcc symbols (for example__addtf3), - on targets where
long doubleis an IEEE‑754 binary128 value (for examplelinux/aarch64), the helpers uselong double, - on other targets, the helper calls are currently stubbed and will trap if executed.
- Diagnostics rule:
u128/f128are language features, so any compiler rejection is an implementation gap in a specific compiler path, not a language-contract rejection. - Typed errors (
error,panic, andT | ErrorType...) are specified in typed errors. The current compiler models typed error contracts as an effect on function return types and expressions. - Separately, type unions (
T1 | T2 | ...) are supported in type annotations as described in type unions. In function declaration return types, union returns must be parenthesized (-> (A | B)) because unparenthesized|after->is reserved for typed-error contracts.
Quick Reference#
The core categories are:
- Booleans:
bool - Examples:
true,false. - Notes: logical values.
- Integers (fixed width):
u8,i8,u16,i16,u32,i32,u64,i64,u128,i128 - Examples:
let n: i32 = 42;. - Notes: signed/unsigned bit-widths.
- Integer (platform):
int - Examples:
let n: int = 1;. - Notes: implementation/default integer.
- Pointer-width integers:
usize,size - Examples:
let n: usize = 1;,let n: size = -1;. - Notes: unsigned/signed integer types whose width matches the target
architecture pointer width (for example 64-bit on
linux/x86_64). - Compatibility:
isizeis accepted as an alias forsize. - Floats:
f32,f64,f128 - Examples:
let x: f64 = 3.14;. - Notes: IEEE‑754.
- Char:
char - Examples:
'A'. - Notes: Unicode scalar.
- String:
string - Examples:
"hello". - Notes: immutable text; multi‑line strings supported.
- Regexp:
regexp - Examples:
/hello/i. - Notes: compiled regular expression bytecode; a non-owning
{ ptr, len }view analogous tostring. Regex literals compile at compile time; runtime compilation and matching helpers live instd::regex(see regex). - Region handle:
Region - Examples:
fn f (r: Region) -> int { with r { ... } }. - Notes: a first-class region allocation context handle; see regions.
- Void / Unit:
void - Examples:
fn foo () -> void {}. - Notes: functions that return nothing.
- Time Types:
Instant,Duration - Examples:
let i: Instant = std::now();. - Notes: specialized
i64-based types for time measurement. - Optional:
T? - Examples:
User?,i32?. - Notes:
None/noneis the canonical empty value;nullmay also be used when an optional type is expected and coerces toNone. Usematch,?.,??. - None (value):
- Examples:
None/none(represented asNonein code samples). - Notes: the distinguished empty value; typed as
T?. Thenullliteral is a distinct literal that can coerce toNonewhen an optional type is expected (see optional). - Reference (borrow):
&T - Examples:
&User. - Notes: reference type; in the Supported forms,
&Structmay appear in parameter types and as local values when produced bynewor by calls that return&Struct. Mutability follows themutborrow contract and per-call aliasing rules described in mutability. - Arrays / Slices:
T[],T[N] - Examples:
i32[],byte[32],u8[1024 * 1024]. - Notes: dynamic slice vs fixed length (compile‑time
N). In the current parser subset,Nmay be: - an integer literal,
- a literal-only integer arithmetic expression using
+,-,*,/,%, and parentheses, - or a single const parameter name in generic type positions.
In Silk, arrays/slices are supported only
when the element type lowers to a fixed scalar slot sequence in the current
scalar-slot memory model (for example primitive scalars,
string, and supportedregexp, supported non-opaque structs, and enums). See structs impls layout for the current scalar-slot memory model. In the Supported forms, fixed array lengths are limited toN <= 4096. Indexingxs[i]traps wheniis out of bounds in the Supported forms. - Range:
range - Examples:
let r: range = 0..4;,let r2: range = (1..) + 2;. - Notes: an
int-indexed range value used for slicing and other index-based operations. Range literal bounds areintvalues; in range literals, bound expressions must have typeint(integer literals may be coerced toint). Range literal expressions support: start..end(end-exclusive) andstart..=end(end-inclusive),start..(open-ended),..end(implicit start0),..(full range). The inclusive (..=) form requires an explicit end bound. In index/slice contexts, open-ended ranges use the container’s length as the effective end bound. Arangevalue can be used for slicing via&xs[r]/mut &xs[r], enabling composed ranges (including inclusive ranges) to drive slicing.- Function Types:
fn(params) -> R - Examples:
fn(i32) -> i32. - Notes: function types are part of the type grammar and function-typed
values are supported as function values (including capturing closures) in
Silk currently.
Concurrency disciplines (
task/async) are implemented on function declarations (see concurrency); function types in type positions do not currently include discipline modifiers. - Capturing Closures:
- Notes: capturing closures are supported as function values with an environment; see “Function Types and Closures” below for Supported forms restrictions.
- Structs (nominal):
- Surface:
struct Name { ... }thenName(...). - Examples:
Point,Option(T). - Notes: user‑defined records; may be parameterized.
- Enums (sum types):
- Surface:
enum Name { ... }. - Notes: nominal tagged unions as described in the spec.
- Type unions:
- Surface:
T1 | T2 | ...(type annotations). - Notes: a tagged “one-of-these-types” type for a small, explicitly defined subset; see type unions.
The compiler must represent these types faithfully in its internal type system and in the C99 ABI mappings, and it must follow the exact surface syntaxes indicated above when parsing and printing types.
Type Aliases (type)#
Silk supports compile-time-only type aliases via type declarations.
Syntax examples:
type Int32 = i32;
type struct Bar = Foo;
type fn IntAdder = fn(int, int) -> int;
type pure fn PureIntAdder = fn(int, int) -> int;
type ResultOf(T) = std::result::Result(T, string);
export type struct PublicBar = Foo;
Semantics :
- A type alias introduces a new name for an existing type; it does not introduce a distinct nominal type.
- The type checker MUST treat uses of the alias name as equivalent to the alias target type (the alias is transparent).
- A type alias may declare generic parameters using the same parameter-list
syntax as generic structs, enums, interfaces, impls, and functions. Applying
the alias in a type position substitutes the supplied type arguments into the
transparent target, for example
ResultOf(int)is equivalent tostd::result::Result(int, string). - Type aliases may be used anywhere a type is expected (parameter/result types,
local annotations, struct fields,
ascasts, etc.). - Cycles in type aliases are rejected (
E2058).
Kind tags:
- A
typedeclaration may optionally specify a kind tag, for exampletype struct Name = Foo;ortype pure fn Name = fn(...) -> ...;. - When present, the compiler MUST validate that the resolved alias target
matches the declared kind (
E2059).
Import/export:
typealiases may be exported (export type ...;) and imported as type names via named file imports (see packages imports exports).
Implicit Call-Argument Coercions#
In Silk currently, Silk supports a small, opt-in implicit coercion mechanism for function call arguments. This exists to keep the current standard library ergonomic while generics and richer overload systems are still evolving.
There are three related mechanisms:
- Coercion to a nominal
structvalueT(by-value parameters and varargs elements) via exported static ctor-like methods. - Coercion to a borrowed reference
&T(read-only&Tparameters) via aconstructormethod that initializes a compiler-generated stack temporary. - Contextual string compatibility for values implementing
std::interfaces::Serialize(string).
1) Coercion to T via exported static ctor-like methods#
Rule (informal):
- When a function call argument type does not match a parameter type, and the
parameter is a nominal
structtypeT, the compiler may rewrite the argument to a call of an exported, static ctor-like method onT. - This also applies to varargs elements (
...args: T).
Supported ctor-like method names (destination type opts in by defining these):
T.int(value: int) -> TT.i128(value: i128) -> TT.u64(value: u64) -> TT.u128(value: u128) -> TT.f64(value: f64) -> TT.f128(value: f128) -> TT.bool(value: bool) -> TT.char(value: char) -> TT.string(value: string) -> TT.regexp(value: regexp) -> TT.Region(value: Region) -> T
Selection (source type → constructor):
- Signed integer primitives (
i8/i16/i32/i64/int/size/isize/Instant/Duration) →int - Signed wide integer primitive (
i128) →i128 - Unsigned integer primitives (
u8/u16/u32/u64/usize) →u64 - Unsigned wide integer primitive (
u128) →u128 - Float primitives (
f32/f64) →f64 - Wide float primitive (
f128) →f128 bool→boolchar→charstring→stringregexp→regexpRegion→Region
Integer width:
- When the source argument is a fixed-width integer, the compiler inserts an implicit integer cast to match the ctor parameter type before calling the ctor.
- When the source argument is
f32and the selected ctor parameter type isf64, the compiler inserts an implicit float cast (f32 -> f64) before calling the ctor.
Example:
struct Counter {
value: int,
}
impl Counter {
public fn int (value: int) -> Counter {
return Counter{ value: value };
}
}
fn takes (c: Counter) -> int {
return c.value;
}
fn main () -> int {
let x: i32 = 7;
return takes(x); // coerces via `Counter.int`
}
Notes:
- Coercions are only attempted when the destination type provides the matching exported static ctor method.
- Today this primarily exists to support
std::fmt::Argand ergonomicstd::io::print/printlncalls without requiring explicitArg.*wrappers everywhere.
2) Coercion to &T via constructor (stack temporary)#
Rule (informal):
- When a call argument does not match a parameter type, and the parameter is a
read-only borrowed reference
&Tto a nominalstructtypeT, the compiler may create an implicit stack temporarytmp: T, initialize it by invokingtmp.constructor(...), and pass&tmpto the callee.
This is intentionally a stack construction mechanism:
- it does not allocate on the heap,
- it is compatible with
silk build --noheap, - and the temporary’s lifetime is the duration of the call (similar to how C++
binds temporaries to
const¶meters).
Eligibility requirements (Supported forms):
- The parameter must be
&T(notmut &T). - The destination type
Tmust provide a visibleconstructoroverload with: - receiver
mut self: &T, - exactly one non-receiver parameter (
value: U), - return type
void. - The call argument type must match the selected overload’s
Uparameter type. - If multiple overloads are viable for a given argument, the coercion is ambiguous and rejected (the call must be written with an explicit construction).
Example:
struct User {
name: string,
}
impl User {
fn constructor (mut self: &Self, name: string) -> void {
self.name = name;
}
}
fn print_user (user: &User) -> void {
std::io::println("user.name = {}", user.name);
}
fn main () -> int {
// Implicitly constructs a temporary `User` from a `string` for this call.
print_user("alice");
return 0;
}
Notes:
- This is an opt-in mechanism: types must provide the matching
constructor. - If the coercion is ambiguous (multiple viable conversion paths), the compiler rejects the call and requires an explicit construction.
- Because this coercion participates in ordinary call argument checking, it can
make additional overloads applicable (for example a copy-constructor
constructor(mut self: &Self, other: &Self)can accept astringargument by first constructing a temporarySelffromstring). Overload resolution prefers overloads that match without requiring such coercions.
3) Contextual std::strings::String compatibility for plain string#
Rule (informal):
- When an expression is checked in a context that explicitly expects
string, and the expression’s type isstd::strings::String, the compiler may accept that expression as satisfying the expectedstring.
Supported contexts include:
let x: string = exprx = exprwhenx: string- ordinary function arguments where the parameter type is
string
Notes:
- This is contextual; it does not change the expression’s nominal type outside
a
string-expecting site. - This rule is currently specialized to
std::strings::String. String.serialize()/String.as_string()still describe the borrowed view that is being exposed, so explicit.as_string()is no longer required solely to satisfy these ordinary borrowedstringcontexts.- Returning a borrowed
stringview derived from a local ownedStringremains a lifetime-sensitive case and is rejected by the compiler; use.as_string()explicitly when you need to spell that borrow locally, or return an ownedstd::strings::Stringinstead.
Explicit Casts (as)#
Silk supports explicit casts using the postfix as operator:
let x: f64 = 3.14;
let n: int = x as int;
This operator is intended for explicit, potentially lossy primitive numeric
conversions. In the Supported forms it also supports explicit conversions via
std::interfaces::Serialize(T) by lowering expr as T to expr.serialize()
when the operand type provides a matching serialize method.
For structured conversions, it also supports std::interfaces::Deserialize(S)
by lowering expr as T to T.deserialize(expr) when the target type provides
a matching static deserialize method.
The supported conversions and semantics for Silk currently are
specified in operators (“Casts (as)”).
Notes:
asis explicit. It does not introduce new implicit coercions.- For call-argument ergonomics, see the separate opt-in coercion mechanism described above (“Implicit Call-Argument Coercions”).
Nominal & Parameterized Types#
Nominal types are introduced by declarations (e.g. struct, enum, interface) and are equal only to themselves. Parameterized types are constructed by applying a type constructor to type arguments.
The compiler must:
- Treat nominal types as distinct even if their field layout is identical.
- In the full language design, support parameterized types in all contexts
where the spec permits them. In Silk currently, type-parameter
generics are supported for nominal declarations (
struct/interface) and for applied types in type positions (Name(u8)). - Const parameters and integer-literal type arguments (
Name(N: int),Name(u8, 1024)) remain tracked work and are rejected (E2016). - The
Option(T)optional sugar described above remains supported for the Supported forms.
Parameterized type syntax#
The initial surface syntax for applying type arguments is:
TypeApply ::= TypeName '(' TypeArgListOpt ')'TypeName ::= Identifier ('::' Identifier)*TypeArgListOpt ::= TypeArgListTypeArgList ::= TypeArg (',' TypeArg)* ','?TypeArg ::= Type | IntLiteral
Examples:
Foo(int, 1024)
Mutex(Account)
Result(int, string)
Notes:
- A
TypeArgmay be a type (e.g.int,&Foo,Option(string)) or a compile-time integer literal for const-parameter-style arguments. - The full semantics (declaring generic parameters, constraint checking, and monomorphization) are still evolving; the key requirement is that the compiler preserves the argument structure in the AST/type system so later stages can enforce and lower it.
Reference Types#
Reference types describe access to values rather than owning them (e.g. references, borrowed views, or other non-owning handles as specified in this document and related language docs).
Key requirements:
- Distinguish owning vs. non-owning types in the type system.
- Preserve aliasing and lifetime constraints so that regions, buffers, and FFI safety rules can be enforced.
Current implementation notes:
&Structis supported in function parameter types and as local values when produced by heap allocation (new) or by calls that return&Struct.&TwhereTis a single-slot scalar primitive (for example&bool,&int,&u64,&f64) is supported in function parameter types and as local values when produced by the borrow operator&expr.- Borrowed
&Structreferences may also be created from stack values: - via the borrow operator
&expron borrowable lvalues, and - via implicit borrow coercions in contexts that expect
&T(for examplelet r: &Pair = pair;). These borrows are checked with conservative lexical lifetime rules (they may not escape the scope of the borrowed stack storage). - Mutable reference parameters use the two-part
mutcontract and conservative per-call aliasing rules; see mutability.
Function Types and Closures#
Silk currently:
- Parses function types in type positions (most notably for
extdeclarations). - Implements function expressions (lambdas) in expression positions:
- expression body form:
fn (x: int, y: int) -> x + y - block body form:
fn (x: int, y: int) -> int { return x + y; } - block body
voidshorthand:fn (x: int, y: int) { ... }(implicitvoid) - Function expressions may declare
&Tparameters only whenTis a single-slot scalar primitive (for example&int/&bool). - Function expression bodies are checked under the
purerules in the current subset. Non-capturing function expressions are inferred aspurefunction types and are permitted inpurecode: - they may call only
purefunctions, - they may not mutate (
let mut/var, assignment), - they may not allocate (
new), - they may not use typed error contracts or
panic. - The checker also supports purity inference (“auto-pure”) for ordinary function
declarations and
implmethods. When inferred, these functions/methods are treated aspurefor call checking and are callable frompurecode. - Capturing closures are implemented as a subset of function values:
- a function expression body may reference immutable locals/parameters from an enclosing scope; those values are captured by value into a heap environment,
- in the Supported forms, only scalar captures are supported (
int, fixed width ints,bool,char,f32,f64,Instant,Duration), - forming captures inside
purecode is rejected (capture environments allocate), but closure values are still checked under thepurerules and remain callable frompurecode once constructed. - Function values are supported end-to-end for this subset (non-capturing and capturing):
- they may be passed as arguments, returned from functions, stored in structs/arrays, and called indirectly.
- the runtime representation is a pair
{ func_ptr, env_ptr }as specified in memory model. - Discipline modifiers for function declarations (
pure/task/async) are implemented. Function types in type positions do not currently include discipline modifiers.
C Function Pointers (c_fn)#
Silk distinguishes between:
fn (...) -> Rfunction values (which may carry a closure environment), andc_fn (...) -> RC callback pointers (code pointers only; no environment).
c_fn is intended for FFI: it is a safe, storable representation for passing
callbacks to foreign code.
Rules (Supported forms):
- A
c_fnvalue may be formed only from: - a top-level function name, or
- a non-capturing
fn (...) -> ...expression. - Capturing closures are rejected when a
c_fnis required. c_fnvalues are ABI-lowered as a singleu64code pointer.
Source repository · Edit this page · View Markdown