Skip to content

Latest commit

 

History

History
1677 lines (1221 loc) · 41.4 KB

File metadata and controls

1677 lines (1221 loc) · 41.4 KB

The Solid Programming Language Specification

Language Version 0.1.1


Introduction

This is the specification of the Solid programming language, hereinafter referred to as solid-lang.

Language Orientation

  • High Performance
  • Systems Programming
  • A modern, uncompromising alternative to C
  • Manual & Explicit Memory Management

Language Philosophy

  • 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

Types

Primitive Types

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

  • bool size is platform dependent. In most cases, it will occupy 1 byte in memory.

Array Type

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

Struct Type

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

Union Type

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

Enum Type

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 : type specifies 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 = expr is omitted, the value auto-increments: the first field defaults to 0, each subsequent field defaults to the previous field's value plus 1
  • Empty enums are valid
  • Type equivalence: enums must have identical fully qualified names

Variant Type

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 : type specifies 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

Zero-Sized Types

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 &zst expressions 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

Pointer Type

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.

Function Pointer

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 null before 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_name obtains 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

Type Conversion

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 IInto or IFrom interfaces for custom conversions
  • Compiler provides built-in conversions for primitive types

Expressions

Literal Expressions

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

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

Arithmetic Operators

Operator Description Example
+ Addition a + b
- Subtraction a - b
* Multiplication a * b
/ Division a / b
% Modulo a % b

Comparison Operators

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

Logical Operators

Operator Description Example
&& Logical AND a && b
` `
! Logical NOT !a

Bitwise Operators

Operator Description Example
& Bitwise AND a & b
` ` Bitwise OR
^ Bitwise XOR a ^ b
~ Bitwise NOT ~a
<< Left shift a << n
>> Right shift a >> n

Pointer Operators

Operator Description Example
& Take address &value
* Dereference *ptr

Ternary Conditional Operator

var max = a > b ? a : b;
var sign = x > 0 ? 1 : (x < 0 ? -1 : 0);

Postfix Expressions

Field Access

var x = point.x;

Method Call

var len = point.length();

Index Expression

var elem = arr[0];

Function Call

var result = add(1, 2);
var generic = identity<i32>(42);

Statements

Empty Statement

;

Block Statement

{
    var x = 10;
    // other statements
}

Rules:

  • Variables declared inside a block are scoped to that block

Variable Declaration Statement

var count = 0_i32;
var name = "Solid";
var point = Point{x = 1.0, y = 2.0};

Rules:

  • var declarations can only appear inside function bodies, not at the top level
  • The initialization is required

Constant Declaration Statement

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 .rodata segment, initializer must be compile-time evaluatable
  • Local const (inside function): stored on the stack, initializer can be runtime-evaluated

Static Declaration Statement

static global_counter = 0;

func increment() {
    static local_counter = 0;  // Function-local static
    local_counter += 1;
}

Rules:

  • static declares mutable variables with static storage duration
  • Can be declared at top level or inside function bodies
  • Initializer must be compile-time evaluatable
  • Stored in .data segment (initialized) or .bss segment (zero-initialized)
  • Initialized once at program startup

Assignment Statement

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() += b calls get_instance() once, while a.get_instance() = a.get_instance() + b calls it twice

Expression Statement

add(1, 2);
io::println("Hello");

Defer Statement

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 return
    • break / continue — loop control flow is not meaningful at scope exit
    • var / const / static — declaration with deferred initialization has ambiguous storage semantics
    • defer — nested defer directly under defer reduces readability; use defer { defer ... } instead

If Statement

if a > b {
    io::println("a is greater");
} else if a < b {
    io::println("a is less");
} else {
    io::println("equal");
}

Infinite Loop Statement

while {
    // statements
    if should_exit() { break; }
}

Conditional Loop

while i < 10 {
    io::print(i);
    i += 1;
}

For Loop

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; }
}

Switch Statement

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 else arm
  • else arm handles all unmatched cases
  • No fall-through; each arm executes exactly one statement

7.12 Break Statement

while {
    if should_exit() { break; }
}

7.13 Continue Statement

for var i = 0; i < 10; i += 1 {
    if i % 2 == 0 { continue; }
    io::print(i);
}

7.14 Return Statement

func add(a: i32, b: i32): i32 {
    return a + b;
}

func greet(name: String) {
    io::println(name);
    return;  // Optional for functions without return value
}

Functions

Syntax:

[ct_annotates] [namespace_prefix] func <name> [generic_params] (parameters) [calling_convention]:return_type [where_clauses] body

Basic

// with return value
func add(a: i32, b: i32): i32 {
    return a + b;
}

// without return value
func greet(name: String) {
    io::println(name);
}

Overloading

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

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 @export or @import must explicitly specify calling convention and must not be generic (no generic parameters)
  • Calling convention is part of the function signature

Methods

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};
}

Calling

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);
}

Interface & Generics

Generics

Generic Types

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.

Generic Functions

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;
}

Generic Method

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 Instantiation

Generic types must be explicitly instantiated:

var box = Box<i32>{value = 42};
var opt = Option<i32>::Some(42);

Interface

Interfaces define contracts for types. They cannot be instantiated and do not support inheritance.


Generic Interface

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>;
}

Where Clauses

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

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

Interface Implementation

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};
}

Compile-Time

solid-lang provides compile-time annotations and compile-time operators.

Compile-Time Annotations

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

Compile-Time Operators

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 Evaluation

Compile-time evaluatable expressions are required in the following contexts:

  • Array sizes (e.g., [N]i32)
  • Top-level const initializers
  • Top-level static initializers
  • Function-local static initializers

A compile-time evaluatable expression may only contain:

  • Literals (integer, float, string, char, bool)
  • Other top-level const values
  • 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 const may reference other top-level const values
  • The dependency graph formed by such references must be a Directed Acyclic Graph (DAG)
  • Cyclic dependencies between const values 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.

Program Structure

Project

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.

File Organization

Each source file follows this structure:

namespace <namespace_path>;

using <namespace_path>*

<top_level_declaration>*

Rules:

  • The namespace declaration is required and must be unique in a file
  • The namespace declaration must appear at the beginning of the file
  • using declarations must follow the namespace declaration
  • 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();
}

Namespace

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};
}

Using

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{};

Entry Point

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

Top-Level Declarations

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

FFI

12.1 FFI (Foreign Function Interface)

Functions can be declared without a body for FFI:

@import(malloc)
func libc_malloc(size: usize)cdecl: RawPtr;

func forward_declared(x: i32): i32;

12.1.1 Importing External Functions

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:

  • @import takes 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
  • @import functions cannot be generic and must specify a calling convention

12.1.2 Exporting Functions

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:

  • @export functions cannot be generic and must specify a calling convention

12.1.3 Type Compatibility

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

12.1.4 Struct Compatibility

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;
};