No description
  • Rust 99.6%
  • Shell 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
crowmaki 7653760430 docs(language-spec): improve span example (#24)
Co-authored-by: Alic Szecsei <aszecsei@gmail.com>
Reviewed-on: #24
2026-08-27 06:40:10 +00:00
.githooks docs-overhaul (#19) 2026-08-26 05:39:32 +00:00
ast Complete the semantic analysis phase (RFC-0023–0028) (#20) 2026-08-26 17:05:50 +00:00
docs docs(language-spec): improve span example (#24) 2026-08-27 06:40:10 +00:00
lexer docs-overhaul (#19) 2026-08-26 05:39:32 +00:00
parser Complete the semantic analysis phase (RFC-0023–0028) (#20) 2026-08-26 17:05:50 +00:00
sema rfc/0029-lowering (#23) 2026-08-27 04:35:07 +00:00
source Complete the semantic analysis phase (RFC-0023–0028) (#20) 2026-08-26 17:05:50 +00:00
tests/fixtures rfc/0029-lowering (#23) 2026-08-27 04:35:07 +00:00
.gitignore Lexer (lex phase): minimal viable scanner 2026-08-21 03:17:38 -07:00
.known-red Complete the semantic analysis phase (RFC-0023–0028) (#20) 2026-08-26 17:05:50 +00:00
AGENTS.md docs: plan the lowering phase (RFC-0029), and settle the spec questions it opened (#22) 2026-08-26 18:18:16 +00:00
Cargo.toml docs-overhaul (#19) 2026-08-26 05:39:32 +00:00
CLAUDE.md docs-overhaul (#19) 2026-08-26 05:39:32 +00:00
README.md docs-overhaul (#19) 2026-08-26 05:39:32 +00:00

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 owned frees its target when the owner dies, transitively (RAII). Rebinding a live owned field is a compile error until you release it (detach, keep alive) or delete it (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:

  1. 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.
  2. No production code without a failing fixture. Add the .cat fixture, register it in the feature manifest, watch it fail, then implement the least that makes it pass.
  3. Run the hook. Enable it once per clone with git config core.hooksPath .githooks. It runs cargo fmt --check, cargo clippy --all --all-targets -- -D warnings, and cargo test --all against .known-red, and blocks the commit if any of the three disagrees. The workspace is at zero clippy warnings; keep it there.