Dependency injection, inside

Everything happens while the unit is compiled. There is no container at run time, no reflection and no registration API: inject T becomes either a call to a generated getter or a reference to a local of the enclosing block, and by the time the program starts there is nothing left to resolve. The surface is the DI reference; the model as decided is the DI spec. This page is teko_di.tk.

The registry #

A class marks its lifetime by naming IServiceSingleton, IServiceScoped or IServiceTransient in the list after :. Those are names, not base types: they are read where the conformance list is already read, before any lookup in the type table, so a program that writes none of the three words never allocates a row, a global or a symbol for this module at all.

Registration happens when that list closes, against the class's own row:

columnholds
sv_clsthe row of the type table
sv_lifeSingleton, Scoped or Transient
sv_slot1 once the root slot and getter have been emitted
sv_getterthe getter's symbol, once emitted
sv_ownscopea Singleton's own private scope row, or -1

The keys a service answers to are the interfaces its class row already publishes — the flattened set, so an interface's own base counts — plus the class itself. inject IClock and inject Clock therefore reach the same instance.

A class that names two lifetimes is refused, and so is an abstract class as a service.

inject is a deferred placeholder #

tk_inject never resolves anything. It records the site — the node, the wanted key, the innermost open scope, and the position — and hands back a placeholder, exactly the way a . on an untypeable receiver and a ternary do. tk_di_pass (passes.md, the fourth) is what resolves them, once the unit is fully parsed and every service is registered, whatever order the source named them in.

That is also the safety net: if the pass were somehow not registered, the core's own resolver would refuse the placeholder call outright rather than compile something wrong.

Constructor injection #

tk_di_ctor_pick walks the constructors of the service's class and keeps the one with the most parameters, every one of which is either a resolvable service key or carries a default. A tie between two equally long satisfiable constructors is refused rather than guessed; a class none of whose constructors qualify gets its own message. A private or protected constructor is refused with a message of its own, ahead of the generic accessibility one.

tk_di_ctor_args then builds the call: a service-typed parameter recurses into the same resolution, everything else clones its default. A parameter that is both a service key and defaulted takes the service.

di_stk holds the services currently under construction. A key already on it is a cycle, refused with the chain written out (A -> B -> A) before the compiler recurses into itself again — the guard protects the compiler's stack, not the program's.

Scopes #

scope { … } is an ordinary block. A parse-time stack is pushed when it opens and popped when it closes, and every inject parsed inside records the innermost row still open. A function called from inside a scope does not inherit it: parsing that function's body runs with a stack of its own, empty.

lifetimeresolved as
Singletona memoized getter over one global slot: Name_di_slot, Name_di_get()
Scoped, with no scope openthe same root getter — the root is a scope like any other
Scoped, inside scope { }a local of that block, built once per (scope, service) pair
Transienta fresh construction at every injection site

A Transient built inside a scope resolves its own Scoped dependencies against the very scope that asked for it: the scope number is threaded all the way down the constructor argument list. That is what "a Transient inherits the scope of whoever receives it" means mechanically.

The scope's locals are prepended to the block's own statements once the whole unit is parsed. From there the ordinary block-scope reference counting frees them exactly where it frees any other local of class type — one lap of a loop at a time, with no rule of its own (runtime.md).

A Singleton has a scope of its own #

This is the one place teko differs from C#, and the mechanism is why it is safe: a Singleton's dependency graph is built under a private scope row, allocated from the same pool scope { } uses and remembered in sv_ownscope. So a Scoped dependency of a Singleton becomes a local of that Singleton's own builder — one row per Singleton, never shared between two of them, and never captured from a caller's scope that is about to end.

Two refusals worth the reason #

inject written lexically inside a lambda is refused outright. A lambda's body is a separate generated function (teko_deleg.tk), so a Scoped local prepended to the enclosing scope { } would not be reachable from it; the message says to bind it outside and capture it with use (…).

A generic key (IRepo<T>), a keyed registration, a factory and a decoration are not taught at all, and neither is any Services.Get<T>()-shaped API — there is no run-time surface to put one on.

Caps #

TK_MAXSVC 32 services, TK_MAXDISITE 32 inject sites, TK_MAXDISTK 32 services under construction at once, TK_MAXDISCOPE 64 scope rows (lexical ones plus one per Singleton), TK_MAXDISCOPESTK 32 nested open scopes, TK_MAXDISCOPELOC 64 (scope, service) locals. Each overflows with its own message.

Edit this page