Functions
SafeC supports several function forms, from regular C-compatible functions to compile-time evaluated functions, struct methods, and generic functions.
Regular Functions
int add(int a, int b) {
return a + b;
}
void greet(const char *name) {
printf("Hello, %s\n", name);
}Const Functions
Functions marked const may be executed at compile time if all arguments are known constants. They can also be called at runtime.
const int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
const int val = factorial(5); // evaluated at compile time → 120
int dynamic = factorial(x); // called at runtimeConsteval Functions
Functions marked consteval must be executed at compile time. Calling them with non-constant arguments is a compilation error.
consteval int table_size() {
return 256;
}
int lookup[table_size()]; // OK: compile-time constant
// int bad = table_size(); // ERROR if result not used in const contextPure Functions
Functions marked pure guarantee no side effects. The compiler lowers them with LLVM readonly and nounwind attributes, enabling aggressive optimization.
pure double square(double x) {
return x * x;
}Pure functions may not:
- Modify global state
- Perform I/O
- Call non-pure functions
Must-Use Functions
The must_use keyword causes a compiler warning if the return value is discarded:
must_use int compute(int x) {
return x * x + 1;
}
compute(5); // WARNING: return value discarded
int result = compute(5); // OKNoreturn Functions
Functions that never return (e.g., abort, exit, infinite loops) can be annotated with noreturn:
noreturn void panic(const char *msg) {
printf("PANIC: %s\n", msg);
abort();
}Struct Methods
Methods are declared inside a struct body and defined outside using T::method() qualified syntax. The self parameter is implicit.
Declaration
struct Point {
double x;
double y;
double length() const;
void scale(double s);
};Definition
double Point::length() const {
return self.x * self.x + self.y * self.y;
}
void Point::scale(double s) {
self.x = self.x * s;
self.y = self.y * s;
}Calling Methods
Point p = {3.0, 4.0};
double len = p.length(); // calls Point_length(&p)
p.scale(2.0); // calls Point_scale(&p, 2.0)Lowering
Methods are lowered to plain functions with an explicit self pointer:
| SafeC signature | Lowered C signature |
|---|---|
double Point::length() const | double Point_length(const Point* self) |
void Point::scale(double s) | void Point_scale(Point* self, double s) |
Const methods receive a const T* self pointer; non-const methods receive a T* self pointer. Inside the method body, self is typed as &stack T.
Operator Overloading
Struct types can overload binary operators by defining methods named operator+, operator-, etc.
struct Vec2 {
double x;
double y;
Vec2 operator+(Vec2 other) const;
Vec2 operator-(Vec2 other) const;
Vec2 operator*(double s) const;
};
Vec2 Vec2::operator+(Vec2 other) const {
Vec2 result;
result.x = self.x + other.x;
result.y = self.y + other.y;
return result;
}Usage:
Vec2 a = {1.0, 2.0};
Vec2 b = {3.0, 4.0};
Vec2 c = a + b; // calls Vec2_operator+(a, b)Supported overloadable operators: +, -, *, /, %, ==, !=, <, >, <=, >=.
Operator methods are mangled as TypeName_operator+ etc. in the generated code.
Generic Functions
Generic functions use the generic<T> syntax and are fully monomorphized at compile time.
Basic Generic
generic<T>
T max(T a, T b) {
if (a > b) return a;
return b;
}
int m1 = max(3, 7); // instantiates max<int>
double m2 = max(1.5, 2.7); // instantiates max<double>Constrained Generic
Type parameters can be constrained with traits:
generic<T: Numeric>
T clamp(T val, T lo, T hi) {
if (val < lo) return lo;
if (val > hi) return hi;
return val;
}Monomorphization
The compiler deep-clones the function body for each concrete type instantiation, substituting type parameters with concrete types. The mangled name follows the pattern __safec_fn_type:
max<int> → __safec_max_int
max<double> → __safec_max_doubleGeneric bodies are skipped during the first semantic analysis pass. Type inference determines T from argument types at call sites.
Naked Functions
Naked functions have no compiler-generated prologue or epilogue. The body must consist entirely of inline assembly.
naked void isr_handler() {
asm volatile ("iret");
}Interrupt Functions
Interrupt functions use the ISR calling convention. They must be void(void):
interrupt void timer_isr() {
// handle timer interrupt
}Function Pointers
Functions can be referenced as values using the fn type syntax:
fn int(int, int) op = add;
int result = op(3, 4); // calls add(3, 4)
// Higher-order functions
int apply(fn int(int) f, int x) {
return f(x);
}