c3-lang

star 1

C3 language, writing, debugging, or reviewing .c3 files; c3c builds; C3 syntax, stdlib, and tooling

ricardobeat By ricardobeat schedule Updated 6/9/2026

name: c3-lang description: C3 language, writing, debugging, or reviewing .c3 files; c3c builds; C3 syntax, stdlib, and tooling

C3 Programming Language

C3 is a systems language — an evolution over C with modules, optionals, slices, defer, generics, and compile-time features. Compiler: c3c (LLVM backend). Baseline: 0.8.0. Naming conventions (PascalCase types, snake_case functions) are enforced by the lexer.

Resources: c3-lang.org · github.com/c3lang/c3c · Discord

Docs: Language Fundamentals · Optionals (Essential) · Optionals (Advanced) · Generics & Macros · Misc Advanced · Stdlib source

Reference files (load on demand):

  • references/stdlib.md — full stdlib API (io, collections, math, threads, encoding, …)
  • references/advanced.md — macros, generics, interfaces, operator overloading, contracts
  • references/c_interop.md — calling C, exporting C-callable symbols
  • references/debugging-with-lldb.md — how to debug C3 programs with LLDB: breakpoints, stepping, variables

Syntax Cheatsheet

module myapp::utils;          // required for projects
import std::io;               // recursive by default

int x = 42;
const int MAX = 100;
int[] arr = { 1, 2, 3 };      // slice
int[4] fixed = { 1, 2, 3, 4 }; // fixed array (size left of name)

typedef MyInt = int;          // distinct type, no implicit conversion
typedef MyId @constinit = int; // also accepts literals
alias MyPtr = int*;           // transparent alias

fn int add(int a, int b) { return a + b; }
fn int square(int x) => x * x;            // short body
add(a: 1, b: 2);                          // named args

// Method: &self = pointer receiver
fn void Point.translate(&self, int dx, int dy) { self.x += dx; self.y += dy; }

struct Point { int x; int y; }
enum Status : uint { IDLE, BUSY, DONE }   // prefix inferred in context
bitstruct Flags : short { bool read_only : 0; bool hidden : 1; int priority : 2..4; }

Point p = { .x = 1, .y = 2 };

Control Flow

// switch — NO implicit fallthrough; empty case falls to next non-empty case
switch (s) { case IDLE: case BUSY: io::printn("Not done"); case DONE: io::printn("Done"); }
switch (c) { case 'a': nextcase 'A'; case 'A': io::printn("A or a"); }  // explicit fallthrough

foreach (value : arr) { ... }
foreach (index, value : arr) { ... }
foreach (&value : arr) { *value *= 2; }   // by reference
foreach_r (value : arr) { ... }           // reverse

Pointers, Slices

int* ptr = &value;

int[] s  = arr[1..3];  // inclusive end → indices 1,2,3
int[] s2 = arr[1:3];   // start:length
int[] s3 = arr[..];
int last = arr[^1];    // reverse index
sz len = s.len;

int[4] fixed;
takes_ptr(&fixed);     // fixed arrays don't decay — use & explicitly

String hello = "world";
char* cstr = hello.ptr;

Error Handling (Optionals)

T? = value or fault. Zero overhead. Optionals can't be function parameters — use Maybe{T} to store them in structs.

faultdef FILE_NOT_FOUND, PERMISSION_DENIED;

fn String? read_file(String path) {
    if (!file::exists(path)) return FILE_NOT_FOUND~;  // ~ converts fault → empty optional
    return file::load_temp(path)!;                     // ! re-throws
}

String? r = read_file("a.txt");
if (catch err = r) { ... }             // catch + inspect fault
String s = read_file("b.txt")!;        // rethrow to caller
String t = read_file("c.txt") ?? "x"; // or-else (binds tighter than arithmetic)
String u = read_file("d.txt")!!;       // force unwrap — panic if empty
if (try s = read_file("e.txt") && try t = read_file("f.txt")) { ... }
Op Meaning
~ fault → empty optional
! rethrow to caller
?? or-else
!! force unwrap (panic)
if (catch err = x) inspect fault value
if (try val = x) unwrap on success

Memory

// Heap
int* arr = mem::new_array(int, n);   // zero-initialized
mem::free(arr);

// Temp allocator — freed at @pool() exit; never return temp data past the scope
@pool() {
    String s = string::tformat("hello %s", name);
};

// defer — runs on any exit (return, break, fault)
fn void? process(String path) {
    File f = file::open(path, "r")!;
    defer f.close();
    defer try io::printn("ok");       // only on normal exit
    defer catch io::printn("failed"); // only on fault
}

// ZII: structs zero-init by default; @mustinit forbids it
struct Config @mustinit { String path; int retries; }

Key allocators: mem::new(T) / mem::tnew(T), mem::new_array(T,n) / mem::temp_array(T,n), @clone(v) / @tclone(v), mem::free(p).


Modules

module myapp::net::http;

fn void pub_fn()  @public {}   // default
fn void priv_fn() @private {}  // module-only
fn void file_fn() @local {}    // file-only, cannot be overridden

attrdef @Hot = @inline, @export;

Imports are recursive (import mylib includes mylib::net, etc). Use @norecurse to opt out.

Asymmetry: types don't need a module prefix when unambiguous; functions always do.


Compile-Time

$if env::WIN32:  fn void init() { }  $else  fn void init() { }  $endif
fn void win_only() @if(env::WIN32) { }

char[] data = $embed("assets/shader.glsl");

Key builtins: $Typeof(expr), $defined(expr), $assert(cond), Type::size / ::alignment / ::kind, @sizeof(expr), $reflect(field).name, $eval("name"), $expand("expr"), $feature(F).

See references/advanced.md for macros, generics, interfaces, and operator overloading.


Build

c3c init myproject
c3c run                      # build & run debug
c3c build release
c3c compile-run file.c3      # quick snippet test
c3c test / c3c benchmark
c3c vendor-fetch
c3c docgen

project.json targets: "type": "executable", "safe": true/false, "opt": "O0"/"O3", "strip-unused": true.

Verify a snippet: c3c compile-run snippet.c3. Add --use-stdlib=no for stdlib-free examples. c3fmt formats files; copy assets/.c3fmt to project root first.


Testing

fn void test_add() @test { assert(add(1, 2) == 3); }
fn void bench_add() @benchmark { for (int i = 0; i < 1000; i++) add(i, i+1); }

c3lsp Diagnostics Delay

c3lsp has a default diagnostics delay of 2000ms. If diagnostics appear stale or slow to update after edits, ask the user to reduce it via the -diagnostics-delay flag (e.g. -diagnostics-delay=500) in their LSP client configuration.

Common Pitfalls

Pitfall Fix
char[64] won't cast to char* Fixed arrays don't decay — use &array_name
Function call fails without prefix Functions always need module::fn()
Switch falls through unexpectedly C3 auto-breaks; use nextcase for explicit fallthrough
a & b == c wrong result Bitwise binds tighter than == in C3 (opposite of C) — parenthesize
?? precedence surprise f() ?? x + 1 is (f() ?? x) + 1 — binds tighter than arithmetic
Temp data dies after @pool() Copy results out before the block closes
#param side effects repeat #expr args re-evaluated on each use — not memoized
Dangling slice Slice lifetime tied to underlying pointer — don't return stack/temp slices
Unhandled optional String s = fn_returning_optional() — missing !, ??, or catch
Enum as bitmask Use bitstruct instead
Enum arithmetic Use .ordinal for math; ++/-- wrap around
Implicit narrowing / float→int Explicit cast required
Install via CLI
npx skills add https://github.com/ricardobeat/skills --skill c3-lang
Repository Details
star Stars 1
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator