This page walks you through building the Insert compiler, compiling a program, and running a self-modifying program.
Disclaimer: this documentation was written using AI, although I've reviewed and edited it for correctness. It will be rewritten in the future, but should suffice for now.
- A nightly Rust toolchain. The exact version is pinned in
rust-toolchain.toml, so if you use
rustupit will be picked up automatically. Insert relies on a few nightly features. - A C compiler such as
gccorclang. Insert compiles to C, and you build the C yourself. - Node.js, if you want to run the test suite.
From the repository root:
cargo buildThis produces the compiler binary at target/debug/Insert. The test suite uses
the debug build on purpose, because it includes integer overflow checks. You can also
run the compiler directly from cargo with cargo run -- <args>, which can be more
convenient.
Insert source files use the .int extension. The compiler reads one file and
writes C to standard output:
cargo run -- test/src/simple.int > main.c
gcc main.c -o a.out
./a.outBy default the output C is minified (no extra whitespace). If you want to read
it, pass --fancy to get indentation and newlines:
cargo run -- --fancy test/src/simple.intInsert [OPTIONS] <INPUT>
<INPUT> The Insert file to compile.
-t, --target The target language. Only "C" is supported right now
(the default).
-f, --fancy Format the output with indentation and newlines.
-s, --stage How far to take compilation: "parse", "opt", or "target"
(the default). See below.
The --stage option lets you stop early and inspect the intermediate state,
which is useful when you are exploring how the compiler works:
parseprints the MIR right after parsing, before any passes run.optprints the MIR after all the optimization and analysis passes.target(the default) prints the final C.
Imports are resolved relative to the importing file:
import "std/string.int";Paths can be relative ("./helpers.int", "../shared.int") or, as with the
standard library, given relative to where you run the compiler. The standard
library lives in std/ and is itself written in Insert. It provides
things like string() for converting numbers to strings, and printQuine for
printing a quine.
A self-modifying program prints its own next version to standard output. To actually watch it evolve, you compile and run it in a loop, feeding each generation's output back in as the next program.
quineRun.sh does exactly this for the pong example. The core of it is:
while true
do
gcc main.c -o a.out # build the current generation
output=$(./a.out "$key") # run it, its stdout is the next generation
echo "$output" > main.c # overwrite the source with that output
doneSo the workflow is:
- Compile your Insert program to
main.cwith the Insert compiler. - Build and run
main.c. Its output is a new, modifiedmain.c. - Repeat. Each run is a new generation of the program.
For pong this means each generation renders one frame and embeds the updated game state into the source it prints, so the next generation picks up where the last left off.
node test.mjs # compile every test and diff against snapshots
node test.mjs --bless # accept current output as the new snapshotsThe runner builds the compiler first, then compiles every file in
test/src/ at multiple stages and compares the result against the
saved snapshots in test/snapshots/. Files that start with //@check must
compile cleanly and files that start with //@error must fail with the expected
error message.
- Language guide covers the syntax and type system.
- Self-modifying code explains markers, bindings, and quines, which are what make Insert different from an ordinary language.