Shipping a Big Feature in Slices: Lessons from Adding Real Inheritance
A companion to The Anatomy of a Debugging Strategy and Performance Debugging: Lessons from a Real Regression and Its Fix, grounded in the PatLang journey's inheritance arc: a request to add real classes, inheritance, and composable traits to a language that had none of the three, built as five separate, independently-shipped slices rather than one large change — and every single slice found at least one real bug that only existed because the system was finally being exercised for real. The lessons below are less about any one bug and more about the discipline that kept a five-slice, multi-execution-path feature from becoming a five-slice mess.
Confirm the design explicitly before writing a line of code, for anything this size
Handed an open-ended request — "make the object system more composable, support something like interfaces, allow inheritance" — the design got asked for directly rather than assumed: resolution order (own class beats a trait, the last-listed trait wins a collision between traits, traits beat the parent), and syntax shape (a formal declarative block, but also extending the existing ad hoc call directly, with no requirement to use either). Both were confirmed, in the user's own words, before any code existed. The size of the feature is what made this worth doing explicitly rather than trusting instinct: a five-slice, three-execution-path feature is exactly the kind of thing where a wrong assumption about resolution order surfaces two slices in, after a parent-chain-walking function has already been written and tested against the wrong rule.
Slice a large feature into independently-shippable pieces, each fully verified before the next begins
The feature was scoped as five slices — single inheritance with field defaults, then real methods and dispatch, then composable traits, then an inline form needing no declarative syntax at all, then an alternative block delimiter — each one implemented, verified across every execution path the system has, and committed before the next slice started. This is slower to reach the finished feature than writing all five at once, but it means a bug in slice three is caught while slice three is the only thing that changed, not after five slices' worth of new surface area have all landed together and any one of them could be the culprit. Every slice in this arc found a real bug; catching each one against a small, known diff is materially cheaper than catching it against a five-slice diff.
The two obvious registration points for a new capability are not always all of them
Adding one new host-callable function to an existing subsystem looked like it needed exactly two updates: a table mapping the function to the code chunk that provides it, and the function's own implementation. It actually needed a third: a thin dispatch wrapper around that whole subsystem carried its own separate, hardcoded list of which function names it would actually forward calls to — invisible until a program using the brand-new function failed at runtime with "function not found," despite compiling cleanly and running correctly under every other execution path. Before declaring a new capability fully wired in, check for every place that name has to appear, not just the ones that were obvious going in — a working compile and passing interpreter tests are not proof that every dispatch path actually knows the new name exists.
Text your own tooling emits into a generated artifact is not just documentation, even inside a comment
A code comment explaining why a piece of dispatch logic falls back to another subsystem happened to spell out, in plain prose, the exact function name a separate automated check searches for to confirm that subsystem was correctly excluded from a build that never used it. Because that comment lived inside text that gets compiled into every single program regardless of whether it uses that subsystem, every ordinary build now silently tripped the check meant to catch unnecessary inclusion — caused entirely by a sentence explaining the code, not by the code itself. The first attempt at rewording it failed for the same reason: the rewording still quoted the banned string, this time inside its own explanation of why the string was banned. If a comment lives inside anything that's mechanically searched, generated, or re-emitted, its exact wording is part of the system's behaviour, not just a note for a human reader.
Isolate a single function's behaviour directly, rather than rebuilding the whole pipeline around it
A parsing bug that only manifested three build stages downstream, as a confusing generic error message pointing at what looked like the wrong line entirely, took one rebuild of the full toolchain to notice and would have taken several more to bisect by trial and error. Instead, a small, disposable script called the one function under suspicion directly, on a minimal literal input, and printed its raw return value — finding the exact bug (a missing whitespace-skip before returning control to the caller) in one iteration instead of many. When a bug surfaces several stages away from its actual cause, the fastest path to the cause is usually a tiny, throwaway harness around the one function you suspect, not another full run of everything downstream of it.
A test's name doesn't tell you which of several genuinely different code paths it actually exercises
A fix believed complete — corrected in the one place the bug lived, in the system's native code generator — still failed a dedicated test targeting an alternate compilation target, with the identical error as before the fix. The test's name suggested it was exercising that native code generator directly. It wasn't: it read a separate, generated mirror of that same logic off disk and ran that instead, and the mirror hadn't been regenerated yet. The fix was correct; the test simply wasn't running through the code the fix had touched. Before concluding a fix doesn't work because a specific test still fails, check what code path that specific test actually runs through — it may not be the one you just changed.
Before building a substantial new capability, check whether it already exists and is simply unconnected
Not from this arc specifically, but the same lesson recurred inside it in miniature, more than once: a piece of functionality assumed to need building from scratch (a parent-chain field-resolution walk, a class-body parser branch) sometimes already had almost the exact shape needed sitting nearby, built for a structurally similar earlier feature, and reusing it directly was both faster and lower-risk than writing a parallel implementation. A wiring gap and a genuinely missing implementation look identical from the outside; a few minutes checking which one you're actually facing is cheap compared to building a whole new implementation you didn't need.
A short checklist
- For a feature large enough to touch multiple subsystems or execution paths, have you had the design (resolution order, syntax shape, backward compatibility) confirmed explicitly, rather than assumed, before writing code?
- Have you sliced the feature into pieces that can each be verified and shipped independently, rather than landing it all as one large, hard-to-bisect change?
- When wiring a new capability into an existing subsystem, have you checked for every place its name needs to appear — not just the two most obvious registration points?
- If a comment, string, or piece of prose lives inside anything mechanically searched, generated, or re-emitted, have you checked what it says doesn't accidentally trigger the very check it's describing?
- When a bug surfaces several stages downstream of its real cause, have you tried isolating the one suspect function directly before rebuilding everything around it again?
- When a specific test still fails after a fix you believe is correct, have you confirmed what code path that test actually exercises, rather than assuming it must be testing what its name suggests?
- Before building a substantial new capability, have you checked whether something structurally similar already exists nearby that could be reused or extended instead?
See also
The Anatomy of a Debugging Strategy for the general observe/hypothesise/experiment loop several of these lessons specialize. Performance Debugging: Lessons from a Real Regression and Its Fix for the companion piece on performance-specific debugging discipline, including "find every independently-maintained copy of the logic you're optimising" — the direct ancestor of this page's "obvious wiring points aren't all of them" lesson. The PatLang journey's inheritance arc for the full narrative these lessons are drawn from. The Paradigms Guide's Object-Oriented section documents the feature itself. What a Third Grammar Validator and a Dormant Feature Taught About Navigating a Build covers a similar "it already existed" discovery from an earlier arc.