E1: consume config.{env}.ck for multi-provider DI #42

Manually merged
hkowalski merged 5 commits from feat/042-e1-consume-config-env-ck-for-multi-provider-di into master 2026-02-20 15:46:58 +01:00
Owner

E1: consume config.{env}.ck for multi-provider DI

Along the way:

  • E2: auto-resolve effect to its bundled test{} mock (mechanism)
  • E3: mock-owned state region — allocate, initialize, and read
  • E4: collection mutation (.append/.clear) + method sugar + seed execution
  • E5: greeter greet_uses_the_name passes under ckc test (Phase 2 DoD)

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.rs and 40 more.

E1: consume config.{env}.ck for multi-provider DI Along the way: - E2: auto-resolve effect to its bundled test{} mock (mechanism) - E3: mock-owned state region — allocate, initialize, and read - E4: collection mutation (.append/.clear) + method sugar + seed execution - E5: greeter greet_uses_the_name passes under ckc test (Phase 2 DoD) 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.rs` and 40 more.
hkowalski approved these changes 2026-02-20 11:20:20 +01:00
hkowalski left a comment

Reads well. One question on src/rust/crates/ck_driver/src/compile/mock.rs, nothing blocking.

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(
Owner

record_elem_fields reads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?

`record_elem_fields` reads, 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.
Owner

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.

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.
Author
Owner

Split. The decision in record_elem_fields is a pure function now, with its own tests in src/rust/crates/ck_driver/src/compile/mock.rs.

Split. The decision in `record_elem_fields` is a pure function now, with its own tests in `src/rust/crates/ck_driver/src/compile/mock.rs`.
Author
Owner

The lint was right. Fixed the code in src/rust/crates/ck_driver/src/compile/mod.rs and dropped the allow on allow.

The lint was right. Fixed the code in `src/rust/crates/ck_driver/src/compile/mod.rs` and dropped the allow on `allow`.
tvidal approved these changes 2026-02-20 14:24:56 +01:00
tvidal left a comment

Looked through src/rust/crates/ck_driver/src/compile/mock.rs as well. Fine by me.

Looked through `src/rust/crates/ck_driver/src/compile/mock.rs` as 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.
hkowalski manually merged commit 6328c7a4da into master 2026-02-20 15:46:58 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
northgale/cklang-bootstrap!42
No description provided.