Tig (tc-lang) — Simplest possible, usable systems language
- title
- Tig (tc-lang) — Simplest possible, usable systems language
- author
- alonsovm44 (Alonso VM)
- fetched
- 2026-07-21
Tig 🦁
Simplest possible, usable systems language
Repo: https://github.com/alonsovm44/tc-lang Stars: ~40 · License: MIT (per README badge; GitHub API reports no detected SPDX license file) · Primary language: C · Version: 1.3.1s/1.3.2 · Platforms: Windows, Linux, macOS · Description: "A minimalistic ergonomic layer over C"
Disclaimer: For stage0, Tig can use AI codegen for the C core (but test thoroughly); for stage1 of self-hosting, handwritten code only. See CONTRIBUTING.md. Note: see dogfood/ folder for non-trivial examples of Tig being used.
Tig is a minimalistic systems programming language.
What's New in v1.3.2
- Added C compiler flags using
-- - Added freestanding parameter to enable freestanding mode
- Multiple file linking support
- Added binary and hex literals
- Removed old function syntax
- Removed extern blocks
Project Goals
- Make the first usable systems language from Mexico.
- Explore the bare minimum of what a systems language must have to be usable, modern and ergonomic.
- Learn C and master Tig.
- Make an OS with it.
- Get a job.
Philosophy
Why it was made: the author wanted an ergonomic systems language capable of anything, with fewer keywords than Go, no GC and without heavy runtime overhead; as safe as possible (but not necessarily proving safety). A language one could master and teach to others, and put Mexico on the map.
Backend: for now, C11. Core Ideal: Tig has to fit in a single person's head. Small but powerful.
Core principle:
Everything that can be built with libraries has to be built with libraries. The core language is small. Explicit is better than implicit, but exceptions can be made. Flexibility over rigidity. Anything C can run, we run: anywhere C has run, we will run.
Features
- 18 keywords —
if,loop,break,defer,ret,strun,fn,use,pin,match,else,enum,async,select,throw,try,catch,raw. - Zero-boilerplate async — automatic runtime initialization, no manual setup.
- Async functions —
async fn. - Concurrent data structures — built-in
queue<T>andstack<T>. - Ownership transfer —
@operator for safe resource transfer. - Select statements — multi-operation waiting with
select. - Pin — keep variables alive across async boundaries.
- No hidden magic — no GC, no type inference, no shadowing, no aliasing.
- Raw pointers (
->) and fat pointers (=>) with built-in slicing. - Manual memory —
alloc()/free()withdeferfor cleanup. - Packed structs — no padding, predictable layout.
- Rust-style errors — colored diagnostics with source lines and carets.
- One-step compile —
tightc source.tc -c apptranspiles and compiles in one command. - Inline imports —
@use "lib.tc"inlines another.tcfile at compile time. - CLI args —
i32 fn main: =>->i8 args { ... }.
Quick Start
make # build the compiler (requires make)
./tigc stdlib/io.tc -o stdlib/io.h # compile stdlib headers (once)
./tigc samples/fizzbuzz.tc -c fizzbuzz
./fizzbuzz
How It Works
Tig is a source-to-source compiler (transpiler) written in ~4,600 lines of C. It reads .tc files and outputs portable C11.
source.tc -> [Lexer] -> [Parser] -> [AST] -> [Checker] -> [Emitter] -> output.c -> gcc/clang -> binary
Pipeline
- Lexer (
lexer.c) — tokenizes source; tracks line/col for error reporting. - Parser (
parser.c) — builds AST; handles operator precedence, scope-levelpinenforcement, and@usefile inlining (recursive parse + splice). - Emitter (
emitter.c) — walks the AST and outputs C11. Most constructs are 1:1:
| Tig | Emitted C |
|---|---|
=>i32 s |
tc_fat_i32 s (struct with .ptr + .len) |
defer { free(p) } |
scope-exit statements emitted in reverse order before }/return |
pin x |
nothing emitted — enforced at parse time (compile error on reassignment) |
@use "lib.tc" |
declarations inlined directly into AST (no #include) |
alloc(T, n) |
TC_ALLOC(T, n) → calloc(n, sizeof(T)) |
=>->i8 args in main |
main(int argc, char **argv) + local fat pointer wrapping them |
What it is not
Not memory safe — no borrow checker, no GC; memory management is manual (arenas planned). Provides safety features: fat pointers, self-cleaning error functions, defer statements. No optimizer, no IR, no type-inference pass, no code generation beyond string concatenation of C. Can leverage LLVM flags from gcc/clang. Output is always readable, debuggable C (inspect with tightc source.tc -o source.c).
What Tig is good for
CLI tools (tiny/no runtime overhead); embedded / bare-metal (no runtime, no allocator required, packed structs); game engine internals (manual memory, no GC pauses); learning compilers (small enough to read in an afternoon); C codebases that want better ergonomics.
Syntax highlights
Hello world:
use "stdlib/io.tc"
fn i32 main: {
print("hello, world")
ret 0
}
- Variables are C-like, mutable by default; uninitialized default to
0.pin PImakes a variable immutable in the current scope. - Functions:
fn i32 add: i32 a, i32 b { ret a + b }. Varargs via.... - Strunions (
strun) — a struct/union spectrum ([struct]—[strun]—[union]).&marks a union element sharing memory; anonymous types provide padding/grouping. Struns can contain methods (fn void printPoint: self { ... }). - Pointers:
->raw (address-of/deref),=>fat pointer (.ptr/.len), slicingarr[1:3], pointer combos (->->,=>->,->=>,=>=>). Struct pointer access via.>(same as C's->). - Control flow:
if/else if/else,loop { ... break }(infinite),loop if (cond) { ... }(while alias),matchstatements with_wildcard and=arms. - Memory:
alloc(i32, 100)+defer free(arr)(braceless defer allowed). - Imports:
use "lib.tc"(link precompiled.h) vs@use "lib.tc"(inline.tcat compile time). - C FFI:
"C"{ #include "clib.h" }. - Error reporting: Rust-style
error[E000]diagnostics with source lines/carets;tightc --error E000prints help.
Error system
Errors are a built-in type via error. An error is a function-like unit carrying context params plus handling/cleanup code, invoked with throw. try/catch blocks handle them; catch blocks work like match (specify which errors to catch and what return value to use, _ wildcard).
error zero_division: i32 a, i32 b { printf("Error, division by zero: %d / %d", a, b) }
fn f64 divide: i32 a, i32 b {
if(b == (f32)0){ throw zero_division(a, b) }
return (f64)a / (f64)b
}
Note: error system may be tricky in freestanding environments (no longjmp, no debug printf).
Async
Zero-boilerplate: async fn void worker: i32 x { ... }; calling it auto-initializes the runtime (no async_init()/async_shutdown()). Async functions are always void; communicate via channels (queue<T>/stack<T> from stdlib/snq.tc). select { task1() => {...} task2() => {...} } waits on multiple operations.
Types
i8→char, i16→int16_t, i32→int32_t, i64→int64_t, u8→uint8_t, u16→uint16_t, u32→uint32_t, u64→uint64_t, f32→float, f64→double, void→void.
Compiler usage
tigc <input.tc> [-o output.c] [-c binary] [-t] -- [clang/gcc flags]
-o file.c— emit transpiled C (.hgets#pragma once)-c binary— transpile + compile to binary (auto-detects gcc/clang)-t— keep temporary files- (none) — print transpiled C to stdout
Hot reloading
Global hot reloading of functions, strun definitions, and enums — recompile shared libraries on-the-fly without restarting the running app. Host/DLL splitting architecture: main compiled into the host executable; all other functions/structs/enums compiled into a versioned shared library (hotlib_N.dll / hotlib_N.so), exported automatically. Host stubs check the current library version, reload on change, and execute via function pointers; old version files auto-cleaned. Flags: -H <libname> (enable hot reload), --hot (rebuild only the hot library), -t/--temp. See dogfood/HOTSWAPPING/.
Project structure
tc-lang/
compiler/
include/ # headers
src/ # compiler source (C)
stdlib/ # standard library (.tc)
samples/ # example programs
docs/ # language specification
Makefile
Stdlib
Simple stdlib wrapping libc plus runtimes for stacks/queues and async functions.
Built by @alonsovm44.