Development¶
Use this guide to set up the development environment, choose verification commands and work with the pinned Mojo compiler. Start with the design principles, which define the library's scope and the ownership of shared mechanisms.
Environment¶
Set up the environment as in installation. The
tooling scripts call pixi and mojo from PATH, so make sure
$HOME/.pixi/bin is on it. The compile-time measurement and its tooling test use
GNU time (/usr/bin/time); on Debian or Ubuntu install it with
apt-get install time.
Mojo shares one compile cache per environment under
.pixi/envs/default/share/max/cache. A warm cache hides most compile-time cost,
so measure compile time only with an empty MODULAR_CACHE_DIR (the measurement
tooling does this for you).
Repository layout¶
| Path | Contents |
|---|---|
src/fp/ |
The library, one directory per public package plus _internal |
tests/runtime/, ownership/, laws/, integration/ |
Programs with a main that assert their results |
tests/benchmarks/ |
Programs that time library code against hand-written loops; the suite reports them as diagnostics |
tests/compile_fail/ |
Programs that must be rejected, with # error: lines naming the expected diagnostics |
tests/tooling/ |
Python unit tests for the scripts |
scripts/ |
Test, build, measurement and documentation tooling, and the generator of the plain-function pipe, flow, compose, match and rewrite overloads (python scripts/generate_plain_overloads.py; --check reports drift) |
docs/content/ |
This site's pages; navigation is in mkdocs.yml |
docs/examples/ |
The runnable examples |
docs/theme/ |
The MkDocs theme: templates, styles, scripts and bundled fonts |
docs/*.json, docs/hooks.py |
Public export inventory, API descriptions, example outputs and the MkDocs hooks |
Verification gates¶
Run from the repository root. Commands with --output need a directory that
does not exist yet; use a new name under .cache/ each time.
| Command | Checks | Time |
|---|---|---|
pixi run suite --package .cache/b2 |
Every runtime, ownership, law and integration test, every example on the site (with its reviewed output) and the runtime benchmarks, at O3, as four executables | About 2 minutes on 4 cores |
pixi run compile-fail --package .cache/b2 |
Every compile-failure fixture is rejected with its declared diagnostics; one compile per fixture, in parallel | About 2 minutes on 4 cores |
pixi run build --output .cache/b1 |
Builds a source package in <output>/fp/, precompiles the whole package with --Werror, then runs docs/examples/algebra_core.mojo against it |
About 15 s |
pixi run build --format precompiled --output .cache/b2 |
The same with fp.mojoc |
About 15 s |
pixi run -e docs docs-check |
Strict MkDocs build, then links, anchors, assets and the search index | Seconds |
pixi run docs-test --output .cache/d1 --jobs 4 |
Every example on the site through source and package imports at O0 and O3 with --Werror |
A few minutes |
pixi run python -m unittest discover -s tests/tooling |
Tooling unit tests | Seconds |
suite and compile-fail test the library a build produced: pass its output
directory to --package. --source compiles against src/ instead, and with
neither option they precompile src/fp into their own output folder first.
--optimization 0 builds at O0 instead of O3; the suite's O0 executables take
several times longer to build and up to about 5 GB each, so pass --jobs 2 on a
16 GB machine. --filter <text> selects the programs whose path contains the text, and
--output <dir> names the report folder (a fresh one under .cache/ by
default). build bounds the client with a 60-second compile limit, two compiler
threads and a 4 GiB memory limit on Linux.
The suite keeps every program standalone: each still has its own main and can
be run alone with pixi run mojo run -I src <path>. scripts/suite.py
generates one driver per executable that imports the programs as top-level
modules and calls each main between marker lines, builds the four
executables in parallel into one directory (--jobs limits how many build at
once; each needs up to about 3 GB), then runs them in sequence:
| Executable | Programs |
|---|---|
runtime-1 |
tests/runtime/test_[a-c]* |
runtime-2 |
The rest of tests/runtime/ |
ownership |
tests/ownership/ |
integration |
tests/integration/, tests/laws/, the examples in docs/examples.json, then tests/benchmarks/ |
A failing assertion fails only its program. A crash or timeout fails the
program that was running, and the executable restarts after it (--from N). An
example passes only when it prints its reviewed output. Benchmark lines are
reported as diagnostics and never fail the suite. The report in the output folder
keeps the generated drivers, the build and run logs and the executables, and an
independent audit re-derives every outcome from the raw logs.
Programs built into one executable must not spell the same function type over
different declarations that share a name: Mojo 1.1 treats them as one type and
the build fails with invalid redefinition of 'def(...)' (see
native boundaries).
Give a test-local type a name specific to its test, such as PipelineFailure
rather than Failure, when the build reports it. Each executable resolves its
programs by module name, so file names must be unique across its directories;
the suite checks this before building.
Rules that matter:
- Do not edit inputs while
suite,compile-failordocs-testruns. They hashsrc/,tests/,scripts/,docs/,benchmarks/,README.md,pixi.toml,pixi.lockandmkdocs.yml, and fail if anything changes before they finish. - Precompile after changing traits or generic signatures. A package precompile type-checks generic declarations that no client instantiates; several Mojo 1.1 trait errors appear only there.
- Stopping a run.
pkill -f "<pattern>"also matches your own shell if the pattern appears in its command line. Use a bracketed pattern such aspkill -f "scripts/[s]uite.py", and stop orphanedmojo buildchildren too.
Making a change¶
- Design the whole contract first. State ownership, evaluation order, errors, empty and no-match behavior, and costs before editing. A restricted example that works does not complete a feature.
- Find the owner. Every shared mechanism has one implementation (see the overview). Reuse or generalize it and migrate every consumer; never add a parallel version.
- Name API changes explicitly. Public API or behavior changes are decided before they are implemented and documented with a migration.
- Test both ways. Add positive programs and compile-failure programs for the new rejections. Keep expected values independent of the operation under test, pair each rejection with a nearby valid program, and never execute a program the compiler was expected to reject. A compiler crash or timeout is a failure, never a passing rejection.
- Run the suite and the compile-failure fixtures against a fresh package
build, and
docs-checkanddocs-testif pages or examples changed.docs-testalso covers the source route and O0. - Update the documentation in the same change (see maintaining the docs).
Working with Mojo 1.1¶
These practices come from building this library on the pinned compiler; the limits behind them are recorded in native Mojo boundaries.
- Keep the type-level program small. Associated types should name only a
struct's own parameters and their immediate members; put behavior in method
bodies; use one concrete struct per operation. Chains of conditional aliases
and
TypeListpipelines can take minutes to verify, especially through a precompiled package. - Diagnose compile time by bisection.
--mlir-timing --mlir-timing-display=listshows only the phase (Import Mojois the front end). The compiler binary is stripped and attaching a debugger is blocked, so precompile package variants that each remove one declaration, build a tiny import-only client against each in parallel, and compare. - Report admission errors first. The first failing inline
comptime assertis reported before failures inside called helpers. Put admission checks inline and gate the callee withcomptime if admitted:so its errors do not mask the admission message. - Infer a callback's error as
AnyTypebefore catching it. Under aMovable & Deinitablebound, a non-raising function's inferred error is an uninhabited type that is notNever, and atryaround a call that raises it crashes the compiler. Infer it asAnyTypeand normalize it with_internal.errors._NativeError, or call through_forward. - Do not nest calls to an overloaded function. Overload resolution backtracks through nested calls, so compile time grows exponentially with the nesting depth; give each overload of a nested builder its own name.
- Spell signature types flat. A type derived through a chain of generic structs, such as a result computed from a list of clauses, can take compile time exponential in the chain's length when it appears in a signature. Spell it over the parameters directly and pass it to the implementation explicitly.
- Check hot paths in assembly.
OptionalandVariantstorage of non-trivial types is initialized and taken out of line, andOptional.take()is not inlined; use untaggedMaybeUninitslots where it matters. Compare O3--emit asmoutput against hand-written code. - Measure against a hand-written version, then count instructions. Write the
same computation by hand in the shape the library uses (a loop with an
explicit stack, say) to separate the cost of the shape from the cost of the
library. Count instructions with
valgrind --tool=callgrindon a build with-g --target-cpu x86-64-v3(valgrind cannot run AVX-512 instructions), and givecallgrind_annotate --includethe standard-library checkout to see its lines. Wall times on a shared host vary by 2x over minutes, so only compare runs taken back to back. - Inline small helpers on hot paths. The pinned compiler can leave a small
generic helper out of line;
@always_inlineon the matching engine's per-value helpers halved its time. Keep per-element state small: a 64-byte frame instead of a 16-byte one doubled the time of a hand-written loop over a deep value. - Never pass an iterator source through
iter()inside the library: it restarts sources that are both iterator and iterable. Take sources throughiteration._advance._source, which uses an iterator as it is and consumes only an owned collection. - Test heap contents. A
String'sbyte_length()reads inline metadata and can look correct for a dangling buffer; compare contents. - Read the pinned standard library. Clone
https://github.com/modular/modularat branchmojo/v1.1.0and readMojo/stdlib/std;mojo doccannot read the compiledstd.mojoc.