fastC
2026-06-06 · 11 min read · ecosystem, stdlib, agents, fastc-core

fastc-core — one curated answer per domain

Walking through the 11 packages of the fastc-core ecosystem, the v1.0.x cutover, and why we don't want 11 competing JSON libraries.

The previous five posts walked through the language. This one walks through the ecosystem — the eleven curated fastc-core packages, the v1.1 vendor cutover, and the argument for why “one answer per domain” is structural rather than a stylistic preference.

The thesis is unfashionable. Most languages compete on ecosystem breadth: “we have a crate for that.” fastC commits to the opposite — depth on a small named set, with an explicit “use the prelude for the obvious things; FFI to C for the unusual things” escape hatch. The bet is that the cost of choosing between eleven JSON libraries is real, it falls hardest on agents, and removing it is worth the breadth we give up.

We are honest that this bet loses the breadth comparison. Rust has 150,000+ crates. fastc-core has 11 packages. If your niche is not in the curated set and the corresponding C library is not what you want to FFI to, fastC is the wrong tool. The set we picked is the set we think covers the most-common production-systems-code needs.

Why “one curated answer” is structural

Every language ecosystem trades decision-load against expressivity. The trade is usually invisible to senior developers because they have already memoized which crate to reach for. It is very visible to LLM agents, because the model’s training data is built from the entire public corpus, and the model has no way to know which of the eleven JSON crates is the current idiomatic choice.

Ask an agent to add JSON parsing to a Rust project today and you will get one of: serde_json, simd-json, json, json-rust, tinyjson, nanoserde, rustc-serialize (deprecated, still in training data), rmp-serde (MessagePack, but the model misremembers), actix-web::web::Json (works only inside actix), axum::Json (works only inside axum), or jsonschema. The model picks based on training-data frequency, which loosely tracks Cargo downloads, which loosely tracks “what was popular two years ago.” The result is code that compiles but is not what your team uses.

The cost of this is a tax on every “add a feature” diff. The reviewer has to check that the new dep is the one the team uses. The agent has to be told, every time, which crate to reach for. The fastC answer is to make the choice once, at the language level, and ship the answer in the prelude.

The eleven packages

Each package has a public preview repo at github.com/Skelf-Research/fastc-core-<name> with a v0.1.0 release. The implementations today ship inside the fastC v1.0 prelude; the v1.1 cutover (covered below) moves them out into vendored consumption via fastc add. The API surface is stable from v0.1.0; the packaging is what changes.

cli — argv + flag parsing. Subcommands, long and short flags, required and optional positional args, structured help generation. The agent generating a CLI tool calls cli::parse(args, schema) and gets a typed struct back.

use cli;

fn main() -> i32 {
    let caps: Caps = caps::init();
    let args: Vec[Str] = env::argv(addr(caps.env_read));
    let parsed: res(MyArgs, cli::ParseError) =
        cli::parse[MyArgs](args, MyArgs::schema());
    // ...
    return 0;
}

log — structured leveled logging. Five levels (trace, debug, info, warn, error), typed key-value fields via kv_int / kv_str / kv_bool, JSON output by default, ANSI-colored text output behind a flag. No variadic; every field is typed.

log::info("request handled",
    log::kv_str("method", method),
    log::kv_int("status", status),
    log::kv_int("duration_us", elapsed_us));

json — JSON encode + integer-field decode. Encoding handles every value type. Decoding handles flat structures with integer, string, and boolean fields. Nested structures and arrays of records are supported; arbitrary serde-style derive-based decoding is intentionally out of scope. The thesis is that an agent generating a JSON-handling function should know what shape it expects, not derive over an opaque blob.

let body: Str = json::encode_object(
    json::field_str("name", name),
    json::field_int("count", count));

toml — read-only flat-table TOML. The decoder handles flat tables and arrays of flat tables. Nested tables work; arbitrary type annotations are out of scope. Writing TOML is not in the package — for config-file generation the project uses json instead. The asymmetry is deliberate: TOML is a read-config format, not a write-anything format.

http — HTTP/1.1 client through CapNetConnect. GET, POST, headers, response body as Str. TLS through mod tls (a thin wrapper on system TLS). No HTTP/2, no HTTP/3, no streaming bodies above a few megabytes. The client is synchronous; the v2.3 async layer will add a streaming version.

fn fetch(c: ref(CapNetConnect), url: Str) -> res(http::Response, http::Error) {
    return http::get(c, url);
}

time — wall-clock + ISO 8601 through CapTimeRead. Reads the system clock, formats and parses ISO 8601 strings. No timezone arithmetic beyond UTC offset; chrono-style timezone support is out of scope. The thesis is the same as toml: the common case is “log a timestamp” or “parse an API timestamp,” not “do calendar arithmetic across DST transitions.”

base64 — RFC 4648 encode/decode. Standard alphabet and URL-safe alphabet. Constant-time decode for the security-sensitive case (auth tokens). No streaming; the input and output are whole Str/Vec[u8] values.

uuid — RFC 4122 v4 + parse/format through CapRand. Generates v4 UUIDs from the system entropy source, parses canonical and hyphenated forms, formats either. No v1/v3/v5; the common case is v4 random UUIDs and the rest are special-purpose enough to write inline.

crypto-primitives — SHA-256 / HMAC / constant-time compare / random_bytes. The narrow set of crypto primitives a typical application needs: hash an input, verify an HMAC signature, compare two byte strings without timing leaks, read entropy. Not a general crypto library; for AEAD, asymmetric crypto, or anything past these four primitives, FFI to libsodium.

let mac: [u8; 32] = crypto::hmac_sha256(addr(key), addr(message));
let ok: bool = crypto::constant_time_eq(addr(mac), addr(expected_mac));

regex — Thompson NFA, no backreferences. The Rust regex crate’s safety profile (linear-time guarantee, no catastrophic backtracking) is the reference. We do not support backreferences, lookaround, or named-capture-with-rewriting. The compile step is at runtime; for static patterns the agent compiles once at startup and reuses.

sqlite — FFI bindings to libsqlite3 through CapFsWrite. A thin wrapper on libsqlite3.so / libsqlite3.dylib. Open through CapFsWrite (because SQLite writes its DB file), prepared statements, parameter binding, row iteration. The thesis is that SQLite is a stable enough C library that wrapping it in fastC bindings is the right move — versus trying to write a SQLite-equivalent in fastC, which is a five-year project.

The v1.0.x cutover

In v1.0, every fastc-core package ships inside the prelude. A new project gets use cli, use log, use json and so on with no further action — the modules are part of the compiler’s standard imports.

The v1.1 cutover moves the packages out of the prelude and into vendored consumption via fastc add:

$ fastc add fastc-core-cli
$ fastc add fastc-core-json

The fastc.toml gets an entry per added package; fastc.lock records the sha256 and cosign verification (per post 4); the vendor directory gets a copy. The compiler stops bundling the prelude versions and starts using the vendored versions.

The reason for the cutover is two-fold. First, it brings the curated packages onto the same supply-chain footing as third-party packages: cosign-signed, sha256-hashed, SLSA L3-attested, auditable. Second, it lets the packages move independently of the compiler — a fix in fastc-core-http can ship without bumping the compiler version, and a project that wants to pin to an older fastc-core-http can do so without pinning the compiler.

The transition is opt-in for one minor version. v1.0.x projects keep working unchanged; v1.1 projects can choose to migrate. v1.2 is the version where the prelude shrinks to the language-level types (opt, res, Str, Vec, the cap types) and the rest must be added explicitly. We expect most teams will migrate during the v1.1 window once they see the supply-chain benefit on the audit side.

The honest section

Eleven packages is a tiny ecosystem. Rust has 150,000+ crates; npm has more than a million; PyPI is in the hundreds of thousands. fastc-core is two orders of magnitude smaller than the smallest competitor. For ecosystem breadth, every other production language wins.

The trade we are making is that for application systems code — a CLI tool, an HTTP service, a config-file processor, a metrics shipper, a small daemon — the eleven packages cover the common case, and the FFI escape hatch covers the uncommon case at the cost of writing a binding. We are not claiming the trade is right for every workload. A team writing scientific computing code needs the Rust or Python ecosystem; a team writing game engines needs the C++ or Zig ecosystem; a team writing distributed databases probably needs Rust. fastC’s curated set is shaped for the workloads we built it for.

The “write your own binding” path is genuinely a path, not a brush-off. The FFI surface (post 2’s @repr(C)) is small, the C ABI is well-understood, and the curated packages themselves are partly FFI bindings (the sqlite package is the canonical example). The cost is the binding has to be written, reviewed, and maintained.

Where this lands

This is the post where fastC’s “language + curated stdlib” claim earns its name. The language alone is interesting; the language plus the eleven packages is what you actually build production systems on. The next steps are the v1.1 fastc-add flow (the cutover above) and the public repos at github.com/Skelf-Research/fastc-core-<name> filling out from “v0.1.0 with a stable API and the prelude implementation” to “v0.2.0 with the dedicated repo’s implementation, signed and attested per post 4.”

The six-post series ends here. The wedge is: capability-typed I/O, mandatory contracts, no executable build scripts, vendored content-hashed deps, one curated answer per domain, and an agent-tooling layer built on a unified diagnostic envelope. None of those properties can be retrofitted onto a language whose ecosystem is already built around the opposite assumptions. That is why the language exists.

If you read post 1, the framing was “what wedge does fastC take against C, Rust, Zig, and Go.” The wedge is narrow and specific: the working environment where the modal author of code is an LLM and the modal reviewer is a human under bandwidth pressure. If that is your environment, the wedge matters. If it is not, the other languages remain the right answers for their respective workloads. We are not asking to replace any of them. We are asking to occupy the slice of the working environment where the trade-offs we made are the trade-offs that pay.


Comments? Issues? Disagreements? Open an issue at github.com/Skelf-Research/fastc/issues.

← all posts