Skip to content

Functions

SafeC supports several function forms, from regular C-compatible functions to compile-time evaluated functions, struct methods, and generic functions.

Regular Functions

c
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.

c
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 runtime

Consteval Functions

Functions marked consteval must be executed at compile time. Calling them with non-constant arguments is a compilation error.

c
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 context

constinit Globals

Every global variable's initializer must already be a compile-time constant expression — this is enforced unconditionally, as a hard compiler error, not merely when the optional constinit keyword is present:

c
int table_size() { return 256; }
int bad = table_size();   // ERROR: not a compile-time constant expression
int ok  = 256;             // fine

constinit is an optional, purely self-documenting annotation — writing it changes nothing about how the initializer is checked, since every global is already held to the same standard. It exists for readability (matching C++'s constinit keyword) and to make the intent explicit at the declaration site rather than relying on the reader to know the rule:

c
constinit int table_size = 256;

This is a deliberate hardening over what plain C or C++ allow: C++'s constinit only matters because C++ permits dynamic (runtime) initialization of globals by default and constinit opts a specific declaration out of that; SafeC never permits dynamic global initialization at all, so there's no silent fallback to guard against — a non-constant initializer is caught at compile time everywhere, with or without the keyword.

Pure Functions

Functions marked pure guarantee no side effects. The compiler lowers them with LLVM readonly and nounwind attributes, enabling aggressive optimization.

c
pure double square(double x) {
    return x * x;
}

Pure functions may not:

  • Modify global state
  • Perform I/O
  • Call non-pure functions

See Functional Programming for what this buys you beyond the optimization hint.

Must-Use Functions

The must_use keyword causes a compiler warning if the return value is discarded:

c
must_use int compute(int x) {
    return x * x + 1;
}

compute(5);                    // WARNING: return value discarded
int result = compute(5);       // OK

Noreturn Functions

Functions that never return (e.g., abort, exit, infinite loops) can be annotated with noreturn:

c
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

c
struct Point {
    double x;
    double y;

    double length() const;
    void scale(double s);
};

Definition

c
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

c
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 signatureLowered C signature
double Point::length() constdouble 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.

c
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:

c
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.

See Polymorphism & OOP for how this fits alongside generics, traits, and fn_eval as one of several distinct polymorphism mechanisms.

Generic Functions

Generic functions use the generic<T> syntax and are fully monomorphized at compile time.

Basic Generic

c
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:

c
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_double

Generic 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.

c
naked void isr_handler() {
    asm volatile ("iret");
}

Interrupt Functions

Interrupt functions use the ISR calling convention. They must be void(void):

c
interrupt void timer_isr() {
    // handle timer interrupt
}

Function Pointers

Functions can be referenced as values using the fn type syntax:

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

See Functional Programming for a larger higher-order-function example (generic map/fold helpers), and Polymorphism & OOP for using a struct field of fn type plus void* to build explicit runtime dispatch — SafeC's equivalent of a hand-written vtable.

Released under the MIT License.