{"slug": "dev-log-the-milestone-where-build-stops-lying", "title": "Dev Log: The milestone where Build() stops lying", "summary": "A developer revived Munchausen, a C# NuGet package, after 9 years. In milestone M5, the Build() method was implemented to compile a frozen plan from captured builder data and inference results, collecting all errors (LIE001-LIE005) before throwing. The implementation handles recursive types like Employee with a visited set and worklist, and emits placeholder delegates for generators that will be bound in M7.", "body_md": "I’m reviving Munchausen, a C# NuGet package I started 9 years ago. This is\n\npart 6of an 8-part series documenting both the development process and the engineering decisions behind bringing the project back to life.This is the Dev Log: the practical work, cleanup, implementation steps, and day-to-day progress behind this part of the project.\n\nFor three milestones, `Build()`\n\nhas been a method that throws\n\n`NotImplementedException`\n\n. M5 is where it finally earns its name. This is the compiler: take everything the builder captured, plus everything inference decided, and assemble it into one frozen plan, or fail with a tidy list of every reason it couldn't.\n\nWhile designing `Build()`\n\n, I kept returning to one question: what would make a failed definition easy to fix? My answer was to avoid bare exceptions. Every failure gets a stable code, LIE001 through LIE005 for the v1.0 cases, and `Build()`\n\ncollects *all* of them before throwing. If your definition has a bad expression, a rule conflict, and an empty name, you get one exception listing all\n\nthree instead of discovering them one at a time.\n\nI built each diagnostic with a deliberately triggerable test:\n\n`LIE001`\n\n: `With(c => c.Owner.FirstName, ...)`\n\n(nested path, illegal target)`LIE002`\n\n: `With(...)`\n\nand `Ignore(...)`\n\non the same member (contradiction)`LIE003`\n\n: a `required`\n\nmember of an interface type (can't be resolved)`LIE004`\n\n: two equally-resolvable constructors, no tiebreaker`LIE005`\n\n: `WithName(\" \")`\n\n(empty/whitespace)There's something satisfying about writing tests whose job is to *provoke*\n\nfailure and then checking that the failure is well labeled. Good error messages\n\nare a feature. Keeping the code registry and expected diagnostic table in sync\n\nthrough a conformance test makes that feature difficult to erode accidentally.\n\nThe scary part, on paper, was the recursive-type graph. `Employee`\n\nhas a `Manager`\n\nthat's an `Employee`\n\n; compile that naively, and you recurse forever. My original architecture used a two-phase \"allocate empty shells, then fill them\" dance.\n\nWhile implementing it, I found a simpler route: keep a *visited set* of types and a worklist, and have nested members reference their child by *type* rather than a direct plan pointer. The runtime looks the plan up in a dictionary later.\n\nCompiling `Employee`\n\nnow completes in a single pass and records a friendly LIE009 Info note. The implementation improved the architecture.\n\nHere's the uncomfortable bit. The compiler wants to bind a *generator* to each inferred member, but the actual generators (the thing that produces \"Anthony\" for a `FirstName`\n\n) are datasets, and datasets are two milestones away in M7. Classic ordering tension.\n\nThe pragmatic answer was to emit a placeholder delegate that throws \"bound in M7.\" Since generation itself is still stubbed, those placeholders are never called, while everything I *can* verify, the plan structure, constructor choices, diagnostics, and recursive termination, is real. It nagged at me to add deliberate `throw`\n\ns, but this let me preserve the milestone boundary without pretending the leaves were finished. I left the IOUs explicit for future-me in M7.\n\nThis is also the milestone when my beloved `dotnet format`\n\ntrick for generating the public API file suddenly stopped working; it complained about a \"duplicate source file\" and refused to populate anything. I hand-wrote the new exception-type entries instead and moved on, but it bugged me. (Spoiler: I finally diagnosed it in M7, and the fix was embarrassingly simple. Foreshadowing.)\n\n`Lie.Define<Car>().Build()`\n\nnow works end to end. It returns an immutable\n\n`LieDefinition<Car>`\n\nholding a complete, frozen plan: a constructor choice, every\n\nmember's value source, derivations, the reachable child plans, retained\n\nInfo/Warning diagnostics. `Generate()`\n\nstill throws, there's no runtime, but the\n\n*compile* is real, validated, and recursion-safe.\n\nM6: actually running the thing. The generation runtime, lifecycle, cancellation, exception wrapping, depth, and cycle handling. It also forces me to revisit the milestone plan: with the real generators still in M7, what can the runtime honestly produce?", "url": "https://wpnews.pro/news/dev-log-the-milestone-where-build-stops-lying", "canonical_source": "https://dev.to/ernestohs/dev-log-the-milestone-where-build-stops-lying-1b2o", "published_at": "2026-06-20 20:35:00+00:00", "updated_at": "2026-06-20 21:07:03.650514+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models"], "entities": ["Munchausen", "C#", "NuGet", "Build()", "LIE001", "LIE002", "LIE003", "LIE004"], "alternates": {"html": "https://wpnews.pro/news/dev-log-the-milestone-where-build-stops-lying", "markdown": "https://wpnews.pro/news/dev-log-the-milestone-where-build-stops-lying.md", "text": "https://wpnews.pro/news/dev-log-the-milestone-where-build-stops-lying.txt", "jsonld": "https://wpnews.pro/news/dev-log-the-milestone-where-build-stops-lying.jsonld"}}