Language Version 0.1.1
This is the specification of the Solid programming language, hereinafter referred to as solid-lang.
- High Performance
- Systems Programming
- A modern, uncompromising alternative to C
- Manual & Explicit Memory Management
- Purely Static
- Full static typing
- Fixed static layout
- Safe Implicit
- Safe implicit type inference
- Safe implicit conversions
- No implicit memory management
- No implicit syntax rules
- Strong Orthogonality
- Regular and systematic syntax
- Minimal atomic primitive set
- Uniform consistent semantics
- Unrestricted valid combination
| Type | Size | Description |
|---|---|---|
i8 |
8-bit | 8-bit signed integer |
i16 |
16-bit | 16-bit signed integer |
i32 |
32-bit | 32-bit signed integer |
i64 |
64-bit | 64-bit signed integer |
isize |
Pointer-sized | Platform dependent size signed integer |
u8 |
8-bit | 8-bit unsigned integer |
u16 |
16-bit | 16-bit unsigned integer |
u32 |
32-bit | 32-bit unsigned integer |
u64 |
64-bit | 64-bit unsigned integer |
usize |
Pointer-sized | Platform dependent size unsigned integer |
f32 |
32-bit | Single precision IEEE 754 |
f64 |
64-bit | Double precision IEEE 754 |
bool |
1-bit | boolean, which can be true or false |
Remarks
boolsize is platform dependent. In most cases, it will occupy 1 byte in memory.
Arrays are fixed-size sequences of elements.
Syntax:
var arr1: [5]i32 = [5]i32{1, 2, 3, 4, 5};
// arr2 is [3]u32;
var arr2 = [3]u32{1, 2, 3};
// zero initialization
var arr3 = [6]i32{};
// partial initialization
var arr4: [6]i32 = [6]i32{1, 2, 3};
// value copy, arr is of type [5]i32
var arr5 = arr1;
// invalid, type not same
// var arr6: [6]i32 = arr1;
Rules:
- Array size must be a compile-time constant
- Arrays are value types (copying creates a full copy)
- Partial initialization is allowed; remaining elements are zero-initialized
- Type equivalence: arrays must have identical base type and length
Structs are composite types with named fields.
Declaration:
struct Point {
x: f32,
y: f32,
}
Usage:
var point = Point {x = 1.0, y = 2.0};
var zero = Point {};
var partial = Point {x = 1.0};
Layout Annotations:
@align(16)
struct AlignedStruct{
}
@packed
struct PackedStruct{
}
@explicit
struct ExplicitStruct{
@offset(0) data: [8]u8,
@offset(0) point: Point,
}
Rules:
- Structs are user-defined named types declared at the top level
- Empty structs are valid (zero-sized types); useful for phantom type markers, capability tokens, and compile-time state machines
- Memory layout follows declaration order by default
- Natural alignment is used by default (like in C), can use layout annotations to specify the layout
- Type equivalence: structs must have identical fully qualified names
Unions share memory layout among fields.
Declaration:
union Value {
as_i32: i32,
as_f32: f32,
as_bytes: [4]u8,
}
Usage:
var v1 = Value::as_i32(12345);
v1.as_f32 = 1.0f;
v1.as_i32 = 1;
v1.as_bytes = [4]u8 {2,5,6,8};
// zero initialized
var v2 = Value{};
Layout Annotations:
@align(16)
union AlignedUnion{
}
@packed
union PackedUnion{
}
Rules:
- Unions are user-defined named types declared at the top level
- All fields share the same memory space
- Size equals the largest field
- Empty unions are valid (zero-sized types)
- Type equivalence: unions must have identical fully qualified names
Enums are discrete sets of named constants.
Declaration:
enum Color: u32 {
Transparent = 0x00000000,
Red = 0xFF0000FF,
Green = 0xFF00FF00,
Blue = 0xFFFF0000,
}
Usage:
var color = Color::Red;
Flags Annotation:
@flags
enum Permissions: u32 {
None = 0,
Read = 1 << 0,
Write = 1 << 1,
Execute = 1 << 2,
}
Rules:
- Enums are user-defined named types declared at the top level
- Optional
: typespecifies the underlying type (default:i32, allowing up to 256 variants)` - Underlying type can be any integer type:
u8,u16,u32,u64,usize,i8,i16,i32,i64,isize - When
= expris omitted, the value auto-increments: the first field defaults to0, each subsequent field defaults to the previous field's value plus1 - Empty enums are valid
- Type equivalence: enums must have identical fully qualified names
Variants are tagged unions combining an enum discriminant with union data.
Declaration:
variant Option<T> {
Some: T,
None,
}
variant Result<TValue, TError>: u8 {
Ok: TValue,
Error: TError,
}
Usage:
var some = Option<i32>::Some(42);
var none = Option<i32>::None;
var ok = Result<i32, String>::Ok(100);
Rules:
- Variants are user-defined named types declared at the top level
- Generic variants require explicit type parameter specification
- Optional
: typespecifies the tag type (default:u8, allowing up to 256 variants) - Tag type can be any integer type:
u8,u16,u32,u64,usize,i8,i16,i32,i64,isize - The tag is automatically managed by the compiler
- Empty variants are valid (zero-sized types)
- Type equivalence: variants must have identical fully qualified names
A type whose size is zero bytes is a zero-sized type (ZST). In solid-lang, the following declarations produce ZSTs:
- Structs with no fields (
struct Empty{}) - Unions with no fields (
union None{}) - Variants with no arms (
variant Void{})
Semantics:
Zero-sized types follow Rust-style semantics: they are fully first-class values that participate in all normal type operations, but their zero size leads to specific behaviors at the machine level.
Instantiation:
ZSTs can be instantiated normally. Construction takes no memory — the value exists purely at the type level.
var e = Empty{}; // no stack space consumed
var n = None{}; // same
Pointers and Address-of:
Taking the address of a ZST is legal and returns a pointer aligned to the type's alignment (minimum 1 byte). The pointer targets a compiler-reserved zero-width phantom location — it is valid to dereference but reads zero bytes of data, and writes are no-ops.
var e = Empty{};
var p: *Empty = &e; // legal; p is 1-aligned, points to phantom location
var e2 = *p; // legal; dereferences zero bytes, returns Empty{}
Consequently:
- Multiple
&zstexpressions may return the same address. - All elements of a ZST array share the same address (each element is 0 bytes apart).
- ZST pointers can be freely cast to and from
*opaque.
Size and Alignment:
| Property | Value |
|---|---|
@sizeof(ZST) |
0 |
@alignof(ZST) |
1 (unless overridden by layout annotations) |
When a ZST has layout annotations (e.g., @align(16)), @alignof returns the specified alignment, but @sizeof remains 0.
Operations:
| Operation | Behavior |
|---|---|
| Field access | Error (no fields exist) |
Value comparison a == b |
Always true (no data to compare) |
Pointer arithmetic p + n |
p + n * 0 = p (advances zero bytes) |
Array [N]ZST |
Total size 0; all elements share the same address |
| Function parameter (by value) | Eliminated at codegen; no register/stack slot used |
| Return value (ZST) | Eliminated at codegen |
Assignment a = b |
No-op at machine level |
*opaque cast |
Allowed; produces a valid opaque pointer |
Use Cases:
- Phantom type markers for compile-time type state (e.g.,
struct Locked{},struct Unlocked{}) - Capability tokens for API safety (e.g.,
struct RootCap{}) - Tag types for generics specialization
- Compile-time state machine witnesses
Pointers are standard indirection types. All pointers to T share a single type *T.
Note: solid-lang does NOT support the *void type. Use RawPtr from the standard library for untyped pointers.
Syntax:
func demo() {
var value: i32 = 42;
var p: *i32 = &value; // *i32
var v: i32 = *p; // dereference (read)
*p = 100; // dereference (write)
}
Rules:
- Type equivalence: two pointers are equivalent if they point to the same target type.
Null Pointers:
The null keyword works identically for both permissions:
struct opaque{}
func demo() {
var ptr: *i32 = null;
var raw: *opaque = null; // equivalent to void* in C
// invalid
// var ptr2 = null;
var ptr3: *Point = raw.into<*Point>();
if ptr == null {
// Handle null case
}
}
Note: The null keyword can only be used with pointer types. For other types requiring a sentinel "empty" value, use appropriate types like Option<T>. In a var declaration without surrounding type context, as null alone does not describe the pointer target type.
Syntax:
func add(left: i32, right: i32):i32{
return left + right;
}
var callback: *func(i32, i32): i32 = &add;
var nullfunc: *func(i32, i32): i32 = null;
// invalid, func signature is not a type
// var signature: func(i32, i32): i32 = add;
Function Pointers (*func(...)):
- Can be
null - Used for optional callbacks and FFI interoperability
- Must be checked for
nullbefore calling - Require explicit dereference to call:
(*ptr)(args)
Rules:
- Function signatures include parameter types, return type, and calling convention
- Function pointers (
*func(...)) are nullable - Calling convention is part of the type signature
&func_nameobtains a function pointer (*func(...))- Function signature equivalence requires: parameter count, types, and order all equal; calling convention equal; return type equal
- Function pointer types (
*func(...)) are equivalent when their function signatures are equivalent
Example:
func add(a: i32, b: i32): i32 {
return a + b;
}
func demo_pointer() {
var ptr_callback = &add;
if ptr_callback != null {
var result = (*ptr_callback)(1, 2);
}
}
Calling Conventions:
| Convention | Description |
|---|---|
cdecl |
C calling convention (default) |
stdcall |
Windows API calling convention |
solid-lang uses explicit conversion methods instead of implicit casting.
Conversion Interfaces:
interface ICast<TFrom, TTo> {
func into<TTo>(from: TFrom): TTo;
func from(value: TFrom): TTo;
}
Usage:
var a = 10;
// Using into<T>() method
var b: i16 = a.into<i16>();
// Using Type.from() method
var c: i16 = i16::from(a);
// Pointer conversion
var raw: *opaque = null;
var ptr = raw.into<*Point>();
Rules:
- All type conversions must be explicit
- Implement
IIntoorIFrominterfaces for custom conversions - Compiler provides built-in conversions for primitive types
Integer Literal:
// integer literals without a suffix are of type i32.
var dec = 12345;
dec = 0;
// invalid, leading zero not allowed, to avoid the ambiguity from C-style octal literal
// dec =0879;
// optional prefix for decimal integer literal
dec = 0d13579;
// 0D is invalid
// dev = 0D13579;
// hexadecimal integer literal
var hex = 0x7FFFFFFF;
// 0X is invalid,
// hex = 0XFFFF;
// octal integer literal
var oct = 0o755;
// 0O is invalid
// oct = 0O755;
// binary integer literal
var bin = 0b10101010;
// 0B is invalid
// bin = 0B010101;
// With underscores for readability
var large = 1_000_000_000;
// successive underscores are invalid
// large = 1__0__0;
// With type suffix, without suffix
var i8_val = 127_i8;
var u64_val = 18446744073709551615_u64;
// Shorthand for pointer-width types
var idx = 0u; // usize
var sgn = 0i; // isize
// type inference
// very clear, indicate the type double times
var int1: i32 = 123_i32;
// 123 is inferenced to u32
var int2: u32 = 123;
// invalid, type conflict
// var int3: u32 = 123_i32;
// int4 is type inferenced to u64
var int4 = 123u64;
// int5 and 32 are inferred to i32, which is default
var int5 = 32;
Float Literal:
var float = 3.1415926;
var f32_val: f32 = 3.14;
var f64_val: f64 = 3.14159265358979;
var exp_val: f64 = 1.5e-10;
// type inference
var f1: f32 = 1.0_f32;
var f2: f64 = 1.0;
// invalid, type conflict
// var f3: f64 = 1.0_f32;
// f4 is of type f32
var f4 = 1.0_f32;
// f5 is of type f64, which is default
var f5 = 1.0;
Character Literal:
Character literals represent Unicode code points.
var letter: Rune = 'A'; // Character literals are of type `Rune`
var newline: Rune = '\n';
var unicode_char: Rune = '中';
var hex_char: Rune = '\x41'; // 'A'
String Literal:
var simple: String = "Hello, World!";
var escaped: String = "Line1\nLine2\tTabbed";
var unicode_str: String = "中文测试🎉";
Bool Literal:
var t:bool = true;
var f:bool = false;
Array Literal:
var arr = [5]i32{1, 2, 3, 4, 5};
var zeroed = [0]i32{};
var nested = [2][3]i32{
[3]i32{1, 2, 3},
[3]i32{4, 5, 6},
};
var nested_zeroed = [2][3]i32{};
Struct Literal:
var point = Point{x = 1.0, y = 2.0};
// All fields zero-initialized
var zero = Point{};
Union Literal:
// struct-like initialize
var v2 = Value{};
// variant-like initialize
var v1 = Value::as_i32(12345);
Enum Literal:
var color = Color::Red;
Variant Literal:
var some = Option<i32>::Some(42);
var none = Option<i32>::None;
Operators are listed from lowest to highest precedence:
| Precedence | Operators | Associativity |
|---|---|---|
| 1 (lowest) | ?: (ternary) |
Right |
| 2 | ` | |
| 3 | && |
Left |
| 4 | ` | ` |
| 5 | ^ |
Left |
| 6 | & |
Left |
| 7 | == != |
Left |
| 8 | < > <= >= |
Left |
| 9 | << >> |
Left |
| 10 | + - |
Left |
| 11 | * / % |
Left |
| 12 (highest) | - + ! ~ & * (unary) |
Right |
| Operator | Description | Example |
|---|---|---|
+ |
Addition | a + b |
- |
Subtraction | a - b |
* |
Multiplication | a * b |
/ |
Division | a / b |
% |
Modulo | a % b |
| Operator | Description | Example |
|---|---|---|
== |
Equal | a == b |
!= |
Not equal | a != b |
< |
Less than | a < b |
> |
Greater than | a > b |
<= |
Less or equal | a <= b |
>= |
Greater or equal | a >= b |
| Operator | Description | Example |
|---|---|---|
&& |
Logical AND | a && b |
| ` | ` | |
! |
Logical NOT | !a |
| Operator | Description | Example |
|---|---|---|
& |
Bitwise AND | a & b |
| ` | ` | Bitwise OR |
^ |
Bitwise XOR | a ^ b |
~ |
Bitwise NOT | ~a |
<< |
Left shift | a << n |
>> |
Right shift | a >> n |
| Operator | Description | Example |
|---|---|---|
& |
Take address | &value |
* |
Dereference | *ptr |
var max = a > b ? a : b;
var sign = x > 0 ? 1 : (x < 0 ? -1 : 0);
var x = point.x;
var len = point.length();
var elem = arr[0];
var result = add(1, 2);
var generic = identity<i32>(42);
;
{
var x = 10;
// other statements
}
Rules:
- Variables declared inside a block are scoped to that block
var count = 0_i32;
var name = "Solid";
var point = Point{x = 1.0, y = 2.0};
Rules:
vardeclarations can only appear inside function bodies, not at the top level- The initialization is required
const MAX_SIZE = 1024u32; // Top-level constant
const PI = 3.14159265359;
func demo() {
const local_flag = check_condition(); // Local constant
}
Rules:
- Top-level
const: stored in.rodatasegment, initializer must be compile-time evaluatable - Local
const(inside function): stored on the stack, initializer can be runtime-evaluated
static global_counter = 0;
func increment() {
static local_counter = 0; // Function-local static
local_counter += 1;
}
Rules:
staticdeclares mutable variables with static storage duration- Can be declared at top level or inside function bodies
- Initializer must be compile-time evaluatable
- Stored in
.datasegment (initialized) or.bsssegment (zero-initialized) - Initialized once at program startup
Assignment Operators
| Operator | Description | Example |
|---|---|---|
= |
Simple assignment | a = b |
+= |
Add and assign | a += b |
-= |
Subtract and assign | a -= b |
*= |
Multiply and assign | a *= b |
/= |
Divide and assign | a /= b |
%= |
Modulo and assign | a %= b |
&= |
AND and assign | a &= b |
|= |
OR and assign | a |= b |
^= |
XOR and assign | a ^= b |
<<= |
Left shift and assign | a <<= b |
>>= |
Right shift and assign | a >>= b |
Rules:
-
Left operand must be a mutable lvalue
-
Compound assignments are not syntactic sugar. The left operand is evaluated only once.
For example,
a.get_instance() += bcallsget_instance()once, whilea.get_instance() = a.get_instance() + bcalls it twice
add(1, 2);
io::println("Hello");
var file = File::open("data.txt");
defer file.dispose();
Multiple Operations:
defer {
buffer.dispose();
file.dispose();
}
Rules:
- Deferred statements execute when leaving the current scope
- Multiple defers execute in LIFO order (last registered, first executed)
- Executes on both normal exit and early return
- Supports the following statement types:
;(empty statement){ ... }(body statement)a = b;(assignment statement)foo();(expression statement)if ... { }(if statement)for ... { }(for statement)switch ... { }(switch statement)
- The following are excluded because their semantics conflict with deferred execution:
return— return value would conflict with the enclosing function's returnbreak/continue— loop control flow is not meaningful at scope exitvar/const/static— declaration with deferred initialization has ambiguous storage semanticsdefer— nested defer directly under defer reduces readability; usedefer { defer ... }instead
if a > b {
io::println("a is greater");
} else if a < b {
io::println("a is less");
} else {
io::println("equal");
}
while {
// statements
if should_exit() { break; }
}
while i < 10 {
io::print(i);
i += 1;
}
for var i = 0; i < 10; i += 1 {
io::print(i);
}
for i: usize = 0; i < 10; i += 1 {
io::print(i);
}
When the condition expression is omitted, it defaults to true:
for var i = 0; ; i += 1 { // Infinite loop with init and increment
if i >= 10 { break; }
}
for ;; { // Infinite loop, equivalent to `while { }`
if should_exit() { break; }
}
var a = 2;
switch a {
1 => io::println("one");
2 => io::println("two");
else => io::println("other");
}
Enum Matching:
var color = Color::Red;
switch color {
Color::Red => io::println("red");
Color::Green => io::println("green");
Color::Blue => io::println("blue");
else => io::println("transparent or unknown");
}
Variant Matching with Binding:
var result = Result<i32, String>::Ok(42);
switch result {
Result<i32, String>::Ok(value) => io::println(value);
Result<i32, String>::Error(msg) => io::println(msg);
}
Rules:
- Exhaustiveness: Must either match all possible values or have an
elsearm elsearm handles all unmatched cases- No fall-through; each arm executes exactly one statement
while {
if should_exit() { break; }
}
for var i = 0; i < 10; i += 1 {
if i % 2 == 0 { continue; }
io::print(i);
}
func add(a: i32, b: i32): i32 {
return a + b;
}
func greet(name: String) {
io::println(name);
return; // Optional for functions without return value
}
Syntax:
[ct_annotates] [namespace_prefix] func <name> [generic_params] (parameters) [calling_convention]:return_type [where_clauses] body
// with return value
func add(a: i32, b: i32): i32 {
return a + b;
}
// without return value
func greet(name: String) {
io::println(name);
}
Function overloading is allowed only when parameter types differ. Overloading based solely on return type is prohibited.
Valid:
func parse(value: String): i32 { ... }
func parse(value: i32): String { ... }
Invalid:
// invalid
// func parse(value: String): i32 { ... }
// func parse(value: String): i64 { ... }
Calling conventions are placed after the parameter list:
func stdcall_func(a: i32, b: i32)stdcall: i32 {
return a + b;
}
| Convention | Description |
|---|---|
cdecl |
C calling convention (default) |
stdcall |
Windows API calling convention |
Rules:
- Functions annotated with
@exportor@importmust explicitly specify calling convention and must not be generic (no generic parameters) - Calling convention is part of the function signature
Methods are functions associated with a type, defined using TypeName::method_name syntax. The :: in the function name distinguishes a method from a free function.
struct Vector2{
x: f32,
y: f32,
}
// func under type scope
func Vector2::negate(self: Vector2){
return Vector2{x = -self.x, y = -self.y};
}
struct Slice<T>{
ptr: *T,
len: usize,
}
// func under type scope with generic parameter
func Slice<T>::subslice(self: Slice<T>, start: usize, len: usize): Result<Slice<T>, Error>{
if start + len > self.len {
return Result<Slice<T>,Error>::Fault(Error::OutOfRange);
}
return Result<Slice<T>,Error>::Okay(Slice<T>{ptr = self.ptr + start, len = len});
}
// type static method
func Slice<T>::new(ptr: *T, len: usize):Slice<T>{
return Slice<T>{ptr = ptr, len = len};
}
func negate(value: i32): i32{
if value > 0 {
return - value;
}else{
return value;
}
}
func main():i32{
var b = negate(100);
// call function with full qualified name
var a = std::math::abs(-1.0);
var v = Vector2<f32>::Identity;
// method calling, below two ways are equivalent
var dot1 = v.Distance();
var dot2 = Vector2<f32>::Distance(v);
}
struct Box<T> where T: ICopy<T> {
value: T,
}
union Either<TLeft, TRight> {
left: TLeft,
right: TRight,
}
variant Result<TValue, TError>: u8 {
Ok: TValue,
Error: TError,
}
Note: Enums do not support generics because enums are fundamentally integer types with named constants. For generic tagged unions, use variant instead.
func identity<T>(value: T): T where T: ICopy<T> {
return value;
}
func max<T>(a: T, b: T): T where T: IComparable<T> {
if a.compare(b) >= 0 {
return a;
}
return b;
}
struct Box<T> where T: ICopy<T> {
value: T,
}
func Box<T>::value(self: Box<T>):T{
return T;
}
func Box<T>::new(value: T): Box<T>{
return Box{value = value};
}
Generic types must be explicitly instantiated:
var box = Box<i32>{value = 42};
var opt = Option<i32>::Some(42);
Interfaces define contracts for types. They cannot be instantiated and do not support inheritance.
Interfaces with generic type parameters:
interface IDrawable<T> {
func draw(self: *T);
}
interface IAdd<TLeft, TRight, TResult> {
func add(left: TLeft, right: TRight): TResult;
}
interface IResetable {
func reset();
}
interface ICounter {
func increment();
func get(): i32;
}
interface IConvert<T> {
func convert<U>(value: T): U
where U: ICopy<U>;
}
Use chain-style where clauses for multiple constraints:
struct HashMap<TKey, TValue>
where TKey: IEqual<TKey>
where TKey: IHash<TKey>
where TKey: ICopy<TKey> {
buckets: Vec<Entry<TKey, TValue>>,
size: usize,
}
interface IComparable<T> where T: IEqual<T> {
func compare(left: T, right: T): i8;
}
interface IMap<TContainer, TKey, TValue, TIter>
where TKey: IEqual<TKey>
where TKey: IHash<TKey>
where TIter: IIterator<TIter, (TKey, TValue)> {
func get(self: *TContainer, key: TKey): Option<TValue>;
func insert(self: *TContainer, key: TKey, value: TValue): Option<TValue>;
func remove(self: *TContainer, key: TKey): Option<TValue>;
func contains(self: *TContainer, key: TKey): bool;
func len(self: *TContainer): usize;
func iter(self: *TContainer): TIter;
}
Operator overloading is implemented through interfaces.
interface IAdd<TLeft, TRight, TResult> {
func add(left: TLeft, right: TRight): TResult;
}
@implements(IAdd<Vector2, Vector2, Vector2>)
@implements(IAdd<f32, Vector2, Vector2>)
@implements(IAdd<Vector2, f32, Vector2>)
struct Vector2 {
x: f32,
y: f32,
}
func Vector2::add(left: Vector2, right: Vector2): Vector2 {
return Vector2{x = left.x + right.x, y = left.y + right.y};
}
Available Operator Interfaces:
| Interface | Operator | Description |
|---|---|---|
IAffirm<TValue, TResult> |
+ (unary) |
Unary plus |
INegate<TValue, TResult> |
- (unary) |
Unary negation |
IBitNot<TValue, TResult> |
~ |
Bitwise NOT |
IAdd<TLeft, TRight, TResult> |
+ |
Addition |
ISubtract<TLeft, TRight, TResult> |
- |
Subtraction |
IMultiply<TLeft, TRight, TResult> |
* |
Multiplication |
IDivide<TLeft, TRight, TResult> |
/ |
Division |
IMod<TLeft, TRight, TResult> |
% |
Modulo |
IShiftLeft<TLeft, TRight, TResult> |
<< |
Left shift |
IShiftRight<TLeft, TRight, TResult> |
>> |
Right shift |
IBitAnd<TLeft, TRight, TResult> |
& |
Bitwise AND |
IBitOr<TLeft, TRight, TResult> |
` | ` |
IBitXor<TLeft, TRight, TResult> |
^ |
Bitwise XOR |
IEqual<T> |
== != |
Equality |
IComparable<T> |
< > <= >= |
Comparison |
IIndexGet<TCollection, TIndex, TValue> |
[] (read) |
Index read |
IIndexSet<TCollection, TIndex, TValue> |
[] (write) |
Index write |
Use @implements annotation on the struct declaration:
@implements(IEqual<Point>)
@implements(IAdd<Point, Point, Point>)
struct Point {
x: f32,
y: f32,
}
func Point::equal(left: Point, right: Point): bool {
return left.x == right.x && left.y == right.y;
}
func Point::add(left: Point, right: Point): Point {
return Point{x = left.x + right.x, y = left.y + right.y};
}
solid-lang provides compile-time annotations and compile-time operators.
Annotations modify compilation behavior without changing syntax structure.
| Annotation | Applies To | Description |
|---|---|---|
@public |
Declarations | Visible across projects |
@private |
Struct fields | Visible only within struct |
@internal |
Declarations | Visible within project |
@import(symbol) |
Functions | Import external function |
@export |
Functions | Export for external use |
@implements(interface) |
Types | Declare interface implementation |
@intrinsic |
Types, Functions | Compiler-implemented |
@inline |
Functions | Suggest inlining |
@align(N) |
Types | Specify alignment |
@packed |
Types | Packed memory layout |
@flags |
Enums | Mark as bit flags |
@sealed |
Types | Prevents direct struct literal construction; instances must be created through designated constructors or factory methods |
Operators evaluated at compile time and replaced with constant values.
| Operator | Type | Description |
|---|---|---|
@is_debug |
bool |
True in debug build |
@is_release |
bool |
True in release build |
@is_os(name) |
bool |
True if target OS matches |
@is_arch(name) |
bool |
True if target architecture matches |
@is_endian(name) |
bool |
True if endianness matches |
@sizeof(T) |
usize |
Size of type in bytes |
@offsetof(T, field) |
usize |
Offset of field in bytes |
@alignof(T) |
usize |
Alignment requirement |
Usage:
struct Vector3 {
x: f32,
y: f32,
z: f32,
}
const VEC3_SIZE = @sizeof(Vector3); // 12 bytes
const VEC3_ALIGN = @alignof(Vector3); // 4 bytes
const VEC3_Y_OFFSET = @offsetof(Vector3, y); // 4 bytes
func main():i32{
if @is_debug {
io::println("Debug mode enabled");
}
if @is_os(linux) {
io::println("Running on Linux");
} else if @is_os(windows) {
io::println("Running on Windows");
}
if @is_arch(x86_64) {
io::println("x86_64 architecture");
}
if @is_endian(little) {
io::println("Little endian");
}
}
Compile-time evaluatable expressions are required in the following contexts:
- Array sizes (e.g.,
[N]i32) - Top-level
constinitializers - Top-level
staticinitializers - Function-local
staticinitializers
A compile-time evaluatable expression may only contain:
- Literals (integer, float, string, char, bool)
- Other top-level
constvalues - Arithmetic, comparison, logical, and bitwise operators applied to compile-time evaluatable operands
The following are NOT allowed in compile-time evaluatable expressions:
- Function calls (including method calls)
- Control flow (
if,switch,for) - Runtime variable access (
var) - Pointer operations
Dependency Rules:
- A top-level
constmay reference other top-levelconstvalues - The dependency graph formed by such references must be a Directed Acyclic Graph (DAG)
- Cyclic dependencies between
constvalues result in a compilation error
Note: Function-local const declarations are stored on the stack and may use runtime-evaluated initializers. They are not subject to compile-time evaluation constraints.
A project is a directory. All .solid files in the directory and its subdirectories constitute the project. The compiler recursively scans all source files for compilation.
Each source file follows this structure:
namespace <namespace_path>;
using <namespace_path>*
<top_level_declaration>*
Rules:
- The
namespacedeclaration is required and must be unique in a file - The
namespacedeclaration must appear at the beginning of the file usingdeclarations must follow thenamespacedeclaration- All top-level declarations in a file belong to the namespace declared in that file
Example:
namespace core::math;
using core::types;
const PI = 3.14159265359;
struct Vector3 {
x: f32,
y: f32,
z: f32,
}
func length(v: Vector3): f32 {
return (v.x * v.x + v.y * v.y + v.z * v.z).sqrt();
}
Cross-File Declaration:
The same namespace can be declared across multiple files. All declarations within the same namespace are merged during compilation.
Symbol Uniqueness:
Within a single namespace, all top-level symbols (functions, named types, constants, static variables) must be unique. Duplicate symbol names result in a compilation error.
Visibility Within Namespace:
Symbols declared within the same namespace can reference each other without a namespace prefix.
// File: vector.solid
namespace core::math;
struct Vector3 {
x: f32,
y: f32,
z: f32,
}
// File: operations.solid
namespace core::math;
// Vector3 is directly visible without prefix
func zero(): Vector3 {
return Vector3{x = 0.0, y = 0.0, z = 0.0};
}
Purpose:
The using declaration imports symbols from another namespace, allowing them to be used without a namespace prefix.
Non-Recursive:
using is non-recursive. using A does not automatically import symbols from A::B.
namespace app;
using core::math; // Imports symbols from core::math only
// using core::math does NOT imply using core::math::internal
Symbol Conflict Resolution:
If the same symbol name exists in multiple namespaces (including the current namespace and any using-imported namespaces), the fully qualified name must be used. Otherwise, a compilation error occurs.
namespace app;
using core::math;
using graphics::math;
// Error: ambiguous, both core::math::Vector3 and graphics::math::Vector3 exist
var v = Vector3{};
// Correct: use fully qualified name
var v1 = core::math::Vector3{};
var v2 = graphics::math::Vector3{};
The program entry point is the main function. Only one main function should exist in the entire program.
File: main.solid
namespace app;
func main(args: Slice<String>): i32 {
// with command-line arguments
return 0;
}
File: main_simple.solid
namespace app;
func main(): i32 {
// without command-line arguments
return 0;
}
Valid main Signatures:
| Signature | Description |
|---|---|
func main(args: Slice<String>): i32 |
With arguments and explicit return |
func main(): i32 |
Without arguments, with explicit return |
The following declarations are allowed at the top level:
| Declaration | Description |
|---|---|
const |
Compile-time constants |
static |
Static storage duration variables |
struct |
Structure type definitions |
union |
Union type definitions |
enum |
Enumeration type definitions |
variant |
Tagged union type definitions |
interface |
Interface definitions |
func |
Function definitions |
Functions can be declared without a body for FFI:
@import(malloc)
func libc_malloc(size: usize)cdecl: RawPtr;
func forward_declared(x: i32): i32;
Use @import to declare external functions. The annotation takes the external function's symbol name directly (as an identifier, not a string):
@import(free)
func libc_free(ptr: RawPtr)cdecl;
@import(memset)
func libc_memset(ptr: RawPtr, value: i32, size: usize)cdecl: RawPtr;
// Windows API with @os annotation
@import(VirtualAlloc)
@os(windows)
func VirtualAlloc(addr: usize, size: usize, alloc_type: u32, protect: u32)stdcall: usize;
// POSIX
@import(mmap)
@os(linux)
func mmap(addr: usize, len: usize, prot: i32, flags: i32, fd: i32, offset: isize)cdecl: usize;
Rules:
@importtakes the symbol name (identifier), not a library name string- The internal function name can differ from the external symbol name
- Use
@os(name)to specify platform-specific imports @importfunctions cannot be generic and must specify a calling convention
Use @export to make functions available externally:
@export
func add(a: i32, b: i32)cdecl: i32 {
return a + b;
}
@export
func process_callback(data: RawPtr)stdcall {
// Implementation
}
Rules:
@exportfunctions cannot be generic and must specify a calling convention
solid-lang types map to C types as follows:
| solid-lang Type | C Type |
|---|---|
i8 |
int8_t / signed char |
u8 |
uint8_t / unsigned char |
i16 |
int16_t / short |
u16 |
uint16_t / unsigned short |
i32 |
int32_t / int |
u32 |
uint32_t / unsigned int |
i64 |
int64_t / long long |
u64 |
uint64_t / unsigned long long |
f32 |
float |
f64 |
double |
usize |
size_t |
bool |
_Bool / int |
RawPtr |
void* |
*T |
T* |
func(...)T |
Function pointer |
solid-lang structs are binary-compatible with C structs (natural alignment by default):
// Solid
struct Point {
x: f32,
y: f32,
}
// Equivalent C
struct Point {
float x;
float y;
};