Mica 6.12.5 is released for Linux x86-64 and AArch64. Its headline is one sentence: Mica gets projects, and becomes a scripting language. A three-line mica.project beside your sources is read identically by the build, the editor, and the run mode — and one Mica file now runs as easily as a shell script. Underneath, the compiler itself was split into four modules with a backend SDK that frontends other than Mica’s own can drive.

But the part of this release we are proudest of is not a feature. Between the preview and this article lie review weeks in which the compiler’s own claims were attacked adversarially — five review lanes hunting data races, deadline holes, pointer misuse, hostile inputs, and debugger lies. Everything they found was fixed at the root before this page was written, and four of the findings were judged important enough to change the language itself.

The focus points

  • Projects — one mica.project, one truth for compiler, language server, and run mode alike.
  • Scriptingmica --run tool.mica, plain mica tool.mica, shebang scripts, extension-less commands on PATH.
  • The breakup — four modules (foundation, spectra, dragon, mica) and a backend SDK, proven by a second, non-Mica frontend.
  • The quality initiative — four ruled language changes, two silent-corruption miscompiles found and root-fixed, a category of optimizer bugs made structurally impossible, and every failure taught to name itself.

Projects: one folder, one truth

The project model is a small file with a large consequence: the compiler, the language server, and the run mode all read the same mica.project, so the diagnostics you see while typing are the diagnostics of your build — same encoding, same contracts, same libraries. One program can consume several separately built libraries, and a script beside your project imports them with no flags at all. The tutorial walks the whole road.

Scripting: one file, one command

mica tool.mica          # compile and run, argv and exit codes as a shell expects
./tool.mica             # with a #!/usr/bin/env mica line and chmod +x

The run mode compiles one file into a per-run scratch directory and executes it — argv, exit codes, stdin, here-documents, and interrupts behave exactly as a shell expects; Ctrl+C and supervisor stops reach your program, and nothing is left behind. The scripting tutorial has every detail.

The breakup: four modules and a backend SDK

The compiler is now four Go modules — foundation, spectra (the intermediate language), dragon (the backend), and mica (the frontend) — and the three lower modules never import the frontend. That is not an internal nicety: the backend has its own documented entry (dragon.Build), and the proof is a second frontend — Wirth’s PL/0 compiles to native executables through the same backend, checked byte-for-byte against its own oracle. The port ships as a fully commented reading copy in the tutorial repository’s sdk, with a guided reading order — and with the honest caveat that the Dragon and Spectra APIs are a preview and may change. The backend also gained an executable validity contract: dragon.Validate answers findings with rule ids and repair sentences, and Build refuses invalid input with the whole repair list. The breakup tells the full story.

Alongside it, the test harness that gates every Mica release moved into its own repository, Apache-2.0 licensed.

The review weeks: the compiler attacked its own claims

Five adversarial lanes ran against the release candidate: smuggling data races through every door, defeating scope deadlines, misusing pointers toward silent corruption, feeding the toolchain hostile input, and lying to the debugger. The score that matters: no crash and no internal error anywhere — and every finding fixed at the root, in this release, not in a backlog.

Four language rulings

The deepest findings changed the language, each ruled after studying what industry compilers do:

  • An element address of a dynamic array is a lend, never a binding. A dynamic array’s backing moves when it grows; a stored pointer keeps naming memory the array has left behind, and writes through it stop meeting writes through the element while both answers still look plausible. Inc(address a[i]) stays exactly as it reads; view := address a[i] is refused, as is lending an element address to a call that grows that same array.
  • A constant expression that overflows its own operand type is refused on every tier. -5 * Second used to wrap silently — a named constant’s value moved with the optimization tier. Both operands are constants, so the exact value exists at compile time; the message states it, and the width that holds it.
  • Every integral family may index an array. The narrowing cast at C boundaries — buffer[count as int32] — is gone, and its removal is the safety improvement: the cast truncated the index before the always-on bounds guard ever judged it. The whole value is now exactly what the guard judges.
  • A mark-bound alias may be lent to a callee that provably stores it nowhere. Shared state can finally be handed to a helper by pointer — but only when the compiler proves the pointer outlives nothing; storing it, a suspending callee, or an imported callee each stay refused with their own sentence.

The error report

Every release now publishes its error report — what newly refuses, and why. Refusals are a design surface in Mica, not an afterthought (refusals as design); the new ones follow the house rule that a refusal names the consequence and the remedy, not just the rule. In 6.12.5:

  • a stream type cannot be given a name — a stream is one live activation, not a shape; name the element type instead
  • a typed file transfer inside a deadline-armed scope earns a warning: the deadline moves on from a parked task, but blocking file stdio is not cancellable — the compiler now says so at compile time
  • user-written star forms (%.*ls, %*lld) in C format strings are refused — they used to print garbage with exit 0
  • a source file that is not honest UTF-8 refuses byte-precisely instead of being silently repaired or truncated
  • --stdlib naming an archive that contradicts --memory-class is refused with the consequence spelled out — it used to silently link a fixed-arena program with no arena at all
  • every hostile command line answers one sentence plus a hint, and the error report is bounded — a 100 MB garbage input refuses in about a second instead of timing out

Silent corruption, found and killed

The review found two miscompiles that answered wrong values with exit 0 — the worst class of bug a compiler can have. A function forwarding into its own result inside a for x in loop could answer another build’s values, or zeroes in a fixed arena; and a retry loop in the blocking-IO layer read a stale error code, discarding successful reads. Both are root-fixed and pinned by deterministic regression tests. Beyond the instances, one category is now structurally impossible: the backend computes which stack slots have their address taken, and every optimization that would drop a store consults that fact — the two optimizations once deleted for this bug class are back, soundly.

Failures name themselves

A failing program should read like a diagnosis, not a shrug. In 6.12.5: out-of-memory traps name the failing line on every road, including inside library helpers; stack exhaustion prints a named report instead of dying silently; when several tasks trap at once under multicore, exactly one complete banner is printed; and a scope deadline now fires whatever the task tree is doing — compute-bound children can no longer defeat it.

Small memory, long loops

Fixed-arena programs — the deployment class for running without a heap — now reclaim each loop iteration’s transients: a condition like while Length(Step(r)) > 0 runs in constant region space, where the same loop used to exhaust a 64 KB arena after a few hundred rounds. Record reassignment in a loop reuses its backing, and growing reassignment extends in place.

And at the checked tier, tensor arithmetic now traps integer overflow exactly as scalar arithmetic does — at the first offending element, with the statement’s source position. Debug and release wrap, unchanged and unpaid.

A changelog that ships with the package

Starting with this release, the package’s Debian changelog carries the release’s real story instead of a version stamp — apt changelog mica answers with the highlights above — and the build refuses to package a release whose story has not been written.

Getting 6.12.5

6.12.5 passed the full release gate before tagging: the complete corpus through the compiler (11,188/0 runs on the widest gate), the same corpus through the legacy lowering as a differential oracle (11,093/0), and the corpus under Go’s race detector (11,093/0) — on both architectures, plus the multicore gate (287/0).

Not in this release, so you do not go looking: the networking library is 6.13, and the Mica-native numeric stack is release 7.

Install it natively or as a container — and if the review weeks’ philosophy resonates, we would genuinely like to hear from you: info@mica-dev.com.