Operators
This document summarizes the operator set and precedence for Silk.
Operator Set#
The language includes the following operators and delimiters:
- Assignment and compound assignment:
=,+=,-=,*=,/=. - Increment/decrement:
++,--(prefix and postfix). - Arithmetic:
+,-,*,/,%. - Currently:
- integer operands support
+,-,*,/, and%, - floating-point operands (
f32/f64) support+,-,*, and/(no%). - unary
-xis supported for both integer and floating-point operands. - time types support a small arithmetic subset:
Duration + Duration,Duration - Duration, and unary-Duration,Instant + Duration,Duration + Instant,Instant - Duration,- and
Instant - Instant(producing aDuration). rangesupports shifting by anintoffset:range + int,range - int,int + range.- Bitwise:
&,|,^,~,<<,>>. - Currently, bitwise operators are defined for
integer operands (
intand the fixed-width integer types): &,|,^perform bitwise AND/OR/XOR on two integer values of the same type and produce a result of that same type.~xperforms bitwise NOT on an integer value and produces a result of that same type.<<,>>shift the left-hand integer operand by an integer shift amount of the same type;>>uses an arithmetic right shift for signed integers (i*/int) and a logical right shift for unsigned integers (u*).- Comparison:
==,!=,<,<=,>,>=. - Currently, comparisons are defined for both integer operands and floating-point operands of the same type.
- In Silk,
==and!=are also defined forbooloperands. - In Silk, comparisons are also defined for
DurationandInstantwhen both operands have the same time type. - In Silk,
==and!=are also defined forstringoperands, comparing the underlying UTF-8 byte sequences for equality (length check + bytewise compare). - In Silk, ordered comparisons over
string(<,<=,>,>=) are defined as bytewise lexicographic ordering over the underlying UTF-8 byte sequences (unsigned byte comparison, with shorter-prefix ordering when one string is a prefix of the other). - In Silk,
==and!=are also defined for supported optional values (T?,string?, optionals of the supportedstructsubset, and nested optionalsT??): None == Noneis true,Some(x) == Some(y)compares the payload values for equality (recursively for nested optionals),- and
!=is the logical negation of==. - Currently,
NoneandSome(...)can appear in equality expressions when the other operand has an optional type (for exampleopt == Noneandopt == Some(x)), using that other operand’s type to infer the optional payload type. - In Silk,
==and!=are also defined for the supportedstructsubset (see structs impls layout), performing slot-wise equality over the lowered scalar slots (including embedded strings, nested structs, and optionals); float slots use IEEE-754 equality semantics. Ordered struct comparisons are not implemented. - Float comparisons follow IEEE-754 semantics:
NaNcompares unequal to everything (including itself), and ordered comparisons (<,<=,>,>=) are false when either operand isNaN. - Logical:
!,&&,||. - Currently:
!is supported forbooloperands.- Member and scope:
.,::,?.. - Currently:
.and::are supported,- and
?.is supported for optional chaining on the supportedstructsubset: opt?.fieldyieldsFieldType?,opt?.method(args...)yieldsResultType?. See optional.- Casts:
asandas raw(postfix). - Syntax:
- numeric/shape cast:
<expr> as <Type>, - raw bit-cast:
<expr> as raw <Type>. asis an explicit, potentially lossy conversion operator intended for primitive numeric conversions (see “Casts (as)” below).as rawis an explicit bit reinterpretation operator intended for generic storage/marshalling of scalar values (see “Raw casts (as raw)” below).- Typed error propagation:
?(postfix). - Syntax:
<call_expr>?. - This propagates typed errors from an error-producing call to the enclosing function; see typed errors.
- Ranges / varargs delimiters:
..,..=,.... ..and..=form range literals of typerange(see types)....is the varargs/rest marker (see varargs).- Other punctuation:
?,??,->,=>,,,;,(,),{,},[,],_,:. - Currently,
??is supported for: - optionals in Silk (including scalar,
string, and the currentstructsubset, plus nested optionals in the supported payload subset; see optional), and - recoverable
Result-like values, whereresult ?? fallbackyields theOk(...)payload and evaluatesfallbackonly forErr(...)(result), and - ordinary named enums with exactly two declared variants, where the first declared variant is the “success” arm:
- if the first variant is unit,
value ?? fallbackyields that enum value, - if the first variant carries exactly one payload, it yields that payload,
- and if the value is the second variant,
fallbackis evaluated. - The right-hand side may be either:
- an ordinary fallback expression, or
- one of the narrow terminal control-flow forms accepted only after
??: value ?? return exprvalue ?? breakvalue ?? continuereturn,break, andcontinuekeep their normal statement validity rules:returnmust match the enclosing function result type,breakandcontinueare only valid inside loops.- This does not make those control-flow forms general expressions
elsewhere; the grammar extension is specific to the right-hand side of
??. The?token is used both in type annotations (T?) and as the postfix typed error propagation operator for error-producing calls (call()?; see typed errors).
The lexer and parser must recognize these tokens exactly as specified, and precedence/associativity must match the formal grammar.
Assignment#
Assignment updates an existing binding (an lvalue). Assignment is “statement-like”:
it is parsed as an expression but has type void and is intended to appear as an
expression statement.
=
x = expr evaluates expr and stores the resulting value into x.
Rules:
- The left-hand side must be an assignable lvalue. In the Supported forms, it may be:
- an identifier that refers to a local
let mutbinding, or - a struct field lvalue
name.field(or nested field lvaluename.field1.field2...) wherenameis either: - a local
let mutbinding of a supported PODstruct, or - a
mutborrowed reference parameter (mut name: &Struct). In Silk, nested field assignment is supported only when the leaf field lowers to a single scalar slot (for examplebool, integer scalars,f32/f64, and unit-only enum tags). Direct field assignment also supports the complete hosted enum layout, including payload-carrying enums, through both mutable local aggregates andmutborrowed aggregate parameters. - Identifier lvalues must refer to
let mutlocal bindings. - The type of
exprmust match the binding’s type. - When a direct field stores an ownership-tracked value,
name.field = sourceconsumes the source binding, drops the old field value exactly once, and installs the new owner. This applies to mutable local aggregates andmut &Structparameters. - A direct owned field on the right-hand side is consumed under the same rule
whether it initializes a new local or replaces an existing local. Thus
target = owner.field; owner.field = empty;is a valid take/reinitialize sequence and the sentinel assignment does not destroytarget. - If that exact field was previously moved out, it has no displaced owner to destroy. Assignment installs the replacement directly and marks the field initialized again; reading the field between the move and reinitialization is a use-after-move error.
- The assignment expression has type
void.
Compound assignment (+=, -=, *=, /=)#
Compound assignments are shorthand for “read-modify-write”:
x += yis equivalent tox = x + y(and similarly for-=/*=//=), withyevaluated exactly once.
Rules:
- The left-hand side must be an assignable lvalue (as described above for
=). - In the Supported forms, compound assignments are supported only for numeric
scalar types (integers and
f32/f64), including numeric struct fields. - The compound assignment expression has type
void.
Increment and Decrement (++ / --)#
++x, x++, --x, and x-- increment or decrement an existing binding by
1.
In Silk, increment/decrement expressions are “statement-like”: they have type
void and are intended to appear only as expression statements.
Rules:
- The operand must be an assignable lvalue (the same lvalue rules as
=). - The operand type must be an integer scalar type (
int,i8,u8,i16,u16,i32,u32,i64,u64,size,usize). (isizeis accepted as an alias forsize.) - Prefix and postfix forms are equivalent in Silk (both update the binding and
produce
void). - Conceptual desugaring:
x++and++xare equivalent tox += 1;x--and--xare equivalent tox -= 1;
sizeof#
sizeof <operand> produces the size of a type or value in bytes.
For string values, sizeof(value) is the canonical way to read the UTF-8 byte
length for FFI pointer/length pairs:
let title_ptr = title as raw u64;
let title_len = sizeof(title);
The operand may be a direct name, a field access, or another expression that
evaluates to string, for example sizeof(options.title) or
sizeof(make_title()).
Use an explicit cast only when calling an API whose contract is intentionally
signed or narrower than usize.
Result type:
sizeofalways returnsusize.
Evaluation mode:
- When the operand is a type name (a primitive type,
struct/enumname, type alias, or qualified type name),sizeofis a compile-time constant. - When the operand is a compile-time constant value (literals and other
const-evaluable expressions),
sizeofis a compile-time constant. - When the operand is a runtime value,
sizeofis evaluated at runtime.
Sized integration:
- Implemented (partial):
sizeof <string value>produces the string’s byte length (asusize). This reads the current string ABI layout ({ ptr: u64, len: i64 }) through the reserved intrinsic__silk_string_len;std::runtime::mem::string_lenremains available only as a compatibility and target-shim helper. - Planned (general): for other runtime values, if the operand type provides an
instance method matching
std::interfaces::Sized(fn size(self: &Self) -> usize),sizeof valuewill lower to a call of that method. - For type operands, if the operand type provides a static, pure method
pure fn size() -> usize, the compiler may foldsizeof Typeto that value when the method body is const-evaluable; otherwise it falls back to the compiler’s built-in size model.
Built-in size model :
- Sizes reflect the current scalar-slot lowering model (structs impls layout): each lowered scalar occupies one 8-byte slot.
- A
stringvalue occupies two slots ((u64 ptr, i64 len)), sosizeof stringis16in the Supported forms. - A
T[]slice value occupies two slots ((u64 ptr, i64 len)), sosizeof T[]is16in the Supported forms. - A
T[N]fixed array occupiesN * sizeof(T)bytes in the Supported forms, using the element’s scalar-slot size.
Notes:
sizeof string(type operand) is the representation size (currently 16 bytes in the scalar-slot model), whilesizeof <string value>is the content size (byte length).sizeofis a byte-size operator. For logical element counts (for example a slice length or vector length), use a.len()method viastd::interfaces::Lenon the relevant type. The standard library does not define a genericlength(...)helper.
Parsing note:
- Because
Name[expr]is also indexing syntax, fixed array type operands should be parenthesized:sizeof (u8[4]). Without parentheses,sizeof u8[4]is parsed as an index expression. - Because
asbinds at postfix precedence, baresizeof x as Tparses assizeof (x as T). To cast the result ofsizeof, writesizeof(x) as Tor(sizeof x) as T.
alignof#
alignof <operand> produces the alignment of a type or value in bytes.
Result type:
alignofalways returnsusize.
Evaluation mode:
- When the operand is a type name (a primitive type,
struct/enumname, type alias, or qualified type name),alignofis a compile-time constant. - When the operand is a compile-time constant value (literals and other
const-evaluable expressions),
alignofis a compile-time constant. - When the operand is a runtime value,
alignofis evaluated at runtime.
Built-in alignment model :
- Alignments reflect the current scalar-slot lowering model (structs impls layout): values are stored as 8-byte slots.
- All non-
voidtypes currently have alignment8. alignof voidis1.
Parsing notes:
- As with
sizeof, fixed array type operands should be parenthesized:alignof (u8[4]). Without parentheses,alignof u8[4]is parsed as an index expression. - Because
asbinds at postfix precedence, barealignof x as Tparses asalignof (x as T). To cast the result ofalignof, writealignof(x) as Tor(alignof x) as T.
offsetof#
offsetof(Type, field_path) produces the byte offset of a struct-like field
within Type in the current memory layout model.
Result type:
offsetofalways returnsusize.
Evaluation mode:
offsetofis always a compile-time constant.
Operands:
Typemust name astructorerrortype (including nested structs).field_pathis one or more field identifiers separated by.(for exampleborinner.header.len).
Built-in offset model :
- Offsets reflect the current scalar-slot lowering model (structs impls layout): each lowered slot is stored in an 8-byte cell, and composite fields (nested structs, optionals, strings, etc.) are expanded into their slot sequences in source order.
offsetof(Type, field)returns the offset of the first slot of that field’s lowered representation, in bytes.- When
field_pathtraverses an optionalT?field, it refers to the payload layout (the path implicitly skips the tag slot).
typename#
typename <expr> and typename(<expr>) produce a string naming the static
type of <expr>.
Result type:
typenamealways returnsstring.
Evaluation mode:
typenameis always a compile-time constant string.
Operand notes (Supported forms):
- When the operand is a bare name that does not resolve to an in-scope
runtime binding (for example
int,User, orstd::wasm::Module), the compiler interprets it as a type name and returns that type’s name. - Formatting uses the compiler’s normal type formatting (for example
T[],&T, andfn (...) -> ...). - For monomorphized generic instantiations, the string is the human-readable
display name (not an internal
__silk_mono__...symbol).
is#
<expr> is <Type> checks whether the static type of <expr> conforms to
<Type>.
Result type:
isalways returnsbool.
Evaluation mode:
isis always a compile-time constant boolean.
Rules (Supported forms):
- The right-hand side must be a type (primitive, nominal
struct/enum/error,interface, a function type, or a type alias for one of those). - If
<Type>is a nominalstructtype,expr is Typeis true when the expression’s static type is exactlyTypeor astructthatextendsType. - If
<Type>is aninterface,expr is Interfaceis true when the expression’s static type declares conformance (impl T as Interface) or when the operand is a module declaredmodule Name as Interface. - For primitive types, enum/error types, reference types (
&T), slice/array types (T[],T[N]), optionals (T?), and function types,iscurrently checks exact type equality (after resolving type aliases).
Notes:
isdoes not perform runtime tagging or value inspection. For runtime discrimination of union/optional values, usematchand the relevant pattern forms.
Examples:
type Adder = fn (x: int, y: int) -> int;
fn my_adder (x: int, y: int) -> int { return x + y; }
if my_adder is Adder { /* ... */ }
struct User { id: u64 = 0 }
struct Beep extends User { boop: string = "" }
let beep = Beep{ boop: "boop" };
if beep is User { /* ... */ }
let n = 123;
if n is int { /* ... */ }
interface Logger { fn log (value: string) -> void; }
module my_logger as Logger {
export log (value: string) { /* ... */ }
}
if my_logger is Logger { /* ... */ }
Wrapping and Overflow#
The spec notes “Arithmetic Wraps” for certain operators. The checker and code generator must:
- Implement deterministic wrapping behavior for integer arithmetic where required.
- Clearly separate wrapping operations from checked or saturating variants (if exposed in the standard library).
Casts (as)#
as is a postfix operator that performs an explicit conversion to a target
type.
Precedence#
as binds at postfix precedence (like calls, field access, and ?). For example:
a + b as intparses asa + (b as int).- To cast the result of
new, use parentheses so the cast applies to the heap reference:(new Type{ ... }) as &Other. Without parentheses,new Type{ ... } as &Otherparses asnew (Type{ ... } as &Other).
Supported conversions#
In Silk, as is supported for primitive scalar
conversions:
-
Integer → Integer (including
Instant,Duration, andchar): -
The conversion is deterministic and may be lossy. It is performed by canonicalizing the underlying bits to the destination integer type (width truncation + sign/zero extension as appropriate). For scalar widths up to 64 bits this matches the behavior of
ir.CastIntin the current IR;i128/u128follow the analogous rule over their{ lo, hi }lane layout. -
Float → Float:
-
f32/f64/f128conversions using standard IEEE-754 conversion and rounding. -
Integer → Float:
-
Converts the integer value to the destination float type (IEEE-754), with rounding when the integer cannot be represented exactly.
-
Float → Integer:
-
Converts by truncating toward zero.
-
If the source value is
NaN, the result is0. -
If the source value is outside the destination integer’s representable range, the result saturates to the nearest bound (min/max for signed,
0/max for unsigned). -
Struct → Struct (safe “shape cast”):
-
A cast from
StoTis permitted when bothSandTname non-opaque struct types and their fields match positionally: -
same field count, and
-
for each index
i, the field type ofSatiis exactly the same type as the field type ofTati(field names may differ). -
This is intended for “newtype-like” wrappers and schema evolution where two structs have the same shape but different field names.
-
Semantics: the cast produces a value copy of the underlying struct slots, retyped as
T. The operation does not reorder fields. -
&Struct→&Struct(safe “shape cast” for references): -
A cast from
&Sto&Tis permitted whenSandTare compatible under the same Struct → Struct “shape cast” rules above. -
Semantics: the cast produces a retyped view of the same referenced storage. It does not allocate and does not copy the underlying struct slots.
-
For refcounted heap references created by
new, the cast is still a view only: it must not change whichdropimplementation runs when the refcount reaches zero. The allocation’s concrete type (tracked through the value, not the view type) determines Drop behavior at the last release. -
This means the two references alias: reading fields through the cast view observes updates made through the original reference (and vice versa).
-
Because the two references alias, the compiler’s per-call mutable-borrow restrictions treat aliases as the same storage: a single call expression may not take multiple mutable borrows (or both mutable and immutable borrows) of the same underlying reference, even if the aliases are held under different local names. See mutability.
-
This cast is intentionally conservative: it is permitted only when the compiler can prove the two referenced struct layouts are identical at the type level (same field types in the same order). It does not permit arbitrary “reinterpret pointer” casts.
-
u64/usize↔T[]/T[N](unsafe pointer/slice view cast): -
Silk’s Supported forms represents raw addresses as
u64and permits pointer-width unsignedusizevalues to be used as raw addresses in these casts. For low-level byte-copy routines and runtime interop,assupports explicit conversions between raw addresses and array/slice views: -
ptr as T[]constructs aT[]slice view where the pointer component isptrand the length component is a dedicated unknown-length sentinel (currently,i64.min). The compiler does not validate the pointer value. -
Indexing and assignment through an unknown-length slice are permitted but unchecked: the runtime performs no
index < lenbounds check. -
Operations that require a known length (iteration, slicing, etc.) trap unless an explicit length is provided.
-
ptr as T[](/silk/docs/len)constructs aT[]slice view where the pointer component isptrand the length component islen(element count). -
ptr as string(len)constructs astringview where the pointer component isptrand the length component islen(byte count). This is sugar forstd::runtime::mem::string_from_ptr_len(ptr, len)(and the reserved intrinsic__silk_string_from_ptr_len). -
slice as u64/slice as usizeextracts the pointer component of aT[]slice. -
arr as u64/arr as usizeextracts the address of element0of a fixed arrayT[N](forN == 0, the result is0). -
These casts remain unsafe:
-
the compiler does not validate pointer provenance (whether the address is valid for the claimed element type).
-
in the current scalar-slot subset,
T[]/T[N]indexing assumes the pointed-to memory is laid out in Silk’s scalar-slot representation. This is not a packed-byte view. For packed byte access (for example string storage), usestd::runtime::mem::{load_u8,store_u8}orstd::arrays::ByteSlice. -
In Silk, indexed accesses through arrays/slices trap when:
-
the pointer is
0, -
the pointer is not 8-byte aligned,
-
the explicit length is negative (when provided),
-
the index is out of bounds (
index < len) when the slice/array has a known (non-unknown) length. -
Serialize(T)-backed casts (explicit conversion viaserialize()): -
When the operand type provides a unique instance method named
serializematching thestd::interfaces::Serialize(T)surface (fn serialize(self: &Type) -> T),expr as Tis permitted and lowers to a call of that method. -
The conversion is explicit (it does not introduce implicit coercions).
-
The
serializemethod must be infallible (no typed errors). -
Purity rules apply: inside
pure fn, theserializemethod must bepure. -
Supported forms limitation: the compiler must be able to resolve the receiver’s nominal type at the cast site so it can lower the implicit
serialize()call. This includes name expressions, field accesses, calls, and array/slice indexing (arr[i] as T) in the Supported forms. -
Deserialize(S)-backed casts (explicit conversion viadeserialize(...)): -
When the target type provides a unique static method named
deserializematching thestd::interfaces::Deserialize(S)surface (fn deserialize(value: S) -> Self),expr as Selfis permitted and lowers toSelf.deserialize(expr). -
This rule is checked before struct shape casts: when both a
deserializeconversion and a shape cast could apply, thedeserializeconversion is used. -
The conversion is explicit (it does not introduce implicit coercions).
-
The
deserializemethod must be infallible (no typed errors). -
Purity rules apply: inside
pure fn, thedeserializemethod must bepure.
Examples (Supported forms):
struct Data {
value: string,
}
struct User {
name: string,
}
fn main () -> int {
let data = Data{ value: "hello" };
let user = data as User;
assert data.value == user.name;
return 0;
}
struct A {
value: string,
}
struct B {
value: string,
}
fn set_value (mut b: &B, value: string) -> void {
b.value = value;
}
fn main () -> int {
let a: &A = new A{ value: "hello" };
var b = a as &B;
set_value(mut b, "world");
assert a.value == "world";
assert b.value == "world";
return 0;
}
Notes:
asdoes not participate in the implicit call-argument coercion mechanism described in types (that mechanism is opt-in per destination struct and is used primarily for stdlib ergonomics).
Raw casts (as raw)#
as raw is a postfix operator that reinterprets the raw bits of a scalar
value as another scalar type. It is intended for use in generic collections
and low-level marshalling where numeric conversion would be incorrect (notably
when storing f32/f64 values in integer-backed storage).
Syntax:
<expr> as raw <Type>
Rules (Supported forms):
- Both the operand and the target type must be numeric primitive types supported by the backend:
- 64-bit-slot scalars:
i8/u8/i16/u16/i32/u32/i64/u64/int,f32/f64, plus int-like primitives lowered to those scalars such asDuration/Instantandchar. - 128-bit wide primitives:
i128/u128/f128(two 8-byte lanes;f128stores the raw IEEE-754 binary128 bit pattern). as rawis not permitted forvoid, optionals, arrays, maps, Silk function value types (fn (...) -> R), or structs/enums.- Special-case:
u64 as raw c_fn (...) -> R(andusize as raw c_fn (...) -> R) is permitted for dynamic symbol loading and C ABI interop. The reverse direction,c_fn (...) -> R as raw u64(orusize), extracts the raw code pointer. This does not apply to Silk closure-carryingfn (...) -> Rvalues. - Special-case:
string as raw u64(andstring as raw usize) is permitted and extracts the string’s underlying byte pointer through the reserved intrinsic__silk_string_ptr. Prefer this direct syntax in application code, examples, and ordinary stdlib facades.std::runtime::mem::string_ptrremains available only as a compatibility and low-level target-shim helper. - Special-case:
&T as raw u64(and&T as raw usize) is permitted and extracts the reference’s underlying address as an integer. This is intended for low-level interop (for example passing&Structpointers to C APIs that usevoid */T *handles). - This does not make integer→reference casts legal:
u64 as raw &Tremains rejected in the Supported forms. - Semantics:
- The operand’s current canonical scalar bits are reinterpreted as the target
type’s canonical scalar bits (bit-level truncation/masking for narrower
target widths such as
u8/u16/u32andf32). - For 128-bit primitives, this is lane-wise:
- the low lane is copied as
u64bits, - the high lane is reinterpreted across
u64/i64as needed, - when casting a 128-bit value to a <=64-bit target, the low lane is used,
- when casting a <=64-bit integer value to
i128/u128, the high lane is sign-extended (i128) or zero-extended (u128) in the current subset. - No numeric conversion is performed. For example,
1.0 as u64yields1, while1.0 as raw u64yields the IEEE-754 bit pattern.
Examples:
let bits: u64 = (1.0 as f32) as raw u64;
let f: f32 = bits as raw f32;
// Pointer + length extraction for low-level interop.
let s: string = "hello";
let ptr: u64 = s as raw u64;
let len: usize = sizeof s;
Source repository · Edit this page · View Markdown