An oracle specification maps index types to response types:
def OracleSpec (ι : Type u) : Type _ := ι → Type vConcretely, spec t is the response type at query index t : ι. OracleSpec ι is the B-component of a polynomial functor with position type A := ι; spec.toPFunctor packages the two together, and OracleSpec.ofPFunctor is its inverse (both rfl-invertible). This is the connection that makes OracleComp a free monad: see OracleComp below.
| Constructor | Notation | Example |
|---|---|---|
| Singleton spec | A →ₒ B |
Bool →ₒ Fin 6 |
| Empty spec | []ₒ |
No oracles |
| Combined specs | spec₁ + spec₂ |
unifSpec + (M →ₒ C) |
Required typeclass instances for probability reasoning:
[spec.Fintype]— all response types areFintype[spec.Inhabited]— all response types areInhabited
Without both, evalDist, probOutput, and Pr[...] will fail with confusing typeclass errors.
Computations with oracle access, defined as a free monad:
def OracleComp {ι : Type u} (spec : OracleSpec.{u,v} ι) : Type w → Type _ :=
PFunctor.FreeM spec.toPFunctor| Function | Purpose |
|---|---|
query t |
Issue an oracle query in the ambient monad (resolves to HasQuery.query) |
spec.query t |
Primitive single-query syntax (returns OracleQuery spec (spec.Range t)) |
OracleSpec.query t |
Same as spec.query t (the protected definition's full name) |
OracleComp.inductionOn |
Induction: pure case + query_bind case |
OracleComp.construct |
Same but result is Type* (not Prop) |
isPure |
Check if computation is pure (no queries) |
totalQueries |
Count total oracle queries |
| Lemma | Use |
|---|---|
bind_eq_pure_iff |
oa >>= ob = pure y ↔ ∃ x, oa = pure x ∧ ob x = pure y |
pure_ne_query |
pure x ≠ query t >>= f |
The bare identifier query is the exported HasQuery.query, so query t : OracleComp spec _ (or any m with HasQuery spec m) returns the result in the ambient monad and supports evalDist (query t : OracleComp spec _) directly. Use spec.query t (or OracleSpec.query t) when you need the primitive single-query syntax OracleQuery spec _ for liftM, OracleQuery.cont, structural induction, etc. The OracleSpec.query definition is protected; the dot-notation form spec.query t works regardless.
Prefer OracleComp.inductionOn over pattern matching on PFunctor.FreeM.pure/roll:
induction oa using OracleComp.inductionOn with
| pure x => ...
| query_bind t oa ih => ...spec ⊂ₒ superSpec means every query in spec can be simulated in superSpec without changing the distribution.
class SubSpec (spec : OracleSpec.{u, w} ι) (superSpec : OracleSpec.{v, w} τ)
extends MonadLift (OracleQuery spec) (OracleQuery superSpec) where
onQuery : spec.Domain → superSpec.Domain
onResponse : (t : spec.Domain) → superSpec.Range (onQuery t) → spec.Range t
liftM_eq_lift :
∀ {β} (q : OracleQuery spec β),
monadLift q = ⟨onQuery q.input, q.cont ∘ onResponse q.input⟩ := by intros; rflonQuery and onResponse together package a PFunctor.Lens spec.toPFunctor superSpec.toPFunctor (call it h.toLens):
| Class field | Lens field |
|---|---|
onQuery : spec.Domain → superSpec.Domain |
toFunA : P.A → Q.A |
onResponse t : superSpec.Range (onQuery t) → spec.Range t |
toFunB t : Q.B (toFunA t) → P.B t |
By the Yoneda lemma for polynomial functors this lens data is in bijection with natural transformations OracleQuery spec ⟹ OracleQuery superSpec. The MonadLift parent records that natural transformation; the liftM_eq_lift field is the propositional coherence axiom forcing it to agree with the lens. Concrete SubSpec instances spell monadLift out by hand (rather than letting it default from the lens data), so that the lifted query reduces fully under isDefEq — this is what makes pattern-matching simp lemmas like probEvent_liftComp actually fire.
SubSpec.toLens exposes the underlying lens; SubSpec.trans is composition of these lenses; MonadLiftT.refl covers the identity.
The fields are a lens. We extend MonadLift for two pragmatic reasons:
- Typeclass synthesis. Lifting
OracleComp spec α → OracleComp superSpec αis plumbed through theMonadLift/MonadLiftTmechanism; bridging throughPFunctor.Lensseparately would require an instance of the formPFunctor.Lens A B → MonadLift (Obj A) (Obj B), which Lean cannot synthesize fromOracleQuery specbecauseOracleQueryis adef(anddef-headed instance heads cannot be matched). - Reducibility under
rw/simp. A defaultedmonadLiftfield becomes opaque toisDefEqduring pattern matching. Hand-writtenmonadLiftper instance keeps the lifted query fully reducible.
LawfulSubSpec spec superSpec extends SubSpec spec superSpec with the propositional requirement that every backward fiber onResponse t is a bijection. This is exactly the PFunctor.Lens.IsCartesian predicate from PolyFun.PFunctor.Lens.Cartesian:
def Lens.IsCartesian (l : Lens P Q) : Prop := ∀ a, Function.Bijective (l.toFunB a)The bridge lemma LawfulSubSpec.toLens_isCartesian is the one-line statement that the underlying lens of a LawfulSubSpec is cartesian.
A cartesian lens is a fiberwise isomorphism over an arbitrary forward map on positions. This is strictly weaker than PFunctor.Lens.Equiv (an isomorphism in the lens category), which would also require onQuery to be a bijection. We intentionally only require fiberwise bijectivity because the basic SubSpec instances embed a small spec into a larger one (e.g. spec₁ ⊂ₒ (spec₁ + spec₂) with onQuery = Sum.inl); these embeddings are essential and would be ruled out by Equiv.
Cartesianness is the precise condition needed to push uniform distributions through the lift: LawfulSubSpec.evalDist_liftM_query shows that pulling the uniform distribution on superSpec.Range (onQuery t) back through onResponse t recovers the uniform distribution on spec.Range t.
When lifting OracleComp spec α to OracleComp superSpec α (e.g., a sub-computation uses fewer oracles than the enclosing computation).
| Lemma | Signature |
|---|---|
liftComp_pure |
liftComp (pure x) superSpec = pure x |
liftComp_bind |
liftComp (mx >>= my) superSpec = liftComp mx superSpec >>= ... |
Probability lemmas (additionally require [spec ⊂ₒ superSpec] [LawfulSubSpec spec superSpec] and uniform probability specs on both specs)
| Lemma | Signature |
|---|---|
evalDist_liftComp |
evalDist (liftComp mx superSpec) = evalDist mx |
probOutput_liftComp |
Pr[= x | liftComp mx superSpec] = Pr[= x | mx] |
probEvent_liftComp |
Pr[p | liftComp mx superSpec] = Pr[p | mx] |
Maps each oracle input to a monadic response:
@[reducible] def QueryImpl (spec : OracleSpec ι) (m : Type u → Type v) :=
(x : spec.Domain) → m (spec.Range x)Constructors:
| Constructor | Use |
|---|---|
QueryImpl.id spec |
Identity (returns queries unchanged) |
QueryImpl.id' spec |
Identity lifted to OracleComp |
QueryImpl.ofLift spec m |
From MonadLift instance |
QueryImpl.ofFn f |
From pure function f : (t : Domain) → Range t |
impl.liftTarget n |
Lift impl from m to n via MonadLiftT |
Substitutes every query t in a computation with impl t:
def simulateQ [Monad r] (impl : QueryImpl spec r) (mx : OracleComp spec α) : r αUniversal property. simulateQ impl is the unique monad morphism OracleComp spec →ᵐ r that agrees with impl on queries. Internally it is PFunctor.FreeM.mapM impl, i.e. the fold of the free-monad syntax tree into r. Every way of "running" an OracleComp factors through simulateQ; there is no other primitive interpreter.
Handler vs denotation. The target monad r determines how simulateQ impl reads:
reffectful (StateT,WriterT,OptionT, anotherOracleComp,IO, …) —simulateQ implis an effect handler: caching, logging, query counting, lazy sampling, simulating a hash oracle, embedding one game in a richer oracle context.rsemantic (PMF,SPMF,Set,Finset) —simulateQ implis a denotation.evalDistandsupportare bothsimulateQinto a semantic monad (see evalDist IS simulateQ below).
So "operational vs denotational" is not a primitive split; both are simulateQ parameterized by the target monad.
| Lemma | Statement |
|---|---|
simulateQ_pure |
simulateQ impl (pure x) = pure x |
simulateQ_bind |
simulateQ impl (mx >>= my) = simulateQ impl mx >>= fun x => simulateQ impl (my x) |
simulateQ_query |
simulateQ impl (liftM q) = q.cont <$> (impl q.input) |
simulateQ_map |
simulateQ impl (f <$> mx) = f <$> simulateQ impl mx |
simulateQ_id' |
simulateQ (QueryImpl.id' spec) mx = mx |
def compose (so' : QueryImpl spec' m) (so : QueryImpl spec (OracleComp spec')) :
QueryImpl spec m
infixl : 65 " ∘ₛ " => QueryImpl.composeKey lemma: simulateQ (so' ∘ₛ so) oa = simulateQ so' (simulateQ so oa)
Prefer these combinators (or their downstream wrappers) over hand-rolling a new QueryImpl whenever the wrapper has the shape "for each query, run a side effect and then delegate to a base implementation". They are defined in VCVio/OracleComp/SimSemantics/QueryImpl/Constructions.lean:
def preInsert (so : QueryImpl spec m) (nx : spec.Domain → n α) :
QueryImpl spec n
def postInsert (so : QueryImpl spec m) (nx : (t : spec.Domain) → spec.Range t → n α) :
QueryImpl spec npreInsert |
postInsert |
|
|---|---|---|
| Side effect runs | before the handler | after the handler |
| Sees the response? | No | Yes (the response is passed to nx) |
| If the handler fails | Side effect still happens | Side effect skipped |
Both come with a complete generic theory: induction principles (simulateQ_preInsert.induct / simulateQ_postInsert.induct), projection / strip lemmas (proj_simulateQ_preInsert, proj_simulateQ_postInsert), and bridge lemmas for probFailure, NeverFail, evalDist, probOutput, support, finSupport, plus IsTotalQueryBound / IsQueryBoundP transfer in QueryBound.lean. Defining a wrapper via preInsert / postInsert makes all of this theory available immediately and avoids re-proving instance-specific lemmas.
| Wrapper | File | Built on |
|---|---|---|
withTraceBefore (response-independent monoid trace) |
QueryTracking/Tracing.lean |
preInsert |
withTrace (response-dependent monoid trace) |
QueryTracking/Tracing.lean |
postInsert |
withTraceAppendBefore / withTraceAppend (Append-flavoured) |
QueryTracking/Tracing.lean |
preInsert / postInsert |
withCost, withCounting |
QueryTracking/CountingOracle.lean |
withTraceBefore |
withAddCost, withUnitCost |
QueryTracking/WriterCost.lean |
withCost |
withLogging |
QueryTracking/LoggingOracle.lean |
withTraceAppend |
appendInputLog (StateT input log) |
QueryTracking/LoggingOracle.lean |
preInsert |
If a new wrapper looks like one of these, add it as a small specialization rather than starting from fun t => ... from scratch.
The combinators assume the underlying handler always runs. They are not the right tool when the wrapper's control flow is conditional on external state or on the would-be response — for instance:
- Cache-on-hit logic (
withCachinginCachingOracle.lean): a cache hit replaces the query body entirely. - Fallback-style seeding (
withPregeninSeededOracle.lean): consumes a pre-generated value when available, otherwise queries. - Budget gating (
enforceOracleinEnforcement.lean): if the budget is exhausted, returnsdefaultand skips the handler. - Game-state handlers that branch on session state, gating flags, or bad-event flags.
These are genuinely custom and stay as hand-written QueryImpl definitions. If you find yourself reaching for preInsert / postInsert and discovering that you need to inspect external state to decide whether to query, you are in this category — write the impl directly.
For OracleComp, support is always available and is definitionally simulateQ into SetM, with each query interpreted by Set.univ.
evalDist : OracleComp spec α → SPMF α is available under [IsProbabilitySpec spec] and is definitionally (rfl) simulateQ into PMF, then lifted to SPMF, with each query interpreted by IsProbabilitySpec.toPMF:
noncomputable instance instMonadLiftTPMF [IsProbabilitySpec spec] :
MonadLiftT (OracleComp spec) PMF where
monadLift mx := simulateQ IsProbabilitySpec.toPMF mxUniform response semantics are supplied by [IsUniformSpec spec], which bundles [spec.Fintype], [spec.Inhabited], [IsProbabilitySpec spec], and a proof that toPMF is PMF.uniformOfFintype. The bridge from support to SPMF.support 𝒟[...] is EvalDistCompatible (OracleComp spec) and also requires [IsUniformSpec spec].
Distinct from the PMF-target evalDist, there is also a syntactic uniform-sampling handler that rewrites queries into ProbComp (i.e. target OracleComp unifSpec, not PMF):
def uniformSampleImpl [∀ i, SampleableType (spec.Range i)] :
QueryImpl spec ProbComp := fun t => $ᵗ spec.Range tPreservation of evalDist through uniformSampleImpl is a lemma, not definitional: uniformSampleImpl.evalDist_simulateQ : evalDist (simulateQ uniformSampleImpl oa) = evalDist oa (VCVio/OracleComp/Constructions/SampleableType.lean:517-523). Companion lemmas probOutput_simulateQ, probEvent_simulateQ, support_simulateQ, finSupport_simulateQ live in the same namespace and are what you reach for when you want to stay inside ProbComp rather than drop to PMF.
Defined in VCVio/OracleComp/QueryTracking/Enforcement.lean. Wraps an oracle with a
per-index query budget tracked via StateT. Queries exceeding the budget return default.
def enforceOracle [DecidableEq ι] [spec.Inhabited] :
QueryImpl spec (StateT (ι → ℕ) (OracleComp spec))Key result: enforceOracle.fst_map_run_simulateQ — if a computation satisfies
IsPerIndexQueryBound oa qb, then running under enforcement with budget qb produces
the same output distribution as running without enforcement.
Requires [DecidableEq ι] and [spec.Inhabited] (for default values).
For stateful oracle simulations, use StateT:
def myImpl : QueryImpl spec (StateT MyState ProbComp) := fun t => do
let st ← get
-- process query using state
set newState
return responseThen run with (simulateQ myImpl computation).run initialState.