Skip to content

Proposal B — split by language

Status: accepted — adopted directly by the design record, reversing its earlier phased plan · This is Option E from that record, written up in full · Supersedes Proposal A — split by concern · See the section overview for the question both answer.

The idea: one repo per target language. comline-python holds the Python code generator, the Python library generator, the Python runtime, and any Python-specific standard-library extras — all together. core stays language-neutral, and generation shrinks to a shared helper crate that every language repo depends on.

Shape

comline-python/
  codegen/       the Python `code` generator (Rust) — frozen IR → .py sources
  libgen/        the Python `lib` generator (Rust) — pyproject.toml, FFI wrapper
  runtime/       comline-runtime-python — Rust core runtime + pyo3 glue
  std-extra/     Python-only runtime std additions
  conformance/   this target's schema → behaviour tests, one CI run

comline-lua/     … the same five directories
comline-node/    …

The only things every language repo shares are core (the language-agnostic IR and compiler) and a small comline-codegen support crate — the GeneratedFile type, the generator interface, the IR-walking helpers.

The case for it

The generator and the runtime have to agree, and here that's checked in one place. The generated Python code calls into the Python runtime across an FFI boundary, so the two sides have to line up exactly. Put them in the same repo and the same CI run builds the generator, runs it, builds the runtime, and runs the generated code against it — on every PR. No version table, no contract doc, no "did we remember to update the other repo". If the two don't match, CI is red, right there.

One obvious home per language. Someone who wants to improve Python support clones comline-python and it's all in front of them. No cross-repo checkouts, and no guessing whether a bug is in the generator or the runtime — both are in the same tree.

Native tooling, end to end. comline-python's CI already has CPython, maturin, and pytest set up, so the library-generator tests can build the wheel they generate and actually import it. That kind of real end-to-end check is awkward when the generator lives off in a Rust-only repo.

Adding a language is copying a template. A new target is a new repo from a template. It doesn't touch any existing repo, doesn't destabilise a shared workspace, and doesn't need anyone to wire up feature flags.

Everything about a language is independent — version numbers, release cadence, issue tracker, even the licence if some ecosystem is fussy about it.

The case against

It bundles a build tool with a shipped library just because they share a language. The code generator runs inside the toolchain on a developer's machine; the runtime ships to end users. Different audiences, different release rhythms, different security exposure — and now they're in one repo with one version number and one release.

The repo ends up bilingual with two toolchains. The Python code generator is light Rust that emits strings. Sitting it next to pyo3 and a CPython build means every CI run pulls in both, even for a change that only touches string formatting.

The shared codegen layer fragments. Every language repo still depends on comline-codegen. Change that interface and you're rolling it out across every language repo, one at a time — the same cross-repo coordination this layout was meant to avoid, just moved from the runtime side to the codegen side.

The CLI depends on a crate from every language repo. comline generate has to know about all of them, so the CLI pulls N generator crates from N repos, and a release of any one of them can move the CLI's lockfile.

You still need a cross-language test suite somewhere. "Do the Rust and Python generators treat an optional field the same way?" can't be answered inside a single language repo. That needs a corpus spanning all of them, and no one repo naturally owns it.

An IR change hits every repo. Change something in core and you're updating every comline-<lang> against it. The core ↔ target boundary becomes a public API you have to version carefully, because a lot of repos consume it.

Example projects

Scored against the same six traits: ✅ yes · 🟡 some languages · ❌ no.

Smithy — the IDL and SDK-generation framework behind the AWS SDKs — is built exactly this way. The language-neutral model tooling is in smithy-lang/smithy; then smithy-rs, smithy-typescript, smithy-go, smithy-swift, smithy-kotlin, smithy-python and others each hold both the code generator and the runtime libraries for that one language. AWS generates every one of its SDKs from this layout.

Trait
Neutral core in its own repo
All code generators in one repo
One repo per target language
Codegen + runtime together, per language
Heavy languages graduate; light ones stay bundled
One repo for the compiler + every language

Cap'n Proto does it for every language except its C++ reference. capnproto/capnproto is the C++ compiler and runtime; capnproto/capnproto-rust bundles the capnpc compiler plugin, the capnp runtime and the RPC layer; capnproto/capnproto-java bundles its plugin and its runtime. Each language repo carries its whole stack.

Trait
Neutral core in its own repo
All code generators in one repo
One repo per target language
Codegen + runtime together, per language
Heavy languages graduate; light ones stay bundled
One repo for the compiler + every language

protobuf-go and grpc-go show that even mono-repo-first projects organise their graduated languages this way. protocolbuffers/protobuf-go ships protoc-gen-go next to the google.golang.org/protobuf runtime; grpc/grpc-go ships protoc-gen-go-grpc next to the gRPC runtime. Codegen and runtime for the language, in one repo — Proposal B applied inside a Proposal A project.

Serde and kotlinx.serialization are the single-language version of the idea: the code generator — a derive macro, a compiler plugin — ships alongside the runtime library, serde with serde_derive, the kotlinx.serialization plugin with its core.

When this would be the right call

Once each language genuinely has its own maintainer or team, ecosystem-native packaging is non-negotiable across the board, and the core ↔ target contract has settled enough to be a real versioned API — at that point a per-language repo isn't overhead, it's just the unit people already work in. It's where Proposal A ends up for runtimes anyway; this proposal gets there sooner and brings codegen along too.