Getting Started
This guide walks you through installing SafeC, compiling your first program, and understanding the basic compilation workflow.
Installation
Run the install script. It downloads a prebuilt safec/safeguard/sc-lsp release from GitHub Releases and configures your shell environment — there is nothing to build. It does need clang 21 or newer on the machine (SafeC hands its LLVM IR to clang to compile and link, and clang imports C headers); Install says how to get one per platform.
curl -fsSL https://raw.githubusercontent.com/safec-org/SafeC/main/install.sh | bash
irm https://raw.githubusercontent.com/safec-org/SafeC/main/install.ps1 | iex
Install Script Options
| Option | Description |
|---|---|
--prefix=<path> |
Install directory (default: ~/safec) |
--version=<tag> |
Release tag to install, e.g. v1.0.0-beta-7 (default: the newest, pre-releases included) |
--std=<sections> |
The prebuilt std sections to install, e.g. core,collections,gui (default: all). std's sources are always installed; a section that isn't is compiled from source when a build needs it |
--std=+<sections> |
Add prebuilt std sections to an existing install, from the release it came from |
--skip-env |
Skip shell environment configuration |
install.ps1 takes the same as -Prefix, -Version, -Std and -SkipEnv.
Published binaries: macOS arm64, Linux x86_64, Windows x86_64. See Releases for the full asset list. They need:
- macOS 11 (Big Sur) or newer: the binaries and the prebuilt std are built for 11.0.
- Linux with glibc 2.25 or newer (Debian 10, Ubuntu 18.04, RHEL 8 and later): the binaries are built against an older glibc and checked for it, and the prebuilt std calls nothing newer than 2.25 (
getrandom). Musl-based distributions (Alpine) aren't supported by the release binaries.
TIP
Building from source (needed for a platform the release workflow doesn't publish, or to build from an unreleased commit) is still possible via each component's own CMakeLists.txt under compiler/, safeguard/, and the sibling sc-language-server repo — see Install → From source and Compiler Architecture.
Verify the install:
safec --version # safec 1.0.0-beta-7
safeguard --version # safeguard 1.0.0-beta-7
Your First Program
Create a new project with safeguard:
safeguard new hello
cd hello
This generates the following structure:
hello/
.gitignore
Package.toml
src/
main.sc
Replace src/main.sc with:
extern int printf(const char* fmt, ...);
int main() {
printf("Hello from SafeC!\n");
return 0;
}
Run in PlaygroundBuild and run:
safeguard build
safeguard run
Output:
Hello from SafeC!
A More Complete Example
This example demonstrates regions, references, arrays, and slices:
extern int printf(const char* fmt, ...);
int main() {
// Stack variable with explicit region reference
int x = 42;
&stack int ref = &x;
printf("x = %d\n", *ref);
// Array with bounds checking
int arr[5];
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
// Slice — a bounds-checked view into the array
[]int s = arr[0..3];
printf("slice length = %ld\n", s.len);
return 0;
}
Run in PlaygroundCompilation Pipeline
The SafeC compiler processes source files through a multi-stage pipeline:
Source (.sc)
|
v
Preprocessor #define, #include, #ifdef, -D/-I flags
|
v
Lexer Tokenization
|
v
Parser Recursive-descent parsing -> AST
|
v
Sema Type checking, region analysis, borrow checking
|
v
ConstEval Compile-time function evaluation, static_assert
|
v
CodeGen LLVM IR generation
|
v
LLVM IR (.ll) Text IR or bitcode output
|
v
clang/lld Link to native binary
Compiler Flags
safec <input.sc> [input2.sc ...] [options]
Every flag safec accepts is below (safec --help also lists LLVM's own options, which SafeC doesn't need).
Output
| Flag | Description |
|---|---|
-o <file> |
Output file path (default -, standard output). With several inputs and no --emit-bin, a directory that gets one output per input; with several inputs and --emit-bin, the one linked binary |
--emit-llvm |
Emit LLVM IR as text (.ll); without it the output is LLVM bitcode |
--emit-bin |
Compile and link a native binary or library straight to -o, through the system clang (see Linking) |
-j <N> |
Parallel compile jobs for several input files (default: the number of hardware threads) |
Optimization and Linking
| Flag | Description |
|---|---|
-O <level> |
Optimization level: 0 (default), 1, 2, 3, s, z |
--release |
Release profile: -O2 unless -O is given |
-l <name> |
Link against library name (--emit-bin only) |
-L <dir> |
Add a library search directory for -l (--emit-bin only) |
--shared |
With --emit-bin, produce a shared library instead of an executable |
--static-lib |
With --emit-bin, produce a static library (.a, via ar) |
--lto[=thin|full] |
With --emit-bin, link-time optimization (thin when no value is given) |
Targets
| Flag | Description |
|---|---|
--target=<alias|triple> |
Cross-compile: a bare-metal alias (arm-7em, rv32i, wasm32, ...) or any LLVM triple (default: the host) |
--target-cpu=<name> |
Target CPU (default: the triple's generic CPU) |
--target-features=<+a,-b> |
Target features, LLVM -mattr syntax |
--target-abi=<name> |
Target ABI (ilp32e, lp64e, ...) |
--freestanding |
Freestanding mode: no hosted runtime and no C header import (implied by the bare-metal aliases) |
--provides=<fn,...> |
Functions another translation unit defines (e.g. a bare-metal runtime's malloc), so calling them gives no freestanding warning |
Debug and Diagnostics
| Flag | Description |
|---|---|
--dump-ast |
Print the AST and exit |
--dump-pp |
Print preprocessed source and exit |
--g lines |
Emit line-table debug info (DWARF) |
--g full |
Emit full debug info with local variables |
-v |
Verbose output |
Compilation Control
| Flag | Description |
|---|---|
--no-sema |
Skip semantic analysis |
--no-consteval |
Skip compile-time evaluation pass |
--compat-preprocessor |
Enable full C preprocessor compatibility (function-like macros, token pasting, stringification — see Preprocessor) |
--no-import-c-headers |
Disable automatic C header import |
--no-strict-aliasing |
Don't emit type-based alias info, for unsafe code that type-puns through pointer casts (see Strict Aliasing) |
Preprocessor
| Flag | Description |
|---|---|
-I <dir> |
Add include search path |
-D NAME[=VALUE] |
Define a preprocessor macro |
Incremental Compilation
Incremental compilation (per-file .bc caching) is on by default — no flag needed to enable it.
| Flag | Description |
|---|---|
--no-incremental |
Disable incremental compilation (always recompile from scratch) |
--cache-dir <dir> |
Set cache directory (default: .safec_cache) |
--clear-cache |
Clear all cached .bc files and exit |
Documentation
What safeguard doc runs to read a package's declarations:
| Flag | Description |
|---|---|
--emit-doc-json |
Parse the inputs (no semantic analysis or codegen) and write their declarations and doc comments as JSON |
--doc-root <dir> |
Document only files under dir (repeatable); paths in the output are relative to it |
--doc-extern <name>=<dir> |
List declarations under dir as items of the external package name, for links into its reference |
--doc-private |
Include static functions and globals |
Information
These need no input file:
| Flag | Prints |
|---|---|
--version |
The compiler version |
--list-targets |
The bare-metal target aliases with their triples and ABIs |
--print-target-clang-flags --target=<t> |
The clang flags that compile and link IR for that target |
--print-clang |
The clang safec hands its IR to (SAFEC_CLANG overrides it) |
C Header Import
SafeC can natively include C standard headers. The compiler parses C header declarations and makes them available without manual extern stubs:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
?&char msg = malloc(64); // raw void* -> ?&char: implicit
match (msg) {
case some(m): {
strcpy(m, "Hello from C headers!");
printf("%s\n", m);
}
case null: { }
}
free(msg);
return 0;
}
Run in PlaygroundThe automatic import works by invoking clang -ast-dump=json on the included header and extracting function and typedef declarations. Disable it with --no-import-c-headers if you prefer manual extern declarations.
Using the Package Manager
For projects with dependencies or multiple source files, use safeguard. The install script installs it alongside safec and sc-lsp, so it's already available.
# Create a new project
safeguard new myproject
cd myproject
# Build and run
safeguard build
safeguard run
The project structure created by safeguard new:
myproject/
.gitignore
Package.toml # Project manifest
src/
main.sc # Entry point
The install script already sets SAFEC_HOME (to the install prefix, ~/safec by default) and adds it to PATH so safeguard can locate the compiler and standard library automatically — restart your shell, or source your shell rc file, after installing. If you skipped that step (--skip-env) or moved the install elsewhere, set it manually:
export SAFEC_HOME=/path/to/safec
export PATH="$SAFEC_HOME/bin:$PATH"
Linking
SafeC compiles to LLVM IR. safec --emit-bin -o hello links it in one step through clang (Linking), and safeguard links a project for you; by hand, the final linking step uses your system's C toolchain:
# Basic linking
clang hello.ll -o hello
# With pthreads (for spawn/join)
clang concurrency.ll -lpthread -o concurrency
# With math library
clang math_demo.ll -lm -o math_demo
Editor Support
An LSP server is available for .sc files, providing:
- Diagnostics (error highlighting)
- Hover information
- Code completion
- Go-to-definition
- Document symbols
The server is sc-lsp, which the install script puts on your PATH; Tools has the VS Code, Neovim and Emacs setups (the VS Code extension is in the editors/vscode/ directory of the sc-language-server repository).
Next Steps
- Design Philosophy — Understand why SafeC makes the choices it does
- Types — Learn about SafeC's type system
- Memory and Regions — Understand region-based memory safety