name: new-ball-language description: > End-to-end playbook for adding a new programming language to Ball. Covers directory scaffold, proto binding generation, compiler, encoder, self-hosted engine, package management, CLI integration, conformance tests, CI/CD, and documentation. USE FOR: bootstrapping a new language (e.g., Rust, Swift, Kotlin, Ruby), or auditing an incomplete language implementation against the checklist. DO NOT USE FOR: modifying an existing mature implementation (use the language-specific rule instead).
Adding a New Language to Ball — Complete Playbook
This skill walks you through every step of adding full Ball support for a new programming language. Follow the phases in order. Each phase has a checklist — complete every item before moving to the next phase.
Throughout this guide, <lang> is the lowercase language name (e.g., rust, swift, kotlin)
and <Lang> is the title-case form (e.g., Rust, Swift, Kotlin).
Phase 0: Prerequisites
Before starting, verify:
- Decide your protobuf strategy (detailed in Phase 1.2). Ball does NOT require your target language to have a protobuf library — if it lacks one, Ball compiles its OWN protobuf runtime to your language. Check buf.build/plugins for an official or community plugin; if none exists you still never need a lossy JSON-only fallback.
- You have the language's toolchain installed locally (compiler, package manager, test runner).
- You understand the 5 core Ball invariants (see
CLAUDE.md→ "Core Invariants"). - You've read at least one existing implementation for reference:
- Dart (most complete):
dart/compiler/,dart/encoder/,dart/engine/ - TypeScript (good second reference):
ts/compiler/,ts/encoder/,ts/engine/ - C++ (shows systems-language patterns):
cpp/compiler/,cpp/encoder/
- Dart (most complete):
Phase 1: Directory Scaffold & Proto Bindings
Ball's headline capability — protobuf for languages that don't have it. Ball does not depend on your target having a protobuf library. Ball ships its OWN complete protobuf runtime written in portable Dart that the encoder turns into a Ball program; any Ball program compiles to your target via the compiler you build in Phase 2. So even a language with zero protobuf support gets a working wire + JSON + gRPC runtime for free. There are two distinct layers — do not conflate them:
ball_protobuf— the FULL runtime (binary wire format, proto3 JSON, well-known types, editions defaults, gRPC framing). Sourcedart/shared/lib/protobuf/*.dart; encoded into a Ball program atdart/shared/ball_protobuf.json/.binbydart/encoder/bin/gen_ball_protobuf.dart. Descriptor-driven: messages are plain maps and a field-descriptor list drivesmarshal()(map→bytes) andunmarshal()(bytes→map), with a parallel proto3-JSON codec.ball_proto— the deterministic ACCESS-PATTERN compat module the self-hosted engine relies on (~101 base functions, no bodies): oneof discriminators (whichExpr…), presence checks (hasBody…), Struct access (getStructField…), proto3 defaults (ensureDefaults…). Sourcedart/shared/lib/ball_proto.dart; generated todart/shared/ball_proto.json/.binbydart/shared/bin/gen_ball_proto.dart.Rule of thumb:
ball_protobufis HOW bytes/JSON are (de)serialized;ball_protois HOW the self-hosted engine reads fields of an already-deserialized Ball program. You needball_prototo run the self-hosted engine (Phase 4); you needball_protobufonly if the target lacks a native protobuf library (decision tree in 1.2).
1.1 Create the directory structure
<lang>/
shared/ # Protobuf bindings + shared types
gen/ # Generated proto code (NEVER edit)
compiler/ # Ball → <Lang> compiler
src/
test/
encoder/ # <Lang> → Ball encoder
src/
test/
engine/ # Ball interpreter OR self-hosted engine wrapper
src/
test/
cli/ # Ball CLI entry point for this language
src/
AGENTS.md # Language-specific agent instructions
<package-manifest> # Cargo.toml / setup.py / go.mod / build.gradle / etc.
1.2 Choose a protobuf strategy
Pick the branch that matches your target language.
(a) Language HAS an official or community buf plugin (preferred). Add a plugin entry to the root
buf.gen.yaml, run buf generate, and verify bindings appear in <lang>/shared/gen/:
- remote: <plugin-path> # see the plugin-path table below
out: <lang>/shared/gen
# opt: paths=source_relative # language-specific options as needed
Plugin paths are not all under protocolbuffers/ — verify each at
buf.build/plugins:
| Language | Plugin path |
|---|---|
| Dart, Go, Java, Kotlin, Python, C++, C#, Ruby | buf.build/protocolbuffers/<lang> |
| TypeScript / JS | buf.build/bufbuild/es |
| Swift | buf.build/apple/swift |
| Rust | buf.build/community/neoeinstein-prost (community — there is no official protocolbuffers/rust) |
(b) Language has a protobuf runtime but no buf plugin. Add the community plugin to buf.gen.yaml
(remote: or local:), or depend on the language's protobuf runtime and hand-write only the thin
descriptor glue. You keep binary + JSON serialization.
(c) Language has NO protobuf support at all. This is exactly what Ball is built for — do NOT fall back to lossy JSON-only parsing. Compile Ball's own protobuf runtime to your target and use it:
- The committed Ball program is
dart/shared/ball_protobuf.json/.bin(15 modules: basestd/std_collections+ 12ball_protobuf.*runtime modules; entry moduleball_protobuf.marshal). Regenerate fromdart/withcd dart/encoder && dart run bin/gen_ball_protobuf.dart [out_dir]. - Compile it to your language with your Phase-2 compiler:
<lang>/compiler/compile dart/shared/ball_protobuf.json -o <lang>/shared/ball_protobuf.<ext> - You now have a native, full-fidelity runtime (binary wire + proto3 JSON + well-known types +
editions defaults + gRPC framing) with no external dependency. Call
marshal(message, descriptor)/unmarshal(bytes, descriptor); the descriptor format is shared by both codecs, so you keep both binary and JSON — you do NOT lose binary serialization.
Option (c) gives you the wire/JSON runtime; the self-hosted engine (Phase 4) additionally needs the
ball_proto access-pattern module regardless of which branch you chose here (see Phase 1.4).
1.3 Initialize the package manifest
Create the language's package manifest with these dependencies:
- Protobuf runtime library for the language
- Test framework
- Any code-generation library needed by the compiler (equivalent of Dart's
code_builder)
Examples by language:
| Language | Manifest | Protobuf Runtime | Test Framework |
|---|---|---|---|
| Rust | Cargo.toml |
prost |
built-in #[test] |
| Go | go.mod |
google.golang.org/protobuf |
built-in testing |
| Python | pyproject.toml |
protobuf |
pytest |
| Kotlin/Java | build.gradle.kts |
com.google.protobuf:protobuf-java |
junit |
| Swift | Package.swift |
swift-protobuf |
XCTest |
| Ruby | Gemfile |
google-protobuf |
rspec |
| C# | *.csproj |
Google.Protobuf |
xunit |
1.4 Create shared utilities
In <lang>/shared/, create helper utilities that compiler, encoder, and engine all need:
Ball value types — runtime representation of Ball values:
BallValue— polymorphic value (int, double, string, bool, null, list, map, function)BallList— ordered collection of BallValuesBallMap— ordered string-keyed map (use ordered map, NOT hash map)BallFunction— callable that takes BallValue and returns BallValue
Module builders — functions that construct std module protobuf messages:
buildStdModule()— builds thestdmodule with all 118 base function signaturesbuildStdCollectionsModule()— 53 collection functionsbuildStdIoModule()— 10 I/O functions- Reference:
dart/shared/lib/std.darthas the canonical definitions
Field extraction — helper to extract named fields from a
MessageCreationexpression:extractFields(MessageCreation) → Map<String, Expression>Protobuf runtime + compat-module artifacts — wire up these committed Ball artifacts per your 1.2 choice:
Artifact (committed) Source Regenerate with Purpose dart/shared/ball_protobuf.json/.bindart/shared/lib/protobuf/*.dartmelos run regen-protobufFull protobuf runtime as a Ball program. Compile to your language ONLY if it lacks native protobuf (1.2 branch (c)). dart/shared/ball_proto.json/.bindart/shared/lib/ball_proto.dartcd dart/shared && dart run bin/gen_ball_proto.dartThe ball_protoaccess-pattern module (oneof discriminators, presence checks, Struct access, proto3 defaults). ALWAYS needed to run the self-hosted engine; implement its ~101 base functions per-platform in Phase 4.The
ball_protobase functions haveisBase: trueand no body — you supply native implementations. In Dart they map onto the protobuf-generated methods (obj.whichExpr(),obj.hasBody()); for a map-based representation implement them against your map type (whichExpr(obj)→ the set oneof field name or"notSet";hasBody(obj)→ whetherbodyis present and non-default; see the variant/field lists indart/shared/lib/ball_proto.dart).
Phase 1 checklist:
- Directory structure created
- Protobuf strategy chosen via the 1.2 decision tree (buf plugin / community runtime / compile
ball_protobuf) - If a buf plugin is used:
buf.gen.yamlupdated andbuf generateproduces<lang>/shared/gen/ - If branch (c):
ball_protobuf.jsoncompiled to<lang>/shared/and a sample message round-trips (marshal → unmarshal) - Package manifest created with protobuf runtime dependency
- BallValue / BallList / BallMap / BallFunction types defined
- Module builders created (or planned for later — at minimum
buildStdModule()) - Field extraction helper implemented
-
ball_protomodule accounted for (implemented in Phase 4 for the self-hosted engine)
Phase 2: Compiler (Ball → Target Language)
The compiler reads a Program protobuf and emits target-language source code.
2.1 Core compiler structure
compile(Program) → String (or multiple files)
1. Build lookup tables: types by name, functions by (module, name)
2. Identify base modules: std, std_collections, std_io, std_memory
3. Generate imports / preamble
4. For each module (skip base modules):
a. Generate types from typeDefs[]
b. Generate functions (compile expression trees)
5. Generate entry point: call entryModule.entryFunction
2.2 Expression compilation
Implement a recursive compileExpression(Expression) → String that handles all 7 types:
| Expression | Strategy |
|---|---|
literal |
Emit native literal (42, "hello", true, null, [1,2,3]) |
reference |
Emit variable name; special-case "input" → function parameter name |
fieldAccess |
compileExpr(object) + "." + fieldName |
messageCreation |
Constructor call or struct/map literal with compiled field values |
call (user fn) |
functionName(compileExpr(input)) |
call (base fn) |
Dispatch table — map to native operators/constructs (see 2.3) |
block |
Scoped block: let-bindings as variable declarations, statements, result expression |
lambda |
Anonymous function / closure capturing lexical scope |
2.3 Base function dispatch (CRITICAL)
This is the heart of the compiler. Create a dispatch function:
compileBaseCall(module, functionName, inputFields) → String
Control flow functions MUST use lazy evaluation — emit native control flow, NOT function calls:
| Function | Compilation Pattern |
|---|---|
std.if |
if (condition) { then } else { else } — NEVER evaluate both branches |
std.for |
for (init; condition; update) { body } |
std.while |
while (condition) { body } |
std.for_each |
for (item in iterable) { body } |
std.try |
try { body } catch(e) { catch } finally { finally } |
std.switch |
switch (value) { case ...: ... } |
std.and |
left && right (short-circuit) |
std.or |
left || right (short-circuit) |
Arithmetic / comparison / logic — emit binary operators:
| Function | Pattern |
|---|---|
std.add |
left + right |
std.subtract |
left - right |
std.multiply |
left * right |
std.divide |
left / right |
std.modulo |
left % right |
std.equals |
left == right |
std.not_equals |
left != right |
std.less_than |
left < right |
std.greater_than |
left > right |
std.negate |
-operand |
std.not |
!operand |
String / print / type ops — emit method calls or built-in functions.
Reference: dart/compiler/lib/compiler.dart _compileBaseCall() has the complete dispatch for
all 118+ std functions.
2.4 Type emission
Read metadata.kind on each TypeDefinition to determine what to emit:
kind |
What to generate |
|---|---|
"class" |
Class with fields, constructors, methods |
"abstract_class" |
Abstract class / interface / protocol |
"enum" |
Enum type |
"mixin" |
Mixin / trait / protocol extension (if language supports it) |
"extension" |
Extension methods (if language supports it, otherwise static utils) |
Fields come from the TypeDefinition.descriptor (a protobuf DescriptorProto).
Methods are functions in the same module whose metadata has is_method: true and
owner_type: "TypeName".
2.5 Multi-module output
For languages that use one-file-per-module (Go, Java, Kotlin, C#, Rust), implement:
compileAllModules(Program) → Map<String, String> // moduleName → sourceCode
Phase 2 checklist:
-
compile(Program) → Stringfunction implemented - All 7 expression types handled recursively
- Base function dispatch covers at minimum: arithmetic (6), comparison (6), logic (3),
print,if,for,while,for_each,assign,index,try,return,break,continue,throw - Control flow uses lazy evaluation (NOT eager)
- Type emission handles class, enum, abstract_class at minimum
- Lambda compilation works (FunctionDefinition with empty name)
-
"input"reference maps to function parameter - Empty module in FunctionCall resolves to current module
- Tests pass for basic programs (hello_world, fibonacci, factorial)
Phase 3: Encoder (Target Language → Ball)
The encoder reads source code and produces a Ball Program protobuf.
3.1 Parser strategy
Choose a parsing approach for the target language:
| Strategy | Examples | Pros | Cons |
|---|---|---|---|
| Official AST API | Dart (analyzer), TS (ts-morph), Rust (syn) |
Accurate, maintained | Tight coupling to toolchain |
| Language server protocol | Any language with LSP | Standardized | Slow, complex setup |
| Compiler JSON AST | C++ (clang -ast-dump=json), Swift (swiftc -dump-ast) |
Detailed | Fragile output format |
| Tree-sitter grammar | Most languages | Fast, universal | Less semantic info |
3.2 AST → Ball mapping
Walk the parsed AST and map each construct:
| Source Construct | Ball Expression |
|---|---|
Binary operator a + b |
call(std, add, messageCreation({left: a, right: b})) |
Function call f(x) |
call(module, f, x) |
| Variable declaration | LetBinding inside a Block |
| If statement | call(std, if, messageCreation({condition, then, else})) |
| For loop | call(std, for, messageCreation({init, condition, update, body})) |
| Class definition | TypeDefinition with DescriptorProto for fields |
| Method | FunctionDefinition with is_method: true in metadata |
| Lambda / closure | FunctionDefinition with name "" |
3.3 Universal module architecture (no language-specific base modules)
Language-specific base modules (dart_std, cpp_std) have been eliminated from Ball.
All constructs must route through universal modules (std, std_collections, std_io,
std_memory). When the source language has constructs that don't map directly to a single
std call, the encoder should expand them into equivalent std expression trees at
encoding time. For example:
- Dart's
?.(null-aware access) expands tostd.if(std.is_null(target), null, fieldAccess) - Dart's
..(cascade) expands to aBlockwith a temp variable and sequential calls - C++ pointer dereference inlines to
std_memoryoperations or field access
Do NOT create <lang>_std base modules. This is a core design principle: encoders must
produce programs that any target compiler/engine can execute without language-specific handlers.
3.4 Metadata preservation
For round-trip fidelity, store cosmetic information in metadata:
- Function metadata:
visibility,is_async,is_static,is_abstract,annotations,return_type,type_parameters - Type metadata:
kind,superclass,interfaces,mixins,is_abstract,is_sealed - Let-binding metadata:
type,is_final,is_const,is_late - Module metadata: language-specific import/export info
3.5 Std module accumulation
As the encoder walks the AST, it tracks which std functions are referenced. After encoding,
call buildStdModules() to construct only the modules actually used by the program. This keeps
encoded programs minimal.
Phase 3 checklist:
- Parser chosen and integrated
- All major AST node types mapped to Ball expressions
- Operator mapping complete (arithmetic, comparison, logic, assignment)
- Control flow encoded as std function calls (if, for, while, etc.)
- Types encoded as TypeDefinitions with DescriptorProto fields
- All constructs route through universal
std(no<lang>_stdbase modules) - Metadata preserved for round-trip fidelity
- Std modules accumulated from actual usage
- Round-trip test: encode → compile → verify output matches original semantics
Phase 4: Engine (Ball Interpreter / Self-Hosted Engine)
You have two options for the engine. Option B (self-hosted) is strongly recommended for new languages — it gives you a working engine with minimal effort.
Option A: Hand-written engine (tree-walking interpreter)
Implement from scratch. Required components:
Scope chain: Lexical scoping with parent pointers
Scope { bindings: Map<String, BallValue>, parent: Scope? } lookup(name) → walk parent chain bind(name, value) → add to current scope set(name, value) → find and update in chainExpression evaluator: Recursive
eval(Expression, Scope) → BallValueModule handler interface:
interface BallModuleHandler { handles(moduleName: String) → bool call(functionName: String, input: BallValue, engine: BallCallable) → BallValue }Std module handler: Dispatch all 118+ std functions to native implementations. Must implement lazy evaluation for control flow (if, for, while, etc.).
Flow signals: Break/continue/return propagate as special values up the call stack.
FlowSignal { kind: "break"|"continue"|"return", label?: String, value?: BallValue }Virtual properties: Built-in properties on native types (
.length,.isEmpty, etc.)
Option B: Self-hosted engine (RECOMMENDED)
The Dart reference engine is self-encoded as a Ball program at dart/self_host/engine.ball.json.
Compile it to your target language using your compiler from Phase 2:
cd <lang>/compiler
<lang-run-command> compile ../../dart/self_host/engine.ball.json -o ../engine/src/compiled_engine.<ext>
Then create a thin wrapper (<lang>/engine/src/index.<ext>) that:
- Loads and deserializes Ball programs (proto3 JSON or binary)
- Calls the compiled engine's entry point
- Provides std function implementations that the compiled engine calls back into
- Handles I/O (print, file ops, etc.) via platform-native code
Reference wrappers:
- TypeScript:
ts/engine/src/index.ts— wrapscompiled_engine.ts - C++:
cpp/shared/include/ball_dyn.h+dart/self_host/lib/engine_rt.cpp
Self-host regeneration command (add to your build tooling):
# Regenerate the self-hosted engine whenever the Dart engine changes
cd dart && dart run compiler/tool/compile_engine_<lang>.dart
# Or use your own compiler:
<lang>/compiler/compile ../../dart/self_host/engine.ball.json -o <lang>/engine/src/compiled_engine.<ext>
Phase 4 checklist:
- Engine can execute
hello_world.ball.jsonand produce correct output - Engine can execute
fibonacci.ball.jsoncorrectly - All control flow works: if/else, for, while, break, continue, return
- Scoping is correct: closures capture lexical scope
- Flow signals propagate correctly through nested expressions
- Virtual properties work on strings, lists, maps
- Engine passes all conformance tests in
tests/conformance/(no tolerated failures — a partial pass rate is progress toward this bar, not completion)
Phase 5: CLI Integration
5.1 CLI entry point
Create <lang>/cli/ with a CLI that supports these subcommands:
| Command | Description |
|---|---|
ball run <file.ball.json> |
Execute a Ball program |
ball compile <file.ball.json> -o <output> |
Compile Ball to target language |
ball encode <source-file> -o <output.ball.json> |
Encode source to Ball |
ball check <file.ball.json> |
Validate a Ball program (type-check, lint) |
5.2 Input formats
Support both:
- Proto3 JSON (
.ball.json) — human-readable, used for examples and debugging - Binary protobuf (
.ball.bin) — compact, used for distribution
5.3 Exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Runtime error (program threw an exception) |
| 2 | Compilation error (invalid Ball program) |
| 3 | I/O error (file not found, permission denied) |
Phase 5 checklist:
-
ball runexecutes programs and prints output to stdout -
ball compilegenerates target-language source -
ball encodeparses source and produces.ball.json - Both
.ball.jsonand.ball.bininputs are supported - Exit codes follow the convention above
-
--helpworks for all subcommands
Phase 6: Conformance Tests
6.1 Conformance test runner
Conformance tests live in tests/conformance/. Each is a .ball.json program with expected
output. Create a test runner that:
- Discovers all
tests/conformance/*.ball.jsonfiles - Runs each through the engine
- Compares stdout output against
tests/conformance/*.expected(or inline expected output) - Reports pass/fail with counts
Output format (for CI matrix parsing):
Results: <passed> passed, <failed> failed, <total> total
6.2 Compiler conformance
Additionally test the compiler path:
- Compile each
.ball.jsonto target language source - Execute the compiled source using the language's native toolchain
- Compare output against expected
6.3 Round-trip conformance
For the encoder, test the round-trip:
- Compile
.ball.json→ target language source - Encode that source back →
.ball.json - Run the re-encoded program through the Dart reference engine
- Verify output matches
Phase 6 checklist:
- Conformance test runner discovers all
tests/conformance/*.ball.json - Engine conformance: run each program, compare output
- Track pass/fail counts with
Results: N passed, M failed, T totaloutput format - At least
hello_world,fibonacci,factorial,string_opspass - Compiler conformance: compile → execute → compare
- Round-trip test exists (even if not all tests pass yet)
Phase 7: CI/CD Integration
7.1 Add to ci.yml
Add a new job to .github/workflows/ci.yml following this template:
<lang>:
name: <Lang>
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# Setup language toolchain
- name: Setup <Lang>
uses: <official-setup-action>
with:
<lang>-version: <version>
# Setup buf for proto generation (if needed at build time)
- uses: bufbuild/buf-action@v1
with:
setup_only: true
# Install dependencies
- name: Install dependencies
run: <package-install-command>
# Build
- name: Build
run: <build-command>
# Test
- name: Run tests
run: <test-command>
# Conformance
- name: Conformance tests
run: <conformance-test-command>
Use the current setup action for the language (verified majors, May 2026 — actions-rs/* is
deprecated, do not use it):
| Language | Setup action (uses:) |
|---|---|
| Dart | dart-lang/setup-dart@v1 |
| Node / TypeScript | actions/setup-node@v6 |
| Python | actions/setup-python@v6 |
| Go | actions/setup-go@v6 |
| Java / Kotlin | actions/setup-java@v5 (with distribution: temurin) |
| Rust | dtolnay/rust-toolchain@stable |
| Swift | swift-actions/setup-swift@v3 |
| Ruby | ruby/setup-ruby@v1 |
7.2 Add to conformance-matrix.yml
Add a new job and wire it into the summary matrix:
<lang>-engine:
name: <Lang> Engine
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup <Lang>
uses: <setup-action>
- name: Install & build
run: <build-commands>
- name: Run conformance tests
id: conformance
run: |
set +e
output=$(<conformance-test-command> 2>&1)
exit_code=$?
echo "$output"
passed=$(echo "$output" | grep -oP 'Results: \K\d+(?= passed)')
failed=$(echo "$output" | grep -oP ', \K\d+(?= failed)')
total=$(echo "$output" | grep -oP ', \K\d+(?= total)')
echo "passed=${passed:-0}" >> "$GITHUB_OUTPUT"
echo "failed=${failed:-0}" >> "$GITHUB_OUTPUT"
echo "total=${total:-0}" >> "$GITHUB_OUTPUT"
exit $exit_code
outputs:
passed: ${{ steps.conformance.outputs.passed }}
failed: ${{ steps.conformance.outputs.failed }}
total: ${{ steps.conformance.outputs.total }}
Then add the new engine to the summary job's needs: list, add a print_row call, and
include it in the failure check.
7.3 Package publishing (optional)
If the language has a public package registry, create a publish workflow:
Modern registries favor OIDC trusted publishing (tokenless, short-lived credentials) over
long-lived secrets. Any OIDC publish job needs permissions: id-token: write.
| Language | Registry | Auth Method (current → legacy fallback) |
|---|---|---|
| TypeScript | npm | OIDC trusted publishing (GA 2025-07-31) → NODE_AUTH_TOKEN |
| Rust | crates.io | OIDC trusted publishing (rust-lang/crates-io-auth-action@v1) → CARGO_REGISTRY_TOKEN |
| Python | PyPI | OIDC trusted publishing → API token |
| Go | proxy.golang.org | Auto (tag-based; no auth/upload) |
| Java/Kotlin | Maven Central | GPG signing + Sonatype Central Portal token (central.sonatype.com). NOTE: legacy OSSRH/s01.oss.sonatype.org was sunset 2025-06-30 — do not use it |
| C# | NuGet | OIDC trusted publishing (short-lived key) → NUGET_API_KEY |
| Ruby | RubyGems | OIDC trusted publishing (rubygems/configure-rubygems-credentials) → GEM_HOST_API_KEY |
| Swift | Swift Package Index | Auto (git-tag discovery; index, not a hosted registry — no auth) |
Phase 7 checklist:
- Job added to
ci.yml— builds and tests on every PR - Job added to
conformance-matrix.yml— tracked in parity matrix - Summary job updated with new engine row
- (Optional) Publish workflow created for the package registry
- All CI jobs pass on a clean checkout
Phase 8: Documentation & Agent Configs
8.1 Create <lang>/AGENTS.md
Follow the pattern from dart/AGENTS.md or ts/AGENTS.md:
# <Lang> Ball Implementation
## Packages
- `<lang>/shared/` — Protobuf bindings and shared types
- `<lang>/compiler/` — Ball → <Lang> compiler
- `<lang>/encoder/` — <Lang> → Ball encoder
- `<lang>/engine/` — Ball interpreter / self-hosted engine
- `<lang>/cli/` — CLI entry point
## Build & Test
<build and test commands>
## Generated Files — NEVER Edit
- `<lang>/shared/gen/` — Protobuf generated types
- `<lang>/engine/src/compiled_engine.<ext>` — Self-hosted engine (if applicable)
## Testing
- Conformance tests: `<conformance-test-command>`
- Unit tests: `<unit-test-command>`
- Prefer conformance tests over unit tests
## Architecture
<brief description of key design decisions>
8.2 Create .claude/rules/<lang>.md
---
paths:
- "<lang>/**"
---
# <Lang>-Specific Instructions
## Package Structure
<description of packages and their roles>
## Key Patterns
### Compiler
<compiler patterns>
### Encoder
<encoder patterns>
### Engine
<engine patterns>
## Generated Files — NEVER Edit
<list of generated files>
## Testing
<test commands and patterns>
## Dependencies
<key dependencies>
8.3 Update root CLAUDE.md
Add the new language to the "Build & Test" section and the "Architecture Big Picture" section.
8.4 Update root AGENTS.md
Add the new language to the "Project Context" section.
Phase 8 checklist:
-
<lang>/AGENTS.mdcreated -
.claude/rules/<lang>.mdcreated - Root
CLAUDE.mdupdated with build/test commands - Root
AGENTS.mdupdated with new language in project context - All existing documentation still accurate after changes
Quality Gates
Before declaring a new language "complete", verify:
| Gate | Minimum | Target |
|---|---|---|
| Compiler: basic programs | hello_world, fibonacci, factorial | All examples/ compile |
| Engine: conformance | core programs (hello_world, fibonacci, factorial) run | 100% of tests/conformance/ (parity with Dart) |
| Encoder: round-trip | 3+ programs round-trip correctly | All examples/ round-trip |
| CI: all jobs green | Build + test pass | Conformance matrix row added |
| Docs: agent-ready | AGENTS.md + rule file exist | Full CLAUDE.md integration |
Reference Implementation Cross-Links
| Component | Dart Reference | TS Reference | C++ Reference |
|---|---|---|---|
| Compiler | dart/compiler/lib/compiler.dart |
ts/compiler/src/compiler.ts |
cpp/compiler/src/compiler.cpp |
| Encoder | dart/encoder/lib/encoder.dart |
ts/encoder/src/encoder.ts |
cpp/encoder/src/encoder.cpp |
| Engine | dart/engine/lib/engine.dart |
ts/engine/src/index.ts |
dart/self_host/lib/engine_rt.cpp |
| Std module | dart/shared/lib/std.dart |
(generated from Dart) | cpp/shared/include/build_std_module.h |
| CLI | dart/cli/ |
ts/cli/ |
N/A |
| Tests | dart/engine/test/ |
ts/engine/test/ |
cpp/test/ |
| Self-host | dart/self_host/engine.ball.json |
ts/engine/src/compiled_engine.ts |
dart/self_host/lib/engine_rt.cpp |