@@ -15,13 +15,61 @@ public section
1515
1616namespace Lean
1717
18+ /--
19+ Returns whether the option `name` is observable by type class resolution without affecting its
20+ result: tracing, pretty printing and formatting (of messages and trace nodes, which capture the
21+ ambient options object and are rendered later), profiling, diagnostics, debugging, resource
22+ limits (exceeding them throws, and exceptions are not cached), and the elaboration/kernel
23+ options read by constant realization triggered from the search (whose results are registered in
24+ the environment on first use and thus shared regardless of options).
25+ -/
26+ def isSynthInertOption (name : Name) : Bool :=
27+ Name.isPrefixOf `trace name || Name.isPrefixOf `pp name || Name.isPrefixOf `format name ||
28+ Name.isPrefixOf `profiler name || Name.isPrefixOf `diagnostics name ||
29+ Name.isPrefixOf `debug name || Name.isPrefixOf `Elab name || Name.isPrefixOf `Kernel name ||
30+ Name.isPrefixOf `interpreter name || Name.isPrefixOf `server name ||
31+ Name.isPrefixOf `internal name ||
32+ -- limits: exceeding them throws (`Lean.checkExponent` is reached from `Meta.check` during
33+ -- cached-result application), and exceptions are not cached
34+ name == `maxHeartbeats || name == `maxRecDepth ||
35+ Name.isPrefixOf `exponentiation name ||
36+ -- pseudo-option marking pattern-printing mode, read by the delaborator when rendering
37+ -- messages that captured a restricted options object (`Options.getInPattern`)
38+ name == `_inPattern
39+
40+ /--
41+ Access restriction on an `Options` object, enforced by the by-name accessors (`Options.find?`,
42+ `Options.get?`, `Options.contains` and everything built on them): accessing an option outside
43+ the allowed set panics and behaves as if the option were unset. Restricting is a constant-time
44+ flag update (`Options.restrict`) that keeps the underlying entries, so iteration (e.g. `ForIn`)
45+ is unaffected.
46+ -/
47+ inductive OptionsRestriction where
48+ /-- No restriction. -/
49+ | none
50+ /--
51+ Only inert options (`isSynthInertOption`) may be read by name: type class resolution records
52+ every result-relevant option lookup as a dependency of the cache entry it is computing (see
53+ `Lean.Meta.getRecordedOption`), so reads on the search path must go through the recording
54+ accessors, which bypass this restriction via `Options.findUnrestricted?`. A pure by-name read
55+ under this restriction is an unrecorded access and panics.
56+ -/
57+ | tcResolution
58+
59+ /-- Returns whether accessing the option `name` is allowed under the restriction. -/
60+ def OptionsRestriction.allows : OptionsRestriction → Name → Bool
61+ | .none, _ => true
62+ | .tcResolution, name => isSynthInertOption name
63+
1864structure Options where
1965 private map : NameMap DataValue
2066 /--
2167 Whether any option with prefix `trace` is set. This does *not* imply that any of such option is
2268 set to `true` but it does capture the most common case that no such option has ever been touched.
2369 -/
2470 hasTrace : Bool
71+ /-- Access restriction enforced by the by-name accessors; see `OptionsRestriction`. -/
72+ restriction : OptionsRestriction := .none
2573
2674namespace Options
2775
@@ -43,14 +91,27 @@ instance : BEq Options where
4391instance : EmptyCollection Options where
4492 emptyCollection := .empty
4593
46- @[inline] def find? (o : Options) (k : Name) : Option DataValue :=
94+ /--
95+ Reads the raw entry for `k`, bypassing the access restriction. Callers are responsible for
96+ recording the access as a dependency where required; see `OptionsRestriction.tcResolution` and
97+ `Lean.Meta.getRecordedOption`.
98+ -/
99+ @[inline] def findUnrestricted? (o : Options) (k : Name) : Option DataValue :=
47100 o.map.find? k
48101
102+ @[inline] def find? (o : Options) (k : Name) : Option DataValue :=
103+ if o.restriction.allows k then
104+ o.map.find? k
105+ else
106+ panic! s! "unrecorded access to option `{ k} ` under the current options restriction; \
107+ reads on the type class resolution path must use the recording accessors, \
108+ see `Lean.OptionsRestriction`"
109+
49110@ [deprecated find? (since := "2026-01-15" )]
50111def find := find?
51112
52113@[inline] def get? {α : Type } [KVMap.Value α] (o : Options) (k : Name) : Option α :=
53- o.map. find? k |>.bind KVMap.Value.ofDataValue?
114+ o.find? k |>.bind KVMap.Value.ofDataValue?
54115
55116@[inline] def get {α : Type } [KVMap.Value α] (o : Options) (k : Name) (defVal : α) : α :=
56117 o.get? k |>.getD defVal
@@ -59,11 +120,21 @@ def find := find?
59120 o.get k defVal
60121
61122@[inline] def contains (o : Options) (k : Name) : Bool :=
62- o.map.contains k
123+ if o.restriction.allows k then
124+ o.map.contains k
125+ else
126+ panic! s! "unrecorded access to option `{ k} ` under the current options restriction; \
127+ reads on the type class resolution path must use the recording accessors, \
128+ see `Lean.OptionsRestriction`"
129+
130+ /-- Restricts by-name access to the options allowed by `r`; see `OptionsRestriction`. -/
131+ @[inline] def restrict (o : Options) (r : OptionsRestriction) : Options :=
132+ { o with restriction := r }
63133
64134@[inline] def insert (o : Options) (k : Name) (v : DataValue) : Options where
65135 map := o.map.insert k v
66136 hasTrace := o.hasTrace || (`trace).isPrefixOf k
137+ restriction := o.restriction
67138
68139def set {α : Type } [KVMap.Value α] (o : Options) (k : Name) (v : α) : Options :=
69140 o.insert k (KVMap.Value.toDataValue v)
@@ -75,10 +146,12 @@ def erase (o : Options) (k : Name) : Options where
75146 map := o.map.erase k
76147 -- `erase` is expected to be used even more rarely than `set` so O(n) is fine
77148 hasTrace := o.map.keys.any (`trace).isPrefixOf
149+ restriction := o.restriction
78150
79151def mergeBy (f : Name → DataValue → DataValue → DataValue) (o1 o2 : Options) : Options where
80152 map := o1.map.mergeWith f o2.map
81153 hasTrace := o1.hasTrace || o2.hasTrace
154+ restriction := o1.restriction
82155
83156end Options
84157
@@ -191,6 +264,14 @@ protected structure Decl (α : Type) where
191264 descr : String := ""
192265 deprecation? : Option OptionDeprecation := none
193266
267+ /--
268+ Reads the option bypassing the access restriction, without recording the access; only for reads
269+ that provably cannot influence a type class resolution cache entry, e.g. limits whose exceedance
270+ throws (exceptions are not cached). See `OptionsRestriction.tcResolution`.
271+ -/
272+ protected def getUnrestricted [KVMap.Value α] (opts : Options) (opt : Lean.Option α) : α :=
273+ ((opts.findUnrestricted? opt.name).bind KVMap.Value.ofDataValue?).getD opt.defValue
274+
194275protected def get? [KVMap.Value α] (opts : Options) (opt : Lean.Option α) : Option α :=
195276 opts.get? opt.name
196277
0 commit comments