fastC
2026-05-15 · 10 min read · agents, tooling, mcp, diagnostics

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:

  • @owns uniqueness. No two modules may claim the same file glob. The compiler builds the ownership map and fails on overlap.
  • @depends exhaustiveness. Every import the module uses must appear in @depends. Imports that are declared but unused fail.
  • @arch DAG layering. The @arch values form a layered architecture; the compiler builds the architecture DAG from the project’s modules and refuses imports that violate the layering. (A parsing-layer module may not import a cli-layer module.)

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.json so the next agent iteration can see what was changed.
  • The fix-it set is enumerable: fastc fix --list prints 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 entry
  • check — run fastc check on the project, return structured diagnostics
  • context — run fastc context on a module, return the operating-manual JSON
  • diff — run fastc diff between two revs, return the semantic diff
  • caps_summary — return the caps.json artifact 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.

← all posts