- Rust 99.6%
- Shell 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .githooks | ||
| ast | ||
| docs | ||
| lexer | ||
| parser | ||
| sema | ||
| source | ||
| tests/fixtures | ||
| .gitignore | ||
| .known-red | ||
| AGENTS.md | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| README.md | ||
Catlang
Catlang is a statically typed systems language with C-like syntax, aimed at low-level work such as game development. It compiles to native code, and it has no automatic memory management.
It reads like C with the modern conveniences filled in: type inference on local bindings, lambdas and first-class functions, generics, interfaces, tagged unions, optionals, string interpolation, slices and ranges.
struct Vector3 {
x: float = 1.0;
y: float = 1.0;
z: float = 1.0;
}
const arr = []int { 1, 2, 3, 4, 5 };
for (x in arr) {
print(x);
}
What it deliberately does not do:
- No garbage collector, no reference counting. Memory is manual.
- No borrow checker. Ownership is tracked for exactly one thing: a struct
field marked
ownedfrees its target when the owner dies, transitively (RAII). Rebinding a liveownedfield is a compile error until youreleaseit (detach, keep alive) ordeleteit (destroy). Everything else is a raw pointer with no lifetime rules, and dangling it is your problem. - No hidden per-object cost. A struct in memory is exactly its fields — no
vtable pointer, no type tag, no RTTI. Method calls are statically dispatched
by default; a table is consulted only when a value is referenced through an
interface, and an interface reference is a two-word
(pointer, table)pair that allocates nothing.
The design tenets, in short: type safety should be kind (infer as much as possible, because tedious safety gets worked around), functions should be first class, and abstractions should cost as little as possible — not zero, but the more common the syntax, the better its default has to be.
Who this is for
Catlang is for people who write the kind of code that C is still used for — engines, arenas, frame pools, tight data layouts — and who want a better type system without a lifetime checker telling them their arena is illegal.
Right now, it is for people who want to read a language design or hack on a compiler front end. It is not for people who want to ship software. Be clear on the status:
| Language spec | Written, and treated as the source of truth |
| Lexer | Working |
| Parser | Working — builds a full syntax tree for every parse fixture |
| Semantic analysis | Under construction. The passes exist and are unit-tested; the top-level sema::check() is still a stub that rejects everything |
| Optimizer, code generation, linker | Do not exist |
| Compiler binary | Does not exist |
So: you cannot compile or run a Catlang program today. There is no catc.
There is a front end that reads Catlang source and a specification that says
what it should mean. Everything below is written with that in mind.
The compiler is intended to bootstrap — be rewritten in Catlang — once the language is stable. Until then Rust hosts every phase.
Install
There is no installable binary, and nothing to cargo install. What you can do
is build and test the front end from source.
You need a Rust toolchain with edition 2024 support (verified on
rustc 1.97.1 / cargo 1.97.1). The workspace has no external dependencies
— standard library only — so the build is offline and fast.
git clone ssh://git@git.crowmaki.me/crowmaki/catlang.git
cd catlang
cargo build
That builds all five library crates in a couple of seconds.
Getting started
Run the test suite
cargo test --all
Expect one failure. This is on purpose and it is worth understanding before you file a bug:
lexer 17 passed
parser 79 passed
ast 10 passed
sema 152 passed
fixture drivers: lex ok, parse ok, semantics FAILED (30 of 30 fixtures)
The semantics fixture driver is red by contract. Development here is
test-driven at the level of the language itself: every feature in the spec has
one small .cat program under tests/fixtures/, and a phase opens with its
driver failing every fixture. That failing count is the phase's progress bar.
The file .known-red names exactly which tests are allowed to fail; the
pre-commit hook checks that list in both directions, so an unexpected failure
blocks a commit and a stale exemption does too.
To run only the parts that are green:
cargo test -p lexer -p parser
Read some Catlang
The fixtures are the fastest way to get the feel of the language. Each one is a few lines lifted from the spec's own examples.
ls tests/fixtures/lex tests/fixtures/parse tests/fixtures/semantics
tests/fixtures/parse/hello-world.cat is the only one with a main:
function main() -> void {
print("Hello, world!");
}
The rest are snippets — bare declarations at file scope, which may reference symbols they never declare. That is deliberate; do not "complete" them.
// tests/fixtures/parse/function-lambda.cat
const timesTwo = (num: int) -> int {
return num * 2;
}
// tests/fixtures/parse/optionals.cat
const myPrint = (num: ?int) -> {
if (num) {
print(num);
} else {
print("Nope!");
}
}
// tests/fixtures/parse/struct-owned.cat
struct Transform {
owned position: *Vector3;
owned rotation: *Quaternion;
}
// tests/fixtures/semantics/spans.cat
let a: Span = 0..256; // Span<u8>
const b = (x: Span) -> void {
for (i in x) { print(i); }
}
b(a);
Fixtures under semantics/ may be negative — a line marked with a trailing
// ERROR: <reason> is one the compiler is required to reject.
Run the parser over your own code
There is no CLI, so the way to feed the front end a file is to drop it into a
fixture directory and run that phase's driver. Write a .cat file into
tests/fixtures/parse/, then:
cargo test -p parser --test fixtures
A parse failure is reported with the file, line, column, and message:
parse\scratch.cat:1:24: expected ')' to close the parameter list
Delete the scratch file when you are done — a committed fixture is a claim about the language, and it belongs in the feature manifest.
Repository map
Cargo.toml workspace manifest (a virtual package; no root crate)
source/ Span, Position, SourceBuffer, Diagnostic. No dependencies
ast/ the syntax tree + its S-expression dump
lexer/ token kinds and the single-pass scanner
parser/ recursive descent + Pratt, producing an ast::Program
sema/ name resolution and type checking, lowering to a typed HIR
tests/fixtures/ one small .cat program per language feature
lex/ must tokenize
parse/ must build a syntax tree
semantics/ must pass semantic analysis (or be rejected as marked)
docs/prd/ the specification — what the language is
docs/rfc/ the implementation notes — how the compiler does it
.known-red tests that are allowed to fail, and what clears each
.githooks/pre-commit fmt + clippy + test gate
Dependencies point strictly downward: source depends on nothing, ast and
lexer depend on source, parser depends on source/lexer/ast, and
sema depends on source/ast only. sema never sees a token; ast never
sees a scanner.
Where to go next
docs/prd/language-spec.md— the language reference. Types, arrays, functions, structs, interfaces, unions, spans, optionals, conversions. Start here if you want to know what Catlang is.docs/prd/0-overview.md— vision, tenets, the memory model, and the object model, with the reasoning behind each.docs/prd/0-feature-manifest.md— the binding table: every feature, its fixture, and the phase that must accept it.docs/prd/— the rest of the specification.docs/rfc/— how the compiler is actually built, phase by phase, and why each decision went the way it did.
Contributing
The working conventions live in docs/rfc/0001-conventions.md.
The short version:
- The spec is the source of truth. If the spec and the code disagree, the code is wrong. If the spec is silent, fix the spec first.
- No production code without a failing fixture. Add the
.catfixture, register it in the feature manifest, watch it fail, then implement the least that makes it pass. - Run the hook. Enable it once per clone with
git config core.hooksPath .githooks. It runscargo fmt --check,cargo clippy --all --all-targets -- -D warnings, andcargo test --allagainst.known-red, and blocks the commit if any of the three disagrees. The workspace is at zero clippy warnings; keep it there.