Best Practices & Idioms
When writing Axon, follow the conventions established by the standard library. These rules make your code easier for both humans and AI agents to read, write, and modify.
Naming
Section titled “Naming”- Lowercase with hyphens for module names and multi-word identifiers:
axon-std,hash-map,http-server. Underscores are reserved for FFI symbols that need to match C headers. - Lowercase single-word for hot-path primitives:
len,push,pop. The short name is the contract — every collection has alen. - Public functions use
pub. Internal helpers omitpub. The compiler enforces visibility. - Module names match file names.
(import foo "std/foo.axs")looks upstd/foo.axs— never alias.
Formatting
Section titled “Formatting”-
One binding per line in
(let ...)and(struct ...). Multi-line bindings go on their own indented lines for readability:(let ((count i64 0)(sum i64 0)(max i64 0))(block ...)) -
No trailing commas, no semicolons. S-expressions are uniform —
(a b c)is the only form. -
2-space indentation. Three or more spaces is a parser error in the formatter (CI enforces).
-
Blank lines between top-level decls in modules. The formatter adds them automatically.
-
Function calls start on their own line for multi-argument calls —
arg alignmentis enforced byaxonc format.
- Always annotate types. The compiler will infer locals, but explicit annotations are mandatory for function signatures. AI agents that produce inferred-only signatures hallucinate types under refactoring pressure.
- Use the narrowest integer type that fits.
i8for byte values,i16for char codes,i32for most counters,i64for sizes and addresses. Wider thani64requires abigintwrapper. - Prefer
booltoi64for true/false values. The compiler can omit dead branches when it knows a value is boolean. - Use
stringfor text,(ptr u8)for raw bytes. Mixing them is a type error.
Errors
Section titled “Errors”-
Return
(Result T E), not(T, i64). Errors-as-data is enforced at the type level. -
(Option T)for “may not exist”. Don’t use sentinel values like-1for “not found”. -
Pattern-match on both branches:
(match (call map_get m key)((some val) (call process val))((none) (call print_str (str "not found")))) -
Never use
panicfor expected errors.(axon_panic ...)is for unrecoverable invariant violations only.
Concurrency
Section titled “Concurrency”- Channels first, mutexes second. If two threads need to share state, use a channel. Mutexes are a code smell — they indicate that the data should probably be an actor.
- Yield in hot loops. Cooperative green threads only switch at
await,chan_recv,evloop_wait. If your hot loop never yields, it starves every other task. - One owner per resource. The compiler enforces this with
(ref 'a T)and(move T)semantics. Trying to share aVec<T>between two actors is a compile error.
Performance
Section titled “Performance”- Profile first, optimise second. Every
axonc --emit-llvm program.axs -o program.llgives you readable LLVM IR — read it before guessing. - Prefer stack allocation.
(let (x i64 0))allocates on the stack;(alloc i64)allocates on the heap. Use heap allocation only when the lifetime outlives the current scope. - Pre-allocate collections.
(let (v (vec_with_cap T (i64 1024))))reserves capacity up front. Resizing a Vec is O(n) and can fragment the allocator. - Avoid string concatenation in loops. Each
str_concatallocates a fresh buffer; for N concatenations that’s O(N²) total. Build the result with a single allocation and usestr_from_bufinstead. - Mark hot functions
@inline. The compiler inlines aggressively by default, but explicit@inlineannotations give you control when the heuristic guesses wrong.
Testing
Section titled “Testing”- Golden-file tests are the default. Write
tests/foo.axsplustests/foo.expected. Thetest:axon-e2ejob diffs stdout against the expected file. No test framework required. - One assertion per test function. When a test fails, you want to know which invariant broke, not “something in this 200-line function”.
- Contract tests for stdlib. Use
axon_assert_eq_i64/axon_assert_ne_i64/axon_assert_trueto verify behaviour — these print PASS lines that the CI parser picks up. - Don’t test the compiler. If you want to assert that axonc rejects a program, the existing
tests/errors/directory is the right home. Application-level tests go intests/.
Modules
Section titled “Modules”- One concern per module.
std/string.axsdoes string operations, not file I/O. Cross-concern code goes instd/io.axsor a higher-level module. - Module names match file paths.
(import http "std/http.axs")is the convention; the import path is a literal file path. - Forward declarations are free.
(extern foo ((_ i64)) void)doesn’t requirefooto exist at parse time. Use this to break circular dependencies between two modules that reference each other. pubonly on the public API. Internal helpers stay private. The formatter will warn onpubuse that isn’t reachable from another module.
Anti-patterns
Section titled “Anti-patterns”(set x x)for side effects. If you want side effects, write(call some_function); don’t pretend to “compute” via self-assignment.(if cond (return x) (return y)). Use(if cond x y)directly. Thereturnform obscures the data flow.- String comparison via
str_eq == 0. Usestring.eqfrom the stdlib. The equality check is(ne (call str_eq a b) (i64 0))— butstring.eqreturns abooldirectly. - Float equality.
(eq x y)withf64values is almost always wrong. Use(abs (sub x y)) < epsilonor compare against a known tolerance. - Long chains of
(if (cond1) (if (cond2) ...)). Use(match ...)or extract a helper. Nestedifis the wrong abstraction.
When in doubt
Section titled “When in doubt”Read std/*.axs. The standard library is the canonical example of idiomatic Axon. If your code looks like the stdlib, you’re doing it right.