Summary
Adding a SECOND constructor to a data type in module M1 causes importing module M2 to fail elaboration with "Could not infer type" on multi-arg helpers that call a function returning that type — even though the same code works inside M1 itself, even though every relevant spec is concrete, and even with explicit type annotations on let bindings.
The defining module compiles fine. The failure is on the importing-module side, where 2+ bound function-arg variables cannot have their types propagated through a call that returns the multi-constructor type. Function-arg vars come back as <_> (uninferred metavars) in the elaborated form.
The full pitfall write-up is in docs/tracking/2026-04-27_GOBLIN_PITFALLS.md entry #32.
Repro skeleton
Inside module M1:
data Step
step : Vat -> State ;; ctor 1
step-out : Vat -> State -> List String ;; ctor 2
spec do-step Op Vat State -> Step
defn do-step | ... -> step v st ;; or step-out v st bytes
Inside an importing module M2:
require [M1 :refer-all]
;; FAILS — "Could not infer type"
spec wrap Op Vat State -> Vat
defn wrap [op v st]
let s := [do-step op v st]
[step-vat s]
Real-world failure
Discovered in OCapN Phase 25 (handshake modelling, commit 4b92416). I extended BridgeStep with a second constructor bridge-step-out carrying immediate-outbound bytes:
data BridgeStep
bridge-step : Vat -> BridgeState ;; existing
bridge-step-out : Vat -> BridgeState -> [List String] ;; NEW
The defining module captp-bridge.prologos continued to compile cleanly — connection-step (which uses bridge-step-vat, bridge-step-state, bridge-step-immediate) was fine.
But the importing module bridge-interop-helpers.prologos failed to define EVERY multi-arg helper that called captp-incoming-with-state and unpacked the result. Failures persisted under:
- 1 let, 2 let, 3+ let chains
- Inline (no let)
- Explicit type annotation on the let binding (
let s : BridgeStep := ...)
- Multi-arity defn syntax (
| op v st ->) instead of bracket syntax ([op v st])
- Both bracket and bare-juxtaposition body shapes
Probe matrix
The probe matrix narrowed the trigger:
| function shape |
defines? |
| 1-arg + let + captp-call (constants for other args) |
yes |
| 2-arg + let + captp-call (1 bound + 1 constant) |
NO |
| 3-arg + let + captp-call (3 bound) |
NO |
| 2-arg + INLINE + captp-call (all bound) |
NO |
| Same shape, but BridgeStep has only 1 constructor |
yes |
So the trigger is: multi-constructor data type as the return type of an imported function whose call site has 2+ bound metavar args.
Hypothesis
The elaborator's bidirectional inference for function applications can't pin down the return type of a function returning a multi-constructor data type when the call's arguments include 2+ unsolved metavariables (function-arg vars whose types haven't been propagated from the spec yet). With a single-constructor return type, the constructor's signature uniquely determines the type, but with N constructors the elaborator may delay the choice in a way that fails to back-propagate through the let.
Workaround
Avoid multi-constructor data types when the constructor's primary use is "value with optional extra payload."
-
Single constructor with all fields. Add a [List X] or [Option X] field that's nil / none for the bare case. Trade-off: every accessor and update function gets an extra wildcard, but it's mechanical. This is what Phase 25 ended up using — pending-out : List String was added to BridgeState (single-constructor) instead of a second bridge-step-out constructor on BridgeStep.
-
Wrap the optional payload in a separate function. Instead of "constructor with X" + "constructor without X," have a single constructor + a separate accumulator (e.g. a list field or queue) that the producing function appends to.
The single-ctor workaround is uniformly applicable and clean. But it's a real expressivity gap — multi-constructor data types are fundamental.
Why filing this is worth it
- Catastrophic failure mode: importing modules fail to compile. The error doesn't point to the data def — it points to the call site.
- Far from surface cause: the data type definition is in a DIFFERENT module from where the failure surfaces. Diagnosis took ~1 hour of probing with shrinking test cases.
- Workaround is invasive: moves what should be optional payload onto the parent type, with field plumbing through every accessor.
- Fundamental expressivity gap: until fixed, the language can't easily express "this dispatch returns a value-with-or-without-extra-output" patterns naturally.
Cross-references
Summary
Adding a SECOND constructor to a
datatype in moduleM1causes importing moduleM2to fail elaboration with "Could not infer type" on multi-arg helpers that call a function returning that type — even though the same code works insideM1itself, even though every relevant spec is concrete, and even with explicit type annotations onletbindings.The defining module compiles fine. The failure is on the importing-module side, where 2+ bound function-arg variables cannot have their types propagated through a call that returns the multi-constructor type. Function-arg vars come back as
<_>(uninferred metavars) in the elaborated form.The full pitfall write-up is in
docs/tracking/2026-04-27_GOBLIN_PITFALLS.mdentry #32.Repro skeleton
Inside module
M1:Inside an importing module
M2:Real-world failure
Discovered in OCapN Phase 25 (handshake modelling, commit
4b92416). I extendedBridgeStepwith a second constructorbridge-step-outcarrying immediate-outbound bytes:The defining module
captp-bridge.prologoscontinued to compile cleanly —connection-step(which usesbridge-step-vat,bridge-step-state,bridge-step-immediate) was fine.But the importing module
bridge-interop-helpers.prologosfailed to define EVERY multi-arg helper that calledcaptp-incoming-with-stateand unpacked the result. Failures persisted under:let s : BridgeStep := ...)| op v st ->) instead of bracket syntax ([op v st])Probe matrix
The probe matrix narrowed the trigger:
So the trigger is: multi-constructor data type as the return type of an imported function whose call site has 2+ bound metavar args.
Hypothesis
The elaborator's bidirectional inference for function applications can't pin down the return type of a function returning a multi-constructor data type when the call's arguments include 2+ unsolved metavariables (function-arg vars whose types haven't been propagated from the spec yet). With a single-constructor return type, the constructor's signature uniquely determines the type, but with N constructors the elaborator may delay the choice in a way that fails to back-propagate through the let.
Workaround
Avoid multi-constructor data types when the constructor's primary use is "value with optional extra payload."
Single constructor with all fields. Add a
[List X]or[Option X]field that'snil/nonefor the bare case. Trade-off: every accessor and update function gets an extra wildcard, but it's mechanical. This is what Phase 25 ended up using —pending-out : List Stringwas added toBridgeState(single-constructor) instead of a secondbridge-step-outconstructor onBridgeStep.Wrap the optional payload in a separate function. Instead of "constructor with X" + "constructor without X," have a single constructor + a separate accumulator (e.g. a list field or queue) that the producing function appends to.
The single-ctor workaround is uniformly applicable and clean. But it's a real expressivity gap — multi-constructor data types are fundamental.
Why filing this is worth it
Cross-references
docs/tracking/2026-04-27_GOBLIN_PITFALLS.md4b92416[a]pattern doesn't match'[1]value (1-element list) #30 (match in deep let-chain triggers inference failure).