E1: consume config.{env}.ck for multi-provider DI #42
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/042-e1-consume-config-env-ck-for-multi-provider-di"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
E1: consume config.{env}.ck for multi-provider DI
Along the way:
Touches 46 files:
current-state.md,docs/effects.md,docs/impl/08-diagnostics.md,docs/impl/resolution/C2-provider-table.md,docs/impl/runtime/R6-mocked-effects.md,src/rust/crates/ck_driver/src/compile/mock.rsand 40 more.Reads well. One question on
src/rust/crates/ck_driver/src/compile/mock.rs, nothing blocking.@ -0,0 +501,4 @@/// its scalar shape, in declaration order (`level: LogLevel` → variant,/// `message: String` → str). Returns `None` if the record is not found or a field/// is non-scalar (a nested record / collection — outside this slice's frame).fn record_elem_fields(record_elem_fieldsreads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?@ -338,3 +342,3 @@/// trivial (null-pointer) behavior, so the empty-body actor fixtures keep their/// exact three-event lifecycle with nothing printed.#[allow(clippy::too_many_arguments)] // the actor-entry inputs are irreducible: name, body, resolutions, the two DI maps, source, interner, and the S1 discovery context.#[allow(clippy::too_many_arguments)] // the actor-entry inputs are irreducible: name, body, resolutions, the two DI maps, the env config, source, interner, and the S1 discovery context.An allow attribute on
allow. Is the lint wrong here, or is the code? If it is the lint, a one-line reason beside it stops the next person deleting it, at src/rust/crates/ck_driver/src/compile/mod.rs:344.Split. The decision in
record_elem_fieldsis a pure function now, with its own tests insrc/rust/crates/ck_driver/src/compile/mock.rs.The lint was right. Fixed the code in
src/rust/crates/ck_driver/src/compile/mod.rsand dropped the allow onallow.Looked through
src/rust/crates/ck_driver/src/compile/mock.rsas well. Fine by me.Parse config.{env}.ck (default env=dev) into an Effect->Provider name map and consume it in the compile-time DI tie-break. build_op_dispatch's two-or-more- provider arm now: config binds the effect to a candidate implementing the op -> use it; config absent or silent -> refined E0504; config names a non-candidate -> new E0505. Sole-impl and zero arms unchanged — config is never consulted for a sole implementation (project_provider_binding_config lock). New driver module ck_driver/src/config.rs (minimal token-window reader of the Ident <- Ident bindings, parsed separately from the module closure like the project.ck manifest). Threaded emit_app_{actor,fn_main} -> build_op_dispatch; single-file + ckc-test paths pass an empty config (tests route through the interface mock — E2). fn-main S1 discovery extracted to user_fns::discover_main_user_fns to hold the file-LoC cap. Fixtures: tests/corpus/config_di (2 Log providers + config picks StdoutLog) and config_di_ambiguous (no config -> E0504). Gate: config_di_run.rs (Docker): ckc run config_di prints "config selected StdoutLog"; config_di_ambiguous is a compile-time E0504, no run. Integration-only — no ck_lower/ck_types/ck_resolve source changes. Gates: fmt clean, clippy 0 warnings, cargo test --workspace exit 0, build clean.In test context, an effect named as a test-fn parameter now dispatches its operations to the effect's bundled test{} mock instead of a production provider. Mechanism only — mock-owned mutable state (E3) and collection mutation (E4) are separate slices. - New driver module ck_driver/src/compile/mock.rs: reads each test-fn param's type head, resolves it to an EffectId, locates the effect's test{} block, and obtains the mock op bodies via a new public ck_resolve API resolve_effect_mock_op_bodies (same per-op resolution N7 already runs; the resolution stays in ck_resolve, the DI stays in the driver). - Each mock op fn is lowered like an S1 user fn under a fresh FnId and matched to its OpId as an OpId -> mock-FnId pre-binding. dispatch::build_op_dispatch consults this mock source BEFORE the provider tree-search, so a mocked op pre-empts sole-impl/config resolution (composes with E1). Codegen's user_fn_callee routes a Resolution::Op the driver bound to a user fn (the mock) to the user-call path; a provider-intrinsic op falls through unchanged. - Production paths (actor / fn main) pass an empty mock map — unchanged. No config.test.ck (tests route through the interface mock, per the lock). Fixture tests/corpus/mock_dispatch (effect Beacon, Assert-only mock, no provider) + gate mock_dispatch_run.rs: ckc test runs ping_dispatches to PASS, the mock's AssertEvaluated("beacon mock dispatched", passed:true) proving the op reached the mock (absent E2 it would be E0503). Registered in corpus.rs RUNTIME_TESTED. Locks held: ck_lower has no ck_resolve dependency (codegen only reads the driver-built dispatch map); no ck_types semantic change; single execution path. Gates: fmt clean, clippy 0 warnings, cargo test --workspace exit 0, build clean.A mocked effect parameter now gets a per-instance state{} region: allocated in the arena at test entry, initialized from each state field's initializer, and readable both via param.field in the test body (l.entries, s.count) and via a bare sibling reference in a mock op body. Read-only — mutation (.append/.clear) and seed-fn execution are E4. - Resolution: new public ck_resolve::resolve_effect_mock_state resolves each state-field initializer against the shared test-block item scope (resolution stays in ck_resolve). - Layout: new ck_lower/src/codegen/mock_state.rs builds the region as a record-style struct reusing record.rs's static-frame writer (value_width / store_value_at / word_offset); a scalar field stores inline, a List field stores one buffer-pointer word (FieldRepr::ListPtr) with the buffer built as a list literal. The region base is held in a per-effect global handle (ck_mock_state_<key>) so the test body and every op body reach the same arena region; E4 mutates it in place through that handle (no_gc — reclaimed wholesale with the arena). - Reads: record::emit_field_values gained a region arm; list index-read extended to a region list-field receiver; Assert::check's == predicate now routes through the locals/region-aware emit_eq. State fields bind as LocalId in declaration order above each op body (front-end already did this). No ck_types change — s.field on an effect-typed param fail-softs to Ty::Unknown. Fixture tests/corpus/mock_state (effect Store, state count:Int=3 + items:List<Int>=[10,20,30]) + gate mock_state_run.rs: ckc test runs reads_mock_state to PASS with three passing asserts (op reads scalar, test reads scalar, test reads list element). Registered in corpus.rs RUNTIME_TESTED. Known interim: the region handle is per-effect; re-initialized at each test entry gives sequential-test isolation (fine for one mock per test). Per-instance freshness across many tests sharing one effect mock is future work. Locks held: ck_lower has no ck_resolve dependency; arena-backed no_gc region; single execution path. Gates: fmt clean, clippy 0 warnings, cargo test --workspace exit 0, build clean.The last mock-machinery mechanism slice before greeter integration. - Method-call lowering: receiver.append(e)/receiver.clear() (ck_hir MethodCall) now lowers. F1 classifies the interned method name into a new floor_ir::ExprKind::ListMutate { receiver, method: Append|Clear, args }. The name->enum needs the interner, which ck_lower lacks, so the driver interns append/clear and threads them via new lower_body_with(hir, types, ListMethods); lower_body is the empty-set wrapper (non-mock bodies unchanged). Codegen list::emit_list_mutate dispatches on the enum alone (interner-free). - Mutable list header: a List state field is now a (ptr, len) 2-word header (ptr first, so E3 index-reads are untouched). Growth is copy-grow: each .append does ck_rt_alloc((len+1)*stride) + memcpy old bytes + store element at index len via the shared store_value_at, then advances ptr/len. .clear sets len=0. Chose (ptr,len)+copy-grow over a dead cap word. Growable interim; bounded List<T,N> static-N is still shortcut C. - Append is element-stride-generic (stride from the appended value's value_width; store_value_at handles records) — so E5's record element rides the same path. Empty-[] element (stride,repr) lifted from the List<T> annotation via ck_types::lift_required_type_ref. Record-element read-back (b.field[0].x off a region list) deferred to E5 (greeter integration). - Seed execution: seed fns resolve via new public ck_resolve::resolve_effect_mock_seed_bodies, lower like op fns, and are called once at test entry after region init, before the body — never by the test (seed_keyword/mock_testing_model locks). Fixture tests/corpus/mock_mutate (effect Bag, xs:List<Int>=[], fn add { xs.append(42) }, seed fn wipe { xs.clear() }, test appends_twice) + gate mock_mutate_run.rs: ckc test runs appends_twice to PASS (b.xs[0]==42, b.xs[1]==42). Registered in corpus.rs RUNTIME_TESTED. R4 function-cap hit on lower_expr fixed by extracting lower_method_call (not by touching the cap). Locks held: explicit in-place mutation on the mock's owned region (no covert incremental optimizer, no buffers/freeze path), no .seed.X accessor, arena growth (no free/GC), no bounded-sizing claim, ck_lower has no ck_resolve dep. Gates: fmt clean, clippy 0 warnings, cargo test --workspace exit 0, build clean.The final mock-machinery integration: the real corpus greeter test now passes end-to-end through the unified, type-checked pipeline. ckc test tests/corpus/greeter -> greet_uses_the_name ... ok Lifted the two restrictions the mechanism slices deferred: - Parameterized mock op fns: E2 rejected mock ops with parameters; now each mock op's parameter types are lifted off its signature (ck_types::lift_required_ type_ref) into LoweredUserFn.params, and bound above the test-block items (param i -> LocalId(param_base + i), param_base = test_block_item_count) so the effect-call arg threads into the mock body (Log::info("Hi, Vincent") -> fn info(msg), msg = LocalId(8), entries = LocalId(0)). - Record-element read-back off a region list: the driver computes each List<T> field's element layout from the declared element type (record/data interner tables) into a codegen-public ElemLayout (Scalar | Record(fields)); MockField.elem became Option<ElemLayout>. ck_lower's list::try_emit_region_ record_field computes buffer + index*stride, finds the named field and loads it (routed from record::emit_field_values ahead of the inline-record path). So l.entries[0].message (String, P2.5 ==) and l.entries[0].level (variant, P2.3 tag compare) project a LogLine element from the region list. Front-end feature this surfaced (genuine gap, not a lock): the module-qualified three-segment variant path Log::LogLevel::Info was Unresolved; implemented ck_resolve::paths::resolve_module_variant mirroring same-module LogLevel::Info (privacy already checked on the middle segment). Meta stream confirms the real path (non-vacuous): "Hi, Vincent" is appended by the mock and read back; both AssertEvaluated (passed) + TestPassed emit. New permanent gate ck_driver/tests/greeter_test_run.rs asserts TestPassed + both hints. greeter trap.md prose points at the new gate (empty front-end-clean block retained; RUNTIME_TESTED wiring stays coupled to the helloWorld bookkeeping slice; runtime_coverage_is_exhaustive green). Locks held: ck_lower has no ck_resolve dependency (element layout computed in the driver); DI stays in the driver; no ck_types semantic change (lift reads only); mutation model / seed / manifest typing / single execution path all respected. Gates: fmt clean, clippy 0 warnings, cargo test --workspace exit 0, build clean.