DEV Community

Cover image for Engineering Post: Making inference inspectable, generating cheaply, and the surface final
Ernesto Herrera Salinas
Ernesto Herrera Salinas

Posted on

Engineering Post: Making inference inspectable, generating cheaply, and the surface final

M8 is the finale: the Explain() report family, the cached Lie<T> automatic path, reflection-free construction, the benchmark suite, and the final goldens.
It tests whether the ideas developed independently across eight milestones hold together as one library.

Explain: a pure walk of the plan

I did not want inferred behavior to remain opaque, so Explain() materializes an InferenceReport by walking the frozen plan, generating nothing. Each member maps to a MemberInferenceReport (source, generator, confidence, derivation order, overridden rules); nested members appear as a single entry, so a recursive
Employee.Manager is one line, not infinite expansion. ToText() renders a human form whose exact prose is explicitly not contractual, the tests assert structure, not wording.

This is also where InferenceConfidence and InferenceSource graduate from the internal mirrors of M4 to public enums in the root namespace. Because a Munchausen.Inference file resolves an unqualified InferenceConfidence against the enclosing Munchausen namespace, the promotion is almost free, move the enums up, delete the internal copies, and every internal reference still compiles.

A subtle fix rode along: nested/collection members now report ChildDefinition as their source (they reported Type before), so the report reads correctly, Car.Owner -> (nested object).

The automatic path

public static class Lie<T>
{
    private static readonly Lazy<GenerationPlan> CachedPlan =
        new(() => DefinitionCompiler.Default.CompileAutomatic(typeof(T)),
            LazyThreadSafetyMode.ExecutionAndPublication);
}
Enter fullscreen mode Exit fullscreen mode

A generic static gives a free, thread-safe, per-closed-type cache;
CompileAutomatic sits over a process-wide plan cache; and Lazy caches a
compilation failure too, so a bad type fails consistently instead of racily. The
acceptance test proves the automatic path equals an inferred-only definition for
the same seed, Lie<Car>.Generate and Lie.Define<Car>().Build().Generate agree
byte for byte.

Zero per-object reflection

One of my performance goals was no reflection at generation time. Rather than trusting inspection alone, I verified it with an allocation test. The last reflective holdout was construction: M5/M6 used ConstructorInfo.Invoke. M8 compiles the constructor to a delegate at build time:

// arguments => (object)new Ctor((T0)arguments[0], (T1)arguments[1], ...)
Expression.Lambda<Func<object?[], object>>(body, arguments).Compile();
Enter fullscreen mode Exit fullscreen mode

Combined with the M2 compiled accessors and Array.Empty for parameterless ctors, steady-state generation touches no reflection. The allocation test generates 50,000 objects and asserts per-object allocation stays under a small bound, boxing of value-type members is accepted in v1.0, but per-object Invoke/GetValue would
blow past it.

Hardening

  • Benchmarks: a BenchmarkDotNet suite covering warm Build, per-object and batch generation, a collection model, and PRNG throughput, tracked for trend, not gated.
  • Docs: every public member is documented (CS1591 is an error, so a clean build is the proof). Ignore/Preserve docs open by contrasting each other; builder methods name the LIE codes they can cause.
  • Surface: PublicAPI.Shipped.txt records the final v1.0 API and the analyzer rejects accidental changes.
  • Goldens: a final Lie<Car> golden with a fixed seed and reference time (so Vehicle.Year is deterministic) pins the whole inference + dataset + traversal pipeline.

What's next: v1.0 is done🎉; v1.1 is a new conversation

That's the v1.0 Foundation, complete: M0 scaffold through M8 hardening. The seams for v1.1 already exist and sit empty by design: provider-stage slots in the pipeline, hook/validator/uniqueness arrays on each TypePlan, the reachable-plan dictionary that child definitions (Use) can substitute into, and a SequenceTable field reserved for the operation. Inference providers, DataAnnotations attributes, hooks, validation, uniqueness, locale packs, and child definitions deserve a new design pass rather than being squeezed into the finished plan.

Top comments (0)