unify: lenient {m0, mw} compatibility (eigentrust pitfall #2)#17
unify: lenient {m0, mw} compatibility (eigentrust pitfall #2)#17kumavis wants to merge 1 commit into
Conversation
691addb to
b4560bf
Compare
|
this should wait hiero's elaboration refactor |
b4560bf to
2ff728d
Compare
…uctor args Eigentrust pitfall memo (2026-04-23) #2 reports that spec scale-vec Rat [List Rat] -> [List Rat] defn scale-vec [s xs] [map (fn [x : Rat] [rat* s x]) xs] fails QTT with "Multiplicity violation," even though no parameter is annotated linear. The user's workaround was to inline the recursion. ROOT CAUSE (diagnosed via tracing checkQ-fb / inferQ-app): a kind-level Pi multiplicity mismatch. Type constructors like List are registered with kind `Pi(m0, Type, Type)` (the type arg is erased — `data List {A}` uses `:0` mult on its implicit binder). Generic combinators' specs like `(spec map {A B : Type} {C : Type -> Type} ...)` elaborate the inner `Type -> Type` via surf-arrow, which defaults `->` to `mw`. So map's `C` parameter expects `Pi(mw, Type, Type)`. When List flows into that slot via implicit insertion, QTT's `unify` rejects the mult mismatch even though typing-core succeeds (typing-core's check uses Pi-domain only, not mult). The user-visible error wrongly accuses the closure body of the violation; the actual conflict is the kind unification. FIX (approach (a) — targeted lattice lenience): in `classify-mult-problem` (unify.rkt), treat the {m0, mw} pair as compatible. Rationale: m0 = "zero runtime uses" trivially satisfies mw = "any number of uses (including zero)." {m1, m0} and {m1, mw} stay incompatible — linear (m1) IS a real value-level usage constraint. SIDE EFFECT FOLLOW-UP: making more unify paths succeed exposed a pre-existing latent bug in typing-core.rkt's check/Pi case (lines 2143, 2151, 2503): three `solve-mult-meta!` calls were unguarded, so re-entry on already-solved metas crashed with `solve-mult-meta!: mult-meta already solved`. Guarded each call with `mult-meta-solved?` (matches the existing QTT-side guards in qtt.rkt:2106-2108). This bug existed before but was masked because the m0/mw mismatch caused unification to fail earlier and short-circuit. APPROACH SELECTION: (a) chosen — targeted unify-mult lenience for {m0, mw}. Surgical, covers the kind-level case, doesn't change the rest of QTT. (b) rejected — defaulting all closure captures to mw would break linearity guarantees in code that relies on the inference. (c) deferred — type-driven inference of closure captures from the callee's spec is the principled fix but requires touching elaborator.rkt + typing-core.rkt + qtt.rkt across multiple argument-position detection sites. Approach (c) remains the future ideal once the spec-position information flows consistently into the closure elaboration site. (d) not needed — (a) handles the actual eigentrust case cleanly. Tests: - Updated test-unify.rkt: replaced the {m0, mw} reject test with two new tests: (i) {m0, m1} still rejects, (ii) {m0, mw} now succeeds. - Added test-closure-mult.rkt with 10 tests covering: unit tests for unify-mult lenience, eigentrust scale-vec reproducer (full invocation), foldr/scale-and-shift defns capturing scalars, explicit -1> linear annotation still rejected on multi-use, and the dict-param mw heuristic regression check. - Full suite: 7818 tests in 545s, 9 failures all pre-existing environmental (collection not found for prologos/propagator + rackcheck — same modules unavailable in this Racket install). - All 105 mult/qtt-related tests pass; all 51 unify tests pass; all 141 capability + qtt-pipeline tests pass. Co-authored-by: kumavis <1474978+kumavis@users.noreply.github.com>
2ff728d to
9fbe9f7
Compare
hierophantos
left a comment
There was a problem hiding this comment.
Substantive review with disposition pending. Let me explain.
Strong PR. Surgical fix (~12 LoC core), self-corrected diagnosis (the original pitfall report's closure-capture framing was wrong; you traced it to the actual kind-level mult mismatch when type constructor Pi(m0, Type, Type) flows into the combinator's Pi(mw, Type, Type) slot), well-tested with both lenience directions and m1 incompatibility regression. The technical fix is sound under QTT's multiplicity semiring partial order (m0 ≤ mw).
We're holding to consider a structural alternative — see #23 — and decide disposition after we audit our prior subtyping infrastructure.
The reasoning, briefly:
The lenience extends unify's semantics from structural-equality to "{m0, mw} compatible." This is a real subtype relation (m0 <: mw in QTT's semiring partial order), and the principled treatment is an explicit subtype? predicate composing with — not extending — unify. Per workflow.md adversarial framing rule, lenience-as-scaffolding risks sticking around even after the structural fix lands. We'd rather pay the architectural cost upfront if it's tractable.
Verified the bug reproduces on current main (post-S2.e). Your fix is correct for the case it covers; the question is whether to land it tactically while the structural fix is in flight, or hold for the structural fix and accept eigentrust's scale-vec blocked in the meantime.
Specifically, our concerns from the review thread (now superseded by the issue):
- Lenience-under-unify vs explicit subtyping — addressed by #23's
subtype?-as-separate-predicate proposal - Defensive
mult-meta-solved?guards as symptom treatment — likely obviated when re-entry semantics are clarified under the structural fix - Coordination with PPN 4C mult-domain universe migration (S2.c-iv, S2.e-iv) — the structural fix can be designed to compose with the universe model from the start
Three observations for if you'd like to engage with #23:
- The auditing work on
unify-multcall sites and thesolve-mult-meta!re-entry paths in your investigation is directly reusable in scoping the structural fix - The 10 test cases you wrote (especially the m1 regression and the eigentrust reproducer) become acceptance criteria for #23's implementation
- If you want to author the structural fix, very welcome —
help wantedmay go on #23 after our prior-art audit. Your familiarity withclassify-mult-problemis an advantage
Disposition decision pending the prior-art audit (how much existing subtyping infrastructure we can compose with, refining the LoC estimate). Will update here once we have a clearer picture. Thanks for the depth of the investigation — the kind-level diagnosis is the load-bearing piece for the structural design.
hierophantos
left a comment
There was a problem hiding this comment.
Updating to formal decline (request-changes) following the prior-art audit summarized in #23 comment. Short version: the structural fix is more tractable than initially estimated.
Audit found ~80% of the subtyping infrastructure already exists (subtype-predicate.rkt, subtype-lattice-merge, variance-aware structural-subtype-ground?, mult-leq partial order, mult ctor-desc with component-lattice). The PPN Track 4 design (2026-04-04) explicitly specifies "multiplicities merge via mult-lattice (max)" — the partial-order join is the named target.
The fix is literally "do for mults what's already done for types": add mult-subtype-merge (parallels subtype-lattice-merge), register it with the SRE domain at the 'subtype relation, change Pi's mult variance from = to +, handle mult-meta interaction. ~220-360 LoC, 2-3 days. The lenience-under-unify scaffolding doesn't pay for itself when the structural fix is this small.
What this means for your work:
- Closing this PR is what we're requesting — the diff itself is correct work, but landing it would create scaffolding our charter explicitly retires (
workflow.mdadversarial framing rule) - Your investigation contributions land in #23 — the kind-level diagnosis (type constructor
Pi(m0, Type, Type)vs combinatorPi(mw, Type, Type)) is load-bearing for the structural fix's design - Your tests become acceptance criteria — the eigentrust reproducer + m1 incompatibility regression are exactly what #23's implementation must satisfy
help wantedmay go on #23 — we'll audit our scheduling in coordination with PPN 4C tropical addendum work; if you'd like to author the structural fix, very welcome. Yourclassify-mult-problemfamiliarity is an advantage
Practical implication: eigentrust's scale-vec use case stays blocked until #23 lands. We accept that — 2-3 days work for the proper architecture is better than landing scaffolding that risks sticking around.
Apologies for the disposition reversal between my earlier comment and this one. The prior-art audit changed our cost estimate substantially (initial fear was multi-week; reality is multi-day given existing infrastructure), which moves the structural fix from "ideal-but-expensive" to "ideal-and-tractable." Your initial PR did its job: forced us to look at the architectural seam.
Closing recommendation. Will reopen if our scheduling shifts and we want to land #17 as bridge — but that's not the current plan.
Wires the OCapN port to a real Racket toolchain and fixes the issues
the test run surfaced.
Library fixes
- vat.prologos: rename `spawn` -> `vat-spawn` (collision with the
reserved surface form recognised in macros.rkt:`'spawn`),
`spawn-actor` -> `vat-spawn-actor`. Same in core.prologos and the
acceptance file.
- vat.prologos: drop the `Sigma Vat Nat` return shape for spawn /
fresh-promise / send. Replace with a named `Allocated` struct +
`alloc-vat` / `alloc-id` accessors. The Sigma form ran into
"could not infer" elaborator errors when the body destructured
via `match | pair a b -> ...` and then re-constructed a Sigma;
`[fst p]` / `[snd p]` reused on the same `p` tripped QTT
multiplicity. The named struct sidesteps both.
- vat.prologos: reorder `resolve-promise` / `break-promise` BEFORE
`apply-effect` (forward-reference rule — module elaboration is
single-pass top-to-bottom). Also reorder `step-after-act` before
`deliver-msg` and `list-length-helper` before `queue-length`.
- vat.prologos: drop the queued-pipeline-flush in resolve-promise /
break-promise. PromiseState's queue is `List SyrupValue` (wire
repr); the vat queue is `List VatMsg` (decoded); flushing across
the boundary would need re-encoding. Phase 1.
Test-fixture fix (load-bearing)
- All 8 OCapN test files were updated to capture and restore
`current-ctor-registry` and `current-type-meta` across the setup-
-> run boundary. The standard fixture pattern from
`test-hashable-01.rkt` does NOT preserve these — fine for tests
that only declare traits, but breaks once a preamble's imports
declare new `data` types (every `data` in our 8 modules). Without
it, the reducer sees a stale ctor-registry and refuses to fire
pattern arms over user constructors; results print as un-reduced
`[reduce ... | vat x y z a -> x] : Nat` strings.
Documented as goblin-pitfall #12; the canonical fixture in
test-support.rkt should grow this for every future test.
Compat fence
- driver.rkt: guard
`(current-parallel-executor (make-parallel-thread-fire-all))` with
a feature-detection try/catch on `thread #:pool 'own`. Racket 9
ships parallel threads; Racket 8 does not. Fence preserves the
Racket-9 fast path and falls back to sequential firing on 8.
Acceptance
- examples/2026-04-27-ocapn-acceptance.prologos updated to match
the new vat-spawn/Allocated API and verified to run clean via
process-file.
Pitfalls catalogue (docs/tracking/2026-04-27_GOBLIN_PITFALLS.md)
- #0 (sandbox/no-Racket): closed.
- +#11 — Racket-8 vs Racket-9 `thread #:pool` compat
- +#12 — test fixture loses ctor-registry/type-meta across calls
[highest-impact; canonical fixture pattern needs update]
- +#13 — `spawn` is a reserved surface keyword; collides silently
- +#14 — `match | pair a b ->` on Sigma + Sigma reconstruction =>
"could not infer"
- +#15 — QTT multiplicity on `[fst p]`/`[snd p]` reused thrice
- +#16 — single-pass module elaboration: forward references error
- +#17 — promise-queue (Syrup) vs vat-queue (VatMsg) type clash
on flush — design pitfall, scope cut
Test results
refr 6/6 syrup 22/22 promise 16/16 message 19/19
behavior 13/13 vat 21/21 pipeline 5/5 captp 7/7
e2e 8/8 total 117/117 PASS
Per user direction: - Replace the body of every DELETED entry with a single-sentence explanation. Numbers reserved per prior instruction. - Delete #15 (QTT multiplicity on fst/snd thrice). I re-tested with a real Racket — `pair [snd p] [fst p]` then a third use of `fst p` works fine; no multiplicity error. The failure I had conflated this with was actually #14's "match-and-reconstruct Sigma" issue. Result: pitfalls doc shrinks from 765 to 534 lines. Remaining real claims: #1, #4, #5, #11, #12, #13, #14, #16, #17, #18, #19, #20 (the user has reviewed only #0-10 so far; #11-20 still pending their review).
All 10 inline comments were legitimate. Three were correctness
issues, three doc/code mismatches, two unbounded-growth bugs in the
assoc-list tables, one mutate-while-iterating bug in the FFI cleanup
helper, and one CI-flakiness fix.
Real bugs
- vat.prologos:actor-table-set + promise-table-set: replace-or-
insert instead of unconditional cons. Each delivery turn was
growing the table without bound and slowing lookups linearly.
(#28#discussion_r3150426596 + #28#discussion_r3150426729)
- vat.prologos:deliver-msg: when the target actor doesn't exist,
BREAK any associated answer-promise instead of dropping the
message silently. Previously `ask`-against-missing-actor would
hang on the result-promise forever.
(#28#discussion_r3150426741)
- behavior.prologos:step-greeter: append the trailing "!" the
docstring promised. Implementation now matches "{g}, {n}!".
(#28#discussion_r3150426776)
- behavior.prologos:step-counter: add the explicit "get" branch
the docstring advertised — previously every non-"inc" tag fell
into the same no-op pile, including "get".
(#28#discussion_r3150426679)
- tcp-ffi.rkt:tcp-table-clear!: snapshot keys via hash-keys before
iterating + closing. The previous in-hash + hash-remove! shape
can raise an iteration error in Racket.
(#28#discussion_r3150426813)
Test/flakiness
- test-ocapn-tcp-testing.rkt: replace fixed port 18763 with a
listen-on-random-port helper that retries on collisions. CI
parallelism / port reuse made the fixed-port choice flaky.
(#28#discussion_r3150426716)
Doc-vs-code mismatches
- promise.prologos:enqueue / take-queue: clarify LIFO storage
(cons-onto-head) and that take-queue does NOT update the
PromiseState. Comments now match the function signatures.
(#28#discussion_r3150426657 + #28#discussion_r3150426758)
- core.prologos top docstring: drop the "Promise pipelining
(send to a promise; flushes on resolution)" claim. Phase 0
explicitly does NOT flush; only the in-actor FullFiller pattern
works for pipelining today. Cross-references goblin-pitfall #17.
(#28#discussion_r3150426694)
Verification: all 149 OCapN tests still pass after the changes
(behavior 13, captp 7, e2e 8, locator 13, message 19, netlayer 14,
pipeline 5, promise 16, refr 6, syrup 22, tcp-testing 5, vat 21).
https://claude.ai/code/session_01YM6gc3cMNH2Ymor4jdZY8u
Wires the OCapN port to a real Racket toolchain and fixes the issues
the test run surfaced.
Library fixes
- vat.prologos: rename `spawn` -> `vat-spawn` (collision with the
reserved surface form recognised in macros.rkt:`'spawn`),
`spawn-actor` -> `vat-spawn-actor`. Same in core.prologos and the
acceptance file.
- vat.prologos: drop the `Sigma Vat Nat` return shape for spawn /
fresh-promise / send. Replace with a named `Allocated` struct +
`alloc-vat` / `alloc-id` accessors. The Sigma form ran into
"could not infer" elaborator errors when the body destructured
via `match | pair a b -> ...` and then re-constructed a Sigma;
`[fst p]` / `[snd p]` reused on the same `p` tripped QTT
multiplicity. The named struct sidesteps both.
- vat.prologos: reorder `resolve-promise` / `break-promise` BEFORE
`apply-effect` (forward-reference rule — module elaboration is
single-pass top-to-bottom). Also reorder `step-after-act` before
`deliver-msg` and `list-length-helper` before `queue-length`.
- vat.prologos: drop the queued-pipeline-flush in resolve-promise /
break-promise. PromiseState's queue is `List SyrupValue` (wire
repr); the vat queue is `List VatMsg` (decoded); flushing across
the boundary would need re-encoding. Phase 1.
Test-fixture fix (load-bearing)
- All 8 OCapN test files were updated to capture and restore
`current-ctor-registry` and `current-type-meta` across the setup-
-> run boundary. The standard fixture pattern from
`test-hashable-01.rkt` does NOT preserve these — fine for tests
that only declare traits, but breaks once a preamble's imports
declare new `data` types (every `data` in our 8 modules). Without
it, the reducer sees a stale ctor-registry and refuses to fire
pattern arms over user constructors; results print as un-reduced
`[reduce ... | vat x y z a -> x] : Nat` strings.
Documented as goblin-pitfall #12; the canonical fixture in
test-support.rkt should grow this for every future test.
Compat fence
- driver.rkt: guard
`(current-parallel-executor (make-parallel-thread-fire-all))` with
a feature-detection try/catch on `thread #:pool 'own`. Racket 9
ships parallel threads; Racket 8 does not. Fence preserves the
Racket-9 fast path and falls back to sequential firing on 8.
Acceptance
- examples/2026-04-27-ocapn-acceptance.prologos updated to match
the new vat-spawn/Allocated API and verified to run clean via
process-file.
Pitfalls catalogue (docs/tracking/2026-04-27_GOBLIN_PITFALLS.md)
- #0 (sandbox/no-Racket): closed.
- +#11 — Racket-8 vs Racket-9 `thread #:pool` compat
- +#12 — test fixture loses ctor-registry/type-meta across calls
[highest-impact; canonical fixture pattern needs update]
- +#13 — `spawn` is a reserved surface keyword; collides silently
- +#14 — `match | pair a b ->` on Sigma + Sigma reconstruction =>
"could not infer"
- +#15 — QTT multiplicity on `[fst p]`/`[snd p]` reused thrice
- +#16 — single-pass module elaboration: forward references error
- +#17 — promise-queue (Syrup) vs vat-queue (VatMsg) type clash
on flush — design pitfall, scope cut
Test results
refr 6/6 syrup 22/22 promise 16/16 message 19/19
behavior 13/13 vat 21/21 pipeline 5/5 captp 7/7
e2e 8/8 total 117/117 PASS
Per user direction: - Replace the body of every DELETED entry with a single-sentence explanation. Numbers reserved per prior instruction. - Delete #15 (QTT multiplicity on fst/snd thrice). I re-tested with a real Racket — `pair [snd p] [fst p]` then a third use of `fst p` works fine; no multiplicity error. The failure I had conflated this with was actually #14's "match-and-reconstruct Sigma" issue. Result: pitfalls doc shrinks from 765 to 534 lines. Remaining real claims: #1, #4, #5, #11, #12, #13, #14, #16, #17, #18, #19, #20 (the user has reviewed only #0-10 so far; #11-20 still pending their review).
All 10 inline comments were legitimate. Three were correctness
issues, three doc/code mismatches, two unbounded-growth bugs in the
assoc-list tables, one mutate-while-iterating bug in the FFI cleanup
helper, and one CI-flakiness fix.
Real bugs
- vat.prologos:actor-table-set + promise-table-set: replace-or-
insert instead of unconditional cons. Each delivery turn was
growing the table without bound and slowing lookups linearly.
(#28#discussion_r3150426596 + #28#discussion_r3150426729)
- vat.prologos:deliver-msg: when the target actor doesn't exist,
BREAK any associated answer-promise instead of dropping the
message silently. Previously `ask`-against-missing-actor would
hang on the result-promise forever.
(#28#discussion_r3150426741)
- behavior.prologos:step-greeter: append the trailing "!" the
docstring promised. Implementation now matches "{g}, {n}!".
(#28#discussion_r3150426776)
- behavior.prologos:step-counter: add the explicit "get" branch
the docstring advertised — previously every non-"inc" tag fell
into the same no-op pile, including "get".
(#28#discussion_r3150426679)
- tcp-ffi.rkt:tcp-table-clear!: snapshot keys via hash-keys before
iterating + closing. The previous in-hash + hash-remove! shape
can raise an iteration error in Racket.
(#28#discussion_r3150426813)
Test/flakiness
- test-ocapn-tcp-testing.rkt: replace fixed port 18763 with a
listen-on-random-port helper that retries on collisions. CI
parallelism / port reuse made the fixed-port choice flaky.
(#28#discussion_r3150426716)
Doc-vs-code mismatches
- promise.prologos:enqueue / take-queue: clarify LIFO storage
(cons-onto-head) and that take-queue does NOT update the
PromiseState. Comments now match the function signatures.
(#28#discussion_r3150426657 + #28#discussion_r3150426758)
- core.prologos top docstring: drop the "Promise pipelining
(send to a promise; flushes on resolution)" claim. Phase 0
explicitly does NOT flush; only the in-actor FullFiller pattern
works for pipelining today. Cross-references goblin-pitfall #17.
(#28#discussion_r3150426694)
Verification: all 149 OCapN tests still pass after the changes
(behavior 13, captp 7, e2e 8, locator 13, message 19, netlayer 14,
pipeline 5, promise 16, refr 6, syrup 22, tcp-testing 5, vat 21).
https://claude.ai/code/session_01YM6gc3cMNH2Ymor4jdZY8u
…+ Variant B impl Builds the A/B measurement harness per addendum §9.4.2.3 (Phase 3B.0 implementation plan). Variant B implementation lands as Phase 3B.0 Pre-0 measurement scaffolding (retires at 3B.B per §9.4.1.6 outcome). atms.rkt changes (~30 LoC): - New parameter current-gray-code-aid-ordering? (default #f preserves classical) - Local gray-code-order-local helper (avoid relations.rkt import cycle) - solver-amb modified: when parameter is #t, permute alternatives via Gray-code ordering BEFORE aid allocation; original alt[i] gets aid with bit-position via gray-code-ordered offset - Provide block: export current-gray-code-aid-ordering? - Cell-layer/data-layer optimization (assumption-id-n is data); NOT scheduler-coupled per Cell/Propagator/Scheduler Orthogonality - Honest scaffolding label: "Phase 3B.0 Pre-0 measurement scaffolding — retires at 3B.B per §9.4.1.6 A/B outcome" bench-fork-on-union-gray-code.rkt (new ~200 LoC): - 5 workloads (W1-W5) per §9.4.1.5: - W1: single binary union (2 branches) - W2: single 4-ary union (4 branches) - W3: 5 sequential binary unions (10 branches total) - W4: 10 sequential 4-ary unions (40 branches total) - W5: worst-case adversarial (35 branches; varied widths) - Statistical machinery: median + IQR via linear-interpolation percentile; warmup runs (default 2) excluded from distribution; n-iter default 10 - A/B variant runner via parameter toggle in same process - run-ns-ws-all per iteration for fresh prelude env + forked network - --quick mode (n-iter=3) for smoke testing - Pre-committed falsification criteria documented in header Smoke test (--quick, n=3 — NOT measurement-grade): W1: A=59.2±0.7 ms, B=60.7±1.3 ms, delta +1.5 ms (+2.6%) W2: A=63.4±0.7 ms, B=63.1±0.6 ms, delta -0.4 ms (-0.6%) W3: A=67.4±5.8 ms, B=67.4±0.7 ms, delta -0.0 ms (-0.0%) W4: A=91.3±0.1 ms, B=90.2±1.1 ms, delta -1.0 ms (-1.1%) W5: A=83.0±9.2 ms, B=81.8±0.6 ms, delta -1.2 ms (-1.4%) All n=3 deltas within ±5% noise floor. Falsification evaluation deferred to full measurement run (n=10 per §9.4.1.6 protocol). Targeted tests pass: test-solver-context.rkt (15 tests) + test-union-types-atms.rkt (26 tests) — 41/41 GREEN. Suite stability assumed (no broader regressions from atms.rkt parameter addition; default #f preserves baseline behavior). Next: full measurement run via the harness (task #17) → evaluate falsification criteria → close 3B.0 VAG (task #18).
Fixes pitfall #2 from
docs/tracking/2026-04-23_eigentrust_pitfalls.md.Summary
failed with a QTT multiplicity error. The pitfall report described it as "closure capture of
sat multiplicity 1 vs many uses." Investigation showed that diagnosis is wrong — the closure is fine. The actual root cause is at the kind level.Root cause (the surprise)
Tracing
checkQ-fbrevealed:Listis registered with kindPi(m0, Type, Type)map's{C : Type → Type}elaborates toPi(mw, Type, Type)(the inner→defaults tomwviasurf-arrow)m0andmwand failsThe "closure captures linearly" message was misleading — the failure was in implicit-type-arg unification at the kind level, not in the value-level closure mult system.
Approach (a) — surgical {m0, mw} lenience in unify-mult
In
unify.rkt'sclassify-mult-problem: extend the compatibility table so{m0, mw}(and{mw, m0}) unify successfully without solving either to the other.Why principled:
m0("zero runtime uses") trivially satisfiesmw("any number, including zero"). Treating these as compatible is sound.{m1, *}pairs stay incompatible — linearity is a real value-level constraint.Latent bug exposed: three
solve-mult-meta!call sites intyping-core.rktre-solve already-solved metas, which the new lenience path makes observable. Guarded withmult-meta-solved?checks.Deferred to a future PR
Approach (c) — full type-driven closure-capture mult inference from the callee's spec — is the principled long-term fix for cases the {m0, mw} lenience doesn't cover (e.g., spec-position threading into closure elaboration). Out of scope here.
Files changed
racket/prologos/unify.rkt(~12 LoC) —classify-mult-problemextended with{m0, mw}compatibility caseracket/prologos/typing-core.rkt— threesolve-mult-meta!calls guarded withmult-meta-solved?(latent bug exposed by the unify change)racket/prologos/tests/test-unify.rkt— replaced one stale assertion with two new tests covering both directions of the new lenienceracket/prologos/tests/test-closure-mult.rkt(new, 10 cases)Test plan
scale-vecexample now produces'[1/4 1/8 1/16] : [List Rat]prologos/propagatorandrackcheckcollections unavailable in the agent's Racket install). None are caused by this change.benchmarks/micro/info.rktskip; redundant once PR Migrate bench-bsp-le-track2.rkt to current ATMS API (CI unblock) #10's actual migration lands)Commits
5fb3930— primary fix inunify.rkt+typing-core.rktguards + tests691addb— CI fix (skip stale bench)https://claude.ai/code/session_01MbncYJnrvjzhbVWw4xGi5x
Generated by Claude Code