Composition articles and compiler notes mixed informal wiring guidance with normative host syntax.
Native dependency injection - Design model
Platform spec article
Native dependency injection - Design model
Spec standingStandard
-
Sibling articles defer to this hub for MUST/SHOULD authority.
Context
Decision
This feature hub must own normative MUST/SHOULD contract text. Sibling articles must not redefine hub requirements and should link here for authority.
Consequences
FAQ locked decisions migrate to ADRs; articles retain examples and troubleshooting tables only.
Verification anchors
/platform-spec/language-meta/composition/dependency-injection/;
verify:trudoc. -
Backends do not perform runtime service lookup for app DI.
Context
Runtime DI containers hide wiring and conflict with the project's explicit composition goals (distinct from Rust pipeline IoC in D-INC-0002).
Decision
The reference compiler must fully resolve the app composition graph at compile time. Backends must not perform runtime service lookup for
host/registry/scopewiring.Consequences
Lowering emits ctor wiring and scope enter/leave; execution hosts activate frozen graphs only.
Verification anchors
compiler/crates/beskid_analysis(plannedcompositionmodule);compiler/crates/beskid_codegen; Flow and algorithm. -
Constructor parameter inject is rejected (E1712).
Context
Constructor injection churns signatures across inheritance and complicates lowering.
Decision
injectmust apply to fields on ordinary types only. Constructor-parameterinjectmust be rejected (E1712).Consequences
Uniform type headers and a single slot-lowering story for injected fields.
Verification anchors
-
Global registrations use registry block name registry.
Context
Early drafts considered alternate container keywords.
Decision
The global registration block must be spelled
registry(locked for v0.2+).Consequences
Parser, diagnostics, and docs use one keyword; no alias
containerin Standard conformance.Verification anchors
Grammar and semantic snapshots when composition lands.
-
Resolution walks innermost named scope toward global.
Context
Authors need predictable scope boundaries for web and console activation patterns.
Decision
Construct Rule Global Merged registryof the launched host after inheritance chainNamed scopeTree under global; per-activation unless single/transientStack Fiber-local scope stack during withResolution Walk innermost → global; global::andparent::qualifiers as specifiedConsequences
Execution backends maintain scope enter/leave with
with; plural inject collects at lowest matching level.Verification anchors
-
Singular inject must be unique at the resolution level.
Context
Apps register multiple implementations of one contract (for example two
Storage).Decision
Multiple implementations must use
inject Contract[](or concreteT[]). Singularinject Contractmust be unique at the resolution level (E1705 when ambiguous).Consequences
Deterministic registration merge order defines array element order.
Verification anchors
-
Exactly one launch per process invocation.
Context
Libraries ship reusable composition roots; executables pick one entry host per run.
Decision
Projects may declare many named
hosttypes. Each process run must use exactly onelaunch Host(args)on an executable target (E1702 for duplicate launch on one path).Consequences
Manifest
apptargets name the launched host; multi-app repos use separate targets, one launch each run.Verification anchors
-
Libraries may declare host types but not launch.
Context
Test and library packages attempted to embed process entry via launch.
Decision
Libproject targets may declarehosttypes for reuse butlaunchis forbidden (E1711). Only app/test host targets maylaunch.Consequences
Consumers reference library hosts from their own app targets or approved harness entries.
Verification anchors
FAQ.
-
Resolves after semantic.snapshot before mod.analyze.
Context
Compiler mods need a frozen composition snapshot but must not mutate app graphs.
Decision
Pipeline phase
composition.resolvemust run aftersemantic.snapshotand beforemod.analyze(see Stage ordering).Consequences
Mods query read-only snapshots; app graph build stays in analysis.
Verification anchors
compiler/crates/beskid_pipeline; Flow and algorithm. -
Compiler Mod type forbids host blocks on mod projects.
Context
Compiler mods and app DI share syntax keywords but different planes.
Decision
Modprojects must not declare apphostblocks or alter app composition graphs. Mod registration uses contractregistrations[]per Compiler Mod SDK.Consequences
Clear boundary between Pipeline composition (Rust) and app DI (Beskid).
Verification anchors
- Native dependency injection - Contracts and edge cases Lifetime rules, plural inject, host override, fail-closed guarantees, E17xx diagnostics.
- Native dependency injection - Design model host, registry, scope hierarchy, global scope, field inject, array inject, dispose, and launch.
- Native dependency injection - Examples Reference fixtures for hosts, plural inject, scope dispose, and library hosts.
- Native dependency injection - FAQ and troubleshooting Locked design decisions, troubleshooting, and v0.3 follow-ups.
- Native dependency injection - Flow and algorithm Host-chain merge, global scope, composition.resolve, plural inject lowering.
- Native dependency injection - Verification and traceability Tests, snapshot versioning, and implementation checklist.
0 revisions (git unavailable at build; counts may be empty)
No commits recorded for this path.
Full tree: run pnpm verify:platform-spec-layout (writes src/generated/platform-spec-layout-report.json).
Vocabulary
Section titled “Vocabulary”| Construct | Role |
|---|---|
host | Named composition root; may extend another host or corelib base |
registry { } | Registrations at the global level for that host |
scope Name(params) { } | Named child container under global or parent scope |
startup(...) | Runs once per launch at global only |
init(...) | Runs once per with activation, before the block body |
dispose(...) | Runs once per activation leave, after the block body (or on unwind) |
with Name(args) { } | Activates a named scope |
launch Host(args) | Starts exactly one host instance for the process run |
inject | Field injection on ordinary types (not constructors) |
Declare scopes on the host. Activate with with. v0.2 uses manual with and ConsoleHost only; WebHost is v0.3+.
Scope hierarchy (normative)
Section titled “Scope hierarchy (normative)”Composition uses a fixed mental model with two layers:
Global scope (implicit)
Section titled “Global scope (implicit)”The global scope is the merged registry of the launched host after walking the host inheritance chain (: ParentHost … down to corelib bases such as ConsoleHost). It is not a separate syntax block.
- Always present for the lifetime of a
launch. startupand unqualifiedinjectresolve here when no active named scope provides a binding.- Treat global + its host parent chain as the root of the scope tree.
Named scopes
Section titled “Named scopes”scope blocks form a tree under the host:
- Top-level
scopechildren attach to global. - Nested
scopechildren attach to their parentscope. - Registrations in a named scope default to per-activation (scoped) unless marked
singleortransient.
Active scope stack
Section titled “Active scope stack”During execution the compiler maintains a scope stack (fiber-local; see Execution):
[ Global, … optional named scopes from outer with … innermost with ]Resolution (default): walk from innermost named scope toward global; first matching binding wins for singular inject. Plural inject collects all matching registrations at the lowest stack level that has at least one registration for that contract (see below).
Navigating between scopes
Section titled “Navigating between scopes”| Form | Meaning |
|---|---|
Unqualified inject T field | Innermost → … → global |
inject global::T field | Global registry only (skip named scopes) |
inject parent::T field | Parent scope on the stack (one level up from innermost named scope; global if innermost is a top-level named scope) |
init, dispose, and startup parameters may use the same qualifiers where injection applies.
Multiple hosts
Section titled “Multiple hosts”Projects may declare many named host types. host is named so libraries and apps can define reusable composition roots.
| Rule | Detail |
|---|---|
| Definitions | Any number of host AppHost, host ApiHost, etc. in a compilation |
| Launch | Exactly one host per process run via launch SomeHost(args) |
| Manifest | The executable app target names the host entry (see Contracts); a workspace must not launch two apps in one run |
Lib projects | May declare host types for reuse; launch is forbidden on Lib targets |
Host inheritance and override
Section titled “Host inheritance and override”host InfraHost : ConsoleHost { registry { single SharedConfig for Configuration; }}
host AppHost(string[] args) : InfraHost { registry { single AppConfig for Configuration; // overrides InfraHost / ConsoleHost for Configuration single SqlStorage for Storage; single FileStorage for Storage; } // ...}Merge order: start at the root base (for example ConsoleHost), apply each : ParentHost toward the launched host. The launched host is topmost — its registry entries override the same contract or self-bind type key from parents.
Override key: contract type for for Contract lines; implementation type for self-bound lines.
Lifetime mismatch on override (parent single, child transient for the same key) is a compile error.
Host declaration (normative shape)
Section titled “Host declaration (normative shape)”host AppHost(string[] args) : ConsoleHost {
registry { single AppConfiguration for Configuration; single SqlStorage for Storage; single FileStorage for Storage; transient DefaultLogger for Logger; }
scope HttpScope(RequestContext request) { ScopedDbSession for DbSession; OIDCAuthService for AuthService;
init( global::Configuration configuration, AuthService auth, ) { // per activation }
dispose(DbSession db, AuthService auth) { auth.SignOut(); db.Close(); }
scope UnitOfWork(bool readOnly) { ScopedTransaction for Transaction;
init(Transaction tx) { tx.Begin(readOnly); }
dispose(Transaction tx) { tx.Commit(); } } }
startup( Configuration configuration, Storage[] storages, ) { // global only; named scopes not active }}Host parameters
Section titled “Host parameters”Parameters on host AppHost(...) are launch inputs only. launch AppHost(args) must match that list.
Registrations (registry spelling locked)
Section titled “Registrations (registry spelling locked)”Root registry (global)
Section titled “Root registry (global)”| Keyword | Meaning |
|---|---|
single | One instance per launch (process-wide for that host run) |
transient | New instance on each resolve at global |
Registration lines
Section titled “Registration lines”single Implementation for Contract;transient Implementation;single ConcreteType;Multiple implementations for one contract at the same container level are allowed. Consumers use array field injection (below).
Inside scope blocks
Section titled “Inside scope blocks”| Keyword | Meaning |
|---|---|
| (omitted) | Scoped — one instance per with activation |
single | One instance per activation of this named scope |
transient | New instance on each resolve while the scope is active |
Field injection (v0.2 Standard)
Section titled “Field injection (v0.2 Standard)”Constructor inject is forbidden. Dependencies are fields only:
type Aggregator { inject Configuration configuration; inject Storage[] storages; // all Storage registrations in resolution container inject global::Logger logger; // force global registry}Singular inject
Section titled “Singular inject”inject DbSession session;Must resolve to exactly one registration in the walked scope chain. If zero → error; if many at the same level → error directing the author to array inject.
Plural inject (multiple implementations)
Section titled “Plural inject (multiple implementations)”inject Storage[] storages;inject StorageProvider[] providers;Must resolve to one or more registrations for that contract (or self-bind type) at the resolution container for the inject site. Order is registration order in the merged container (deterministic): base hosts first, then parent hosts, then launched host; within a registry, source order.
startup, init, and dispose may use plural parameters the same way.
Named scopes: init and dispose
Section titled “Named scopes: init and dispose”init— after activation instances are created, before thewithbody.dispose— after thewithbody on normal completion, and on activation unwind; parameters name services to tear down; order is reverse ofinitparameter order unless a future ordering attribute is added.- Failures in
initprevent the body from running;disposestill runs for activations that completedinitper unwind rules.
Activation: with
Section titled “Activation: with”with HttpScope(request) { with UnitOfWork(readOnly: false) { Save(); }}- Nested
withon the same scope name creates independent activations (separate scoped instance sets). - Child named scopes must have parent active on the stack.
Entry: launch and the app target
Section titled “Entry: launch and the app target”unit main(string[] args) { launch AppHost(args);}Lowering must:
- Merge host chain → global container for the launched host.
- Run
startup(global only). - Enter base host run loop (
ConsoleHostin v0.2).
The project manifest app executable target is the single application entry: it points at main (or equivalent) that performs exactly one launch. Running two launch calls in one process is forbidden.
Library hosts
Section titled “Library hosts”A Lib assembly may declare:
host InfraHost : ConsoleHost { registry { ... }}An app host composes on top:
host AppHost : InfraHost { registry { ... } }launch in a Lib target is a compile error.
Persistent entities (compiler)
Section titled “Persistent entities (compiler)”| Entity | Role |
|---|---|
| Host chain | Ordered hosts from base → launched |
| Global container | Merged registry after overrides |
| Scope tree | Named scopes and parent links |
| Binding plan | Field slots, array aggregates, with enter/leave |
| Composition snapshot | Frozen graph for mods after composition.resolve |
Non-goals (v0.2)
Section titled “Non-goals (v0.2)”- Constructor injection.
- Runtime service locator.
- Attribute-driven scope activation.
launchonLibor dual-app processes.WebHostautomatic request scope (v0.3).hostonModprojects.