What v1.3 added — the agent-tooling layer
Stage 1.3 annotations, the v1.0.x close-out — `fastc fix` / `context` / `diff`, expanded MCP, the unified diagnostic envelope.
The structural-safety wedge (post 3) and the supply-chain wedge (post 4) are the parts of fastC that change the cost of trust. This post is the part of fastC that changes the cost of iteration. Stage 1.3 added the function-level and module-level annotations that make a fastC signature a complete operating manual. The v1.0.x close-out shipped the agent-side surface: fastc fix, fastc context, fastc diff, an expanded MCP server, the unified diagnostic envelope.
The argument here is narrower than the safety wedge. Rust’s clippy + rust-analyzer + cargo-fix stack is mature, well-funded, and ahead of fastC on depth-of-coverage. We are honest about that. What fastC has is the structural foundations laid out from day one — one diagnostic envelope, fix-its first-class in the diagnostic, MCP as a primary protocol surface, signatures that carry the whole operating manual. The depth fills in incrementally; the structure does not have to be retrofitted.
Stage 1.3 — function-level annotations
The four annotations that landed in stage 1.3, in addition to the @requires / @ensures from stage 1.5 and the @caps summary from stage 1.4:
@purity(pure | effect | io). pure means the function does not call into the I/O+allocator banned set: no fs::*, no net::*, no proc::*, no time::*, no env::*, no mem::alloc. effect means the function does memory effects (allocates, mutates through mref(T)) but no I/O. io is the default for functions that do anything I/O-shaped.
The enforcement is real for pure. The compiler walks the call graph (within the crate; FFI calls are treated conservatively) and rejects any function tagged @purity(pure) that transitively calls a banned function. The honest caveat is that the banned set is hand-maintained — it is the set of stdlib functions whose signatures take an I/O capability or call into mem::alloc. A new stdlib function added without the right marker is a soundness hole until we catch it; we have a CI check that enforces the markers on the stdlib itself.
@panics(never | always | on=expr). never means the function does not reach a trap — no fc_trap() call, no panic from a stdlib function that is in the trap-emitting set. always is for unreachable!()-style markers. on=expr is documentation: the function may panic when the expression is true, and the reviewer can read the condition.
never is the one that interacts with the contract discharger. If @panics(never) is on a function whose @requires discharges at runtime (tier 3), the build fails — the runtime trap from a failed @requires is, definitionally, a panic the function would emit. The fix is either to write a body that lets the discharger prove the precondition statically, or to relax @panics(never) to @panics(on=...).
@complexity(O(<shape>)). Documentation only, but it flows through fastc explain JSON and into MCP. An agent querying the complexity of vec::push gets O(1) amortized as a typed answer. The compiler does not verify the complexity (we have no plans to — proving asymptotic complexity statically is its own research field). The value is the structured documentation.
@mem(arena=<ident>). Documentation that names the arena a function allocates into. Today this is a comment that flows through fastc explain. The actual arena-aware allocator enforcement lands in v2.x; the annotation is the surface so that the v2.x check has somewhere to bind to.
Stage 1.3 — module-level mandatory headers
Every fastC module declares a header comment with six fields:
//! @module my_app::parser
//! @owns data/parser/*.fc
//! @arch parsing-layer
//! @depends my_app::lex, fastc-core::str
//! @threading single-threaded
//! @invariants UTF-8 input only; never allocates after init
The header is lenient by default — modules without it warn, but compile. The strict mode is opt-in via the manifest:
[package]
strict_modules = true
In strict mode, missing headers are errors. The cross-module checks that run regardless:
@ownsuniqueness. No two modules may claim the same file glob. The compiler builds the ownership map and fails on overlap.@dependsexhaustiveness. Every import the module uses must appear in@depends. Imports that are declared but unused fail.@archDAG layering. The@archvalues form a layered architecture; the compiler builds the architecture DAG from the project’s modules and refuses imports that violate the layering. (Aparsing-layermodule may not import acli-layermodule.)
The architectural-layering check is the one we use the most internally. It catches the agent’s natural temptation to reach upward in the layer stack when the easy fix is in a higher layer. The error message names the offending module and the offending import and suggests refactor paths.
The v1.0.x close-out — fastc fix
fastc fix walks the diagnostic stream, collects every fix-it, and applies them in batch. The structured Fixit registry is the new piece: every diagnostic that ships a fix-it does so through a typed Fixit { span, replacement, rationale } struct, not a free-form string. The registry guarantees that:
- Two fix-its on overlapping spans never apply simultaneously (the conflict is detected and the second is skipped with a warning).
- Every applied fix-it is recorded in
.fastc/fix-history.jsonso the next agent iteration can see what was changed. - The fix-it set is enumerable:
fastc fix --listprints every fix-it kind the compiler knows about.
The honest caveat is the per-diagnostic backfill is ongoing. Roughly 60% of the diagnostic kinds have first-class fix-its today; the rest fall back to a fix-it that points at the relevant docs page. We are filling in the rest in priority order (most-common diagnostics first).
fastc context
fastc context <path> emits a JSON blob designed for AI context windows. It packages: the module header, the public signatures of every function in the module (with annotations), the caps.json extract for those functions, the discharge.json extract for those functions, and the most-recent diagnostics from .fastc/diag-cache.
The shape:
{
"version": 1,
"module": "my_app::parser",
"header": { "owns": "data/parser/*.fc", "arch": "parsing-layer", "...": "..." },
"signatures": [
{
"name": "my_app::parser::parse",
"params": [{"name": "src", "type": "raw(u8)"}, {"name": "n", "type": "usize"}],
"ret": "res(Ast, ParseError)",
"annotations": {
"purity": "pure",
"panics": "never",
"complexity": "O(n)",
"requires": ["n > 0"],
"ensures": []
},
"caps": []
}
],
"recent_diagnostics": []
}
The agent that calls fastc context instead of cat-ing the whole module gets the operating-manual subset, in JSON, with a token budget about a tenth of the full source.
fastc diff
fastc diff <old> <new> does AST-level semantic diff. Two source files that differ only in whitespace, comment placement, or let-vs-let parenthesization produce an empty semantic diff. Two source files that change the order of two unrelated top-level items produce a diff that says “reordered” rather than “deleted then added.” The output is JSON-structured by AST node, with the same envelope shape every other fastC tool uses.
The use case is the agent-review loop: the agent sees the actual semantic change, not the textual diff. PRs whose textual diff is two thousand lines (because the formatter ran) but whose semantic diff is fifteen nodes review in fifteen minutes instead of an hour.
Expanded MCP — fastc-mcp
The Model Context Protocol server fastc-mcp exposes the build’s typed artifacts as MCP resources and the build commands as MCP tools. The v1.0.x close-out expanded the tool surface:
explain— given an error code or fix-it kind, return the structured docs entrycheck— runfastc checkon the project, return structured diagnosticscontext— runfastc contexton a module, return the operating-manual JSONdiff— runfastc diffbetween two revs, return the semantic diffcaps_summary— return thecaps.jsonartifact for the current build
Claude Code, Cursor, and Codex bind to fastc-mcp and get all of the above as typed tools instead of text-parsing CLI output. The loop tightens: the agent asks “what does this error mean?”, the MCP returns the structured explain entry with the fix-it text inline, the agent applies the fix and re-runs check. No grep, no awk, no English-language compiler output to disambiguate.
LSP — code-actions, semantic-tokens, rename
The LSP side of the close-out: code-actions advertised for every fix-it the compiler emits; semantic-tokens for accurate syntax highlighting (@requires and @ensures get their own token class, distinct from regular identifiers); rename capability advertised across the workspace (a rename of a function ripples to every use import and every call site).
The code-actions integration is the one we get the most feedback on. An editor showing the fix-it as a one-click suggestion turns a class of recurring diagnostics (missing cast, forgotten discard, capability not in scope) into edits the developer accepts without context-switching.
The unified diagnostic envelope
Every fastC error kind serializes through one JSON shape:
{
"code": "E0410",
"category": "capability",
"severity": "error",
"message": "cannot call `fs::read` from a function that does not carry `CapFsRead`",
"spans": [
{
"file": "src/parse_args.fc",
"line": 14,
"column": 27,
"length": 8,
"label": "requires `ref(CapFsRead)`"
}
],
"fixits": [
{
"span": {"file": "src/parse_args.fc", "line": 12, "column": 14, "length": 0},
"replacement": "c: ref(CapFsRead), ",
"rationale": "Add the capability to the function signature."
}
],
"related_docs": ["docs.skelfresearch.com/fastc/errors/E0410"]
}
The envelope is one shape across fastc check, fastc build, fastc fix, the LSP, the MCP, and the JSON output of every other subcommand. The agent loop, the CI hook, and the editor surface all consume one shape instead of five. The reviewer reading a CI log gets the same fields the agent sees.
The honest section
fastC’s agent tooling is real but not yet at the depth of Rust’s clippy + rust-analyzer + cargo-fix stack. clippy has hundreds of lints with custom-tailored messages; we have dozens. rust-analyzer has years of editor-integration polish; ours is solid but newer. The Rust ecosystem has a wealth of third-party agent-loop integrations; ours are mostly first-party.
What fastC has is the structural foundations — one envelope, MCP as primary, fix-its first-class, signatures as operating manuals — laid out from day one. The depth backfills incrementally as we add per-diagnostic fix-its, lint kinds, and editor polish. The structure does not have to be retrofitted. That is the bet.
The next post is fastc-core — the eleven curated packages of the ecosystem, the v1.1 vendor cutover, and the one-answer-per-domain thesis that ties everything above into the working surface for application code.
Comments? Issues? Disagreements? Open an issue at github.com/Skelf-Research/fastc/issues.