comfylang is a low-level, statically-typed systems language, with the comfyc compiler currently compiling straight to ARM32 assembly.
The dev environment is a Nix flake providing the Rust toolchain, an ARM32 cross toolchain (assembler + linker), and qemu-arm, so you can build and run ARM32 binaries from any host architecture:
nix develop
# compile a .cfy file to ARM32 assembly
cargo run -- examples/exit_code.cfy
# assemble + link the generated assembly
util/link_compile.sh build/main.s
# run the resulting binary (native on ARM32 hosts, qemu-arm everywhere else)
util/host-run.sh build/main
# ...or all of the above in one go:
util/run.sh examples/exit_code.cfyControl flow & recursion:
// examples/showcase.cfy
fn factorial(n: int) -> int {
if n <= 1 {
return 1;
}
return n * factorial(n - 1);
}
fn main() {
let mut code = 0;
if factorial(4) >= 20 && !(factorial(4) < 0) {
code = factorial(4); // 24
}
$syscall(1, code, 0, 0, 0, 0, 0); // exit 24
}Structs, arrays & pointers:
// examples/structs/nested.cfy (trimmed)
struct Point {
x: int,
y: int,
}
fn main() {
let mut p = Point { x: 1, y: 2 };
p.x = p.x + 4; // 5
let mut nums = [10, 20, 30];
nums[1] = 99;
let mut total = 0;
let mut out = &total;
*out = p.x + p.y + nums[0] + nums[1] + nums[2]; // 5+2+10+99+30 = 146
$syscall(1, total, 0, 0, 0, 0, 0); // exit 146
}More examples in examples/.
Variables
let NAME = expr;is an immutable binding. Whenexpris provably constant it's folded away entirely (zero runtime cost); otherwise it falls back to a real, immutable stack slot.let mut NAME = expr;is always a real stack-allocated local that can be reassigned withNAME = expr;.
Arithmetic & comparisons
+ - * / %and unary-are constant-folded when possible; runtime arithmetic falls back to a simple stack-machine codegen. Runtime (non-constant)/and%aren't supported yet - arm32 has no hardware divide instruction and the compiler is-nostdlib, so it can't call into libgcc for a software fallback.== != < <= > >=produce a realbool, and can't be chained (a < b < cdoesn't parse).
Control flow
if/else/else if,while. Conditions must bebool- comfy does not implicitly convert integers to booleans.&& || !are genuine short-circuiting logical operators.
Functions
- Typed parameters (
int/boolso far) and an optional-> Typereturn; omitting it means the function returns(). - Functions must end with a
returnstatement if they return a value. Calls follow the AAPCS calling convention (up to 4 arguments inr0-r3, return value inr0), and recursion works.
Pointers
*Tis a pointer toT(recursive, e.g.**T).&xtakes the address of a local (only bare identifiers so far);*pdereferences, and works both to read (let y = *p;) and, as the direct target of=, to write (*p = v;).
Arrays
[T; N]is a fixed-size array ofT. Array literals ([e1, e2, ...]) are the only way to create one, and only as alet/let mutinitializer (not a general expression yet); length and element type are inferred from the literal.- Indexing (
arr[i]) works for both reads and, on amutarray, writes (arr[i] = v;) - the address is computed at runtime (base + i * elem_size), with no bounds checking yet. Locals only, not function parameters or return values.
Structs
struct Name { field: Type, ... }declares a struct, forward-declared like functions (order-independent).- Struct literals (
Name { field: value, ... }) work like array literals - only as alet/let mutinitializer, fields in any order but all required. - Field access (
s.field, chainable ass.a.b.c) works for reads and, on amutstruct, writes, and composes with arrays (s.arr[i]). - A struct can't contain itself by value (infinite size) - only through a pointer (
*Name), same as recursive data structures in C. Locals only, not function parameters or return values.
Syscalls
$syscall(nr, a0, a1, a2, a3, a4, a5)is the sole compiler intrinsic, usable as a statement or an expression (its return value, fromr0, can be captured). It maps directly onto the Linux ARM EABI syscall convention. Arguments may be pointers as well as integers, for passing buffer addresses. Named wrappers likewrite/read/exitwill come back as ordinary standard-library functions once comfylang has a standard library.
comfyc lowers checked ASTs to a flat, three-address IR before codegen, and runs the following passes on it to a fixpoint, entirely at compile time:
- Constant propagation - through virtual registers and stack-local slots alike; a
letbinding that's provably constant is folded away entirely. - Redundant load elimination - a repeated read of an unmodified local becomes a cheap register copy instead of a stack reload.
- Branch folding - an
if/whilecondition that folds to a compile-time constant becomes an unconditional jump (or vanishes) instead of a runtime comparison. - Common subexpression elimination - a repeated computation over unchanged inputs is computed once and reused.
- Strength reduction - multiplying by a compile-time power of two becomes a shift;
*0/*1collapse to their trivial equivalents. - Dead code elimination - unused pure computations, unread stores, and unreachable code (after a
return/unconditional jump) are all removed. - Tail-call elimination - a
return f(...)in tail position reuses the current stack frame instead of growing the stack, so self-recursive tail calls run in constant space.
Register allocation is a simple linear-scan allocator mapping virtual registers onto a handful of real ARM registers, spilling to the stack only once it runs out.
Planned, in rough order:
Lexer/parser/codegen skeleton with real diagnostics✅Integer types + variables (✅let/let mut, assignment)Arithmetic + comparison expressions with precedence✅Control flow (✅if/else,while)Logical operators (✅&&,||,!) with short-circuit evaluationUser-defined functions, calling convention, recursion✅Pointers (✅*T,&x,*p)Arrays (✅[T; N], indexing)✅structsIR + optimization passes (constant propagation, dead code/branch/store elimination, CSE, strength reduction, tail calls) + linear-scan register allocation✅- Additional backends (x86_64, ...), standard library, self-hosting prep
