Project 8: An Upstream Contribution

Get a change merged into a real language runtime.

Effort: unbounded, and that is the point. Value: it is the difference between "I built a toy runtime" and "I contribute to language runtimes" — and after four months you are genuinely qualified, which is unusual for this domain.


Why You Are Ready

Language runtimes are intimidating to contribute to because the entry cost is high: you cannot fix a GC bug without understanding the collector, and you cannot understand the collector by reading a CONTRIBUTING file.

You have already paid that cost. You know what an upvalue is, why it has two states, what a write barrier protects, why the validator exists, and what a stale handle means. That is exactly the knowledge that makes these codebases readable — and it is why this project comes last.


Where to Contribute

ProjectLanguageWhy it suits youWhere to start
mluaRust bindings to LuaYou know the C API's shape and the lifetime problems it solvesDocs, UserData ergonomics, async edges
runeRust-native, embeddableClosest to Ember in design; active and welcomingDiagnostics, stdlib gaps, the book
kotoRust-native, expression-orientedSmall enough to read in a weekendStdlib, error messages
piccoloRust Lua with a GC arenaIts GC design is the interesting alternative to yoursGC, Collect derive, stdlib
rhaiRust-native, embedding-focusedExcellent docs; strong on the host-boundary problemsErgonomics, performance, docs
hematitaRust Lua VMSmall, incomplete — plenty of gapsAlmost anything
lua (PUC-Rio)CThe reference. High bar, mailing-list workflowDocumentation, test cases, bug reports
CPythonCEnormous, well-organized, excellent mentoringDoc/, dis, error messages
wasmtime/craneliftRustIf Lab 30 caught youCranelift docs, IR verification

Pick one, and pick the smallest. koto and hematita are readable in a weekend; mlua and rune have active maintainers who review quickly. CPython and Lua are worth aspiring to and are not where to learn the workflow.


What to Contribute, in Order of Increasing Ambition

1. Documentation

Not a consolation prize. You have just spent four months finding out which explanations are missing, and that is a rare and perishable perspective — in six months you will have forgotten what was confusing.

Look for: an undocumented invariant you had to derive, a design decision with no rationale recorded, an example that does not compile, a term used before it is defined.

2. A Test Case

Every project's test suite has gaps, and you now know where they are because you know which properties are hard:

  • A closure capturing a loop variable.
  • Upvalue closing on break out of a captured block.
  • A table key that is a table, surviving a collection.
  • select('#') with trailing nils.
  • A metamethod that mutates the table it is dispatched from.
  • An inconsistent table.sort comparator.

Submit a failing test with a clear title before submitting a fix. Maintainers can merge a test immediately; a fix needs review of the fix and the diagnosis.

3. A Bug Report With a Minimal Reproducer

You have a whole runtime to differential-test against. Run their implementation and yours on the same program, and when they disagree, work out which is right — the same four-step procedure from Lab 12, applied across projects rather than across backends.

A minimal reproducer plus a specification citation is a gift. Most bug reports are neither.

4. A Fix

Start where you have the most context:

If you enjoyedLook at
Section 1Parser error recovery, diagnostic spans
Section 3The compiler, the disassembler, bytecode validation
Section 4The GC, upvalue handling, table internals
Section 5The host API, marshaling, userdata ergonomics
Section 6Fuzz targets, CI, the test matrix
Section 7Benchmarks, inline caches, specialization

5. A Feature

Only after 1–4, and only after a maintainer has agreed to the design. An unsolicited large PR is usually a waste of everybody's time, and language runtimes are conservative for good reasons: every feature is forever, and every semantic is a compatibility promise.


The Workflow

  1. Read CONTRIBUTING.md and the last 20 merged PRs. The second is more informative than the first: it shows you what actually gets merged, how big it is, and how the reviewer talks.
  2. Open an issue before writing code, unless the change is trivially small. "I noticed X; is a PR doing Y welcome?" costs you nothing and saves a rejected week.
  3. One change per PR. A fix plus a refactor plus a rename is three reviews wearing a trenchcoat.
  4. A test with every behavioral change. You already believe this.
  5. Match the project's style, including things you disagree with. Their repository, their conventions.
  6. Respond to review quickly and without defensiveness. A reviewer's time is the scarce resource.

What to Expect

  • The first PR takes longer than the change. Setup, CI, conventions, and review latency.
  • Review may be slow. Maintainers of small language projects are usually one or two people doing it in evenings. Ping politely after two weeks, then wait.
  • You may be wrong. You have deep knowledge of your design; theirs may differ for reasons that are not in the code. Ask before asserting.
  • Small merged beats large stalled. A merged docs PR makes you a contributor; a 2,000-line feature PR sitting for six months does not.

Deliverables

  • One merged PR to a real language runtime.
  • A docs/learning/16-upstream.md entry: what you contributed, what review taught you, what their design does differently from yours and why.
  • At least one thing you would change in Ember because of what you read in their codebase. That is the actual output of this project.
  • Optional and better: a second contribution, from a bug you found with your own runtime as the oracle.

Where to Look First

# The Rust-native ones, in rough order of approachability:
git clone https://github.com/koto-lang/koto
git clone https://github.com/rune-rs/rune
git clone https://github.com/mlua-rs/mlua
git clone https://github.com/kyren/piccolo
git clone https://github.com/rhaiscript/rhai

# Then read, in each: the GC (if any), the value representation, and the host API.
# You now know exactly what to look for, and how each one differs from your choices.

Read piccolo's GC first, whichever you contribute to. Its arena-and-branding approach to GC-in-Rust is the strongest alternative to Ember's handles, it uses lifetimes to make unrooted access a compile error rather than a runtime one, and comparing the two designs is the single most educational hour available after finishing this curriculum.


Validation / Self-check

  1. Why are you qualified now in a way you were not four months ago?
  2. Why is documentation a first-class contribution here specifically, and why is the window short?
  3. Give three test cases you could write for someone else's runtime, from properties you know are hard.
  4. What do the last 20 merged PRs tell you that CONTRIBUTING.md does not?
  5. Why open an issue before writing code?
  6. What is the actual output of this project, beyond the merged PR?
  7. How does piccolo's GC design differ from yours, and which would you choose now?

Next: The Appendix.