illicit/lib.rs
1//! An implicit environment which is indexed by type.
2//!
3//! # Offering values to the local environment
4//!
5//! Add types to the local environment by creating and `enter`ing a [`Layer`],
6//! and retrieve them using [`get`] or [`expect`]:
7//!
8//! ```
9//! #[derive(Copy, Clone, Debug, PartialEq)]
10//! enum Theme {
11//! Light,
12//! Dark,
13//! }
14//!
15//! // no theme has been set yet
16//! assert!(illicit::get::<Theme>().is_err());
17//!
18//! illicit::Layer::new().offer(Theme::Light).enter(|| {
19//! assert_eq!(*illicit::expect::<Theme>(), Theme::Light,);
20//! });
21//!
22//! // previous theme "expired"
23//! assert!(illicit::get::<Theme>().is_err());
24//! ```
25//!
26//! # Receiving arguments "by environment"
27//!
28//! Annotate functions which require access to types in the environment with
29//! [`from_env`]:
30//!
31//! ```
32//! # #[derive(Copy, Clone, Debug, PartialEq)]
33//! # enum Theme {
34//! # Light,
35//! # Dark,
36//! # }
37//! #
38//! impl Theme {
39//! /// Calls `child` with this theme set as current.
40//! fn set<R>(self, child: impl FnOnce() -> R) -> R {
41//! illicit::Layer::new().offer(self).enter(child)
42//! }
43//!
44//! /// Retrieves the current theme. Panics if none has been set.
45//! #[illicit::from_env(current_theme: &Theme)]
46//! fn current() -> Self {
47//! *current_theme
48//! }
49//! }
50//!
51//! Theme::Light.set(|| {
52//! assert_eq!(Theme::current(), Theme::Light);
53//!
54//! // we can set a temporary override for part of our call tree
55//! Theme::Dark.set(|| assert_eq!(Theme::current(), Theme::Dark));
56//!
57//! // but it only lasts as long as the inner `enter` call
58//! assert_eq!(Theme::current(), Theme::Light);
59//! });
60//! ```
61//!
62//! ## Caution
63//!
64//! This provides convenient sugar for values stored in the current environment
65//! as an alternative to thread-locals or a manually propagated context object.
66//! However this approach incurs a significant cost in that the following code
67//! will panic:
68//!
69//! ```should_panic
70//! # #[derive(Copy, Clone, Debug, PartialEq)]
71//! # enum Theme {
72//! # Light,
73//! # Dark,
74//! # }
75//! #
76//! # impl Theme {
77//! # #[illicit::from_env(current_theme: &Theme)]
78//! # fn current() -> Self {
79//! # *current_theme
80//! # }
81//! # }
82//! println!("{:?}", Theme::current());
83//! ```
84//!
85//! See the attribute's documentation for more details, and please consider
86//! whether this is appropriate for your use case before taking it on as a
87//! dependency.
88//!
89//! # Debugging
90//!
91//! Use [`Snapshot::get`] to retrieve a copy of the current local environment.
92//!
93//! # Comparisons
94//!
95//! ## execution-context
96//!
97//! `illicit` provides capabilities very similar in ways to
98//! [execution-context]. Both crates allow one to propagate implicit values to
99//! "downstream" code, and they both allow that downstream code to provide its
100//! own additional values to the context. Both crates prevent mutation of
101//! contained types without interior mutability.
102//!
103//! One notable difference is how they handle multi-threading.
104//! `execution-context` requires types contained in a "flow-local" to implement
105//! `Send` so that contexts can be sent between threads. In contrast, while
106//! `illicit` supports the reuse of contexts, it prioritizes the single-threaded
107//! use case and does not require `Send`.
108//!
109//! The other noteworthy difference is in how they're "addressed":
110//! `execution-context` defines static variables that are referenced by
111//! name/symbol, whereas `illicit` allows the definition of a single local value
112//! per type and does not rely on naming. This offers some nice properties but
113//! it also sacrifices the static guarantee that there will always be a default
114//! value.
115//!
116//! ## `thread_local!`
117//!
118//! This crate is implemented on top of a thread-local variable which stores the
119//! context, and can be seen as utilities for dynamically creating and
120//! destroying thread-locals for arbitrary types. The other key difference is
121//! that the ability to temporarily override a thread-local is built-in.
122//!
123//! The main cost over writing one's own thread-local is that this does incur
124//! the additional overhead of a `HashMap` access, some `TypeId` comparison, and
125//! some pointer dereferences.
126//!
127//! [execution-context]: https://docs.rs/execution-context
128
129#![forbid(unsafe_code)]
130#![deny(clippy::all, missing_docs)]
131
132mod anon_rc;
133
134use anon_rc::AnonRc;
135use std::{
136 any::{Any, TypeId},
137 cell::RefCell,
138 collections::BTreeMap,
139 fmt::{Debug, Display, Formatter, Result as FmtResult},
140 mem::replace,
141 ops::Deref,
142};
143
144/// Defines required `illicit::get` values for a function. Binds the provided
145/// types as if references to them were implicit function arguments:
146///
147/// ```
148/// #[derive(Debug, PartialEq)]
149/// enum TextDirection {
150/// Ltr,
151/// Rtl,
152/// }
153///
154/// impl TextDirection {
155/// fn set<R>(self, op: impl FnOnce() -> R) -> R {
156/// illicit::Layer::new().offer(self).enter(op)
157/// }
158/// }
159///
160/// #[illicit::from_env(direction: &TextDirection)]
161/// fn align_text_to_end(t: &str, width: usize) -> String {
162/// assert!(t.len() <= width, "no linebreaking included, this is unicode-sinful as it is");
163/// match direction {
164/// TextDirection::Ltr => format!("{0:>1$}", t, width),
165/// TextDirection::Rtl => format!("{0:<1$}", t, width),
166/// }
167/// }
168///
169/// let get_aligned = || align_text_to_end("whoa", 8);
170///
171/// let right_aligned = TextDirection::Ltr.set(get_aligned);
172/// let left_aligned = TextDirection::Rtl.set(get_aligned);
173///
174/// assert_eq!(right_aligned, " whoa");
175/// assert_eq!(left_aligned, "whoa ");
176/// ```
177///
178/// # Panics
179///
180/// Will cause the annotated function to panic if it is invoked without the
181/// requested type in its environment:
182///
183/// ```should_panic
184/// # #[derive(Debug, PartialEq)]
185/// # enum TextDirection {
186/// # Ltr,
187/// # Rtl,
188/// # }
189/// #
190/// # impl TextDirection {
191/// # fn set<R>(self, op: impl FnOnce() -> R) -> R {
192/// # illicit::Layer::new().offer(self).enter(op)
193/// # }
194/// # }
195/// #
196/// # #[illicit::from_env(direction: &TextDirection)]
197/// # fn align_text_to_end(t: &str, width: usize) -> String {
198/// # assert!(t.len() <= width, "no linebreaking included, this is unicode-sinful as it is");
199/// # match direction {
200/// # TextDirection::Ltr => format!("{0:>1$}", t, width),
201/// # TextDirection::Rtl => format!("{0:<1$}", t, width),
202/// # }
203/// # }
204/// #
205/// align_text_to_end("oopsie", 8);
206/// ```
207///
208/// This attribute adds an `Environment Expectations` section to the doc
209/// comments of the annotated function to communicate this risk to users.
210#[doc(inline)]
211pub use illicit_macro::from_env;
212
213thread_local! {
214 /// The current dynamic scope.
215 static CURRENT_SCOPE: RefCell<Layer> = RefCell::new(Layer {
216 depth: 0,
217 values: Default::default(),
218 }
219 );
220}
221
222/// Returns a reference to a value in the current environment if it is
223/// present.
224///
225/// # Errors
226///
227/// Returns an error if the requested type is not available in the local
228/// environment.
229///
230/// # Examples
231///
232/// ```
233/// let msg = "hello!";
234/// illicit::Layer::new().offer(String::from(msg)).enter(|| {
235/// assert_eq!(&*illicit::get::<String>().unwrap(), msg);
236/// });
237/// ```
238///
239/// Returns [`GetFailed`] if the requested type is not in the environment:
240///
241/// ```
242/// assert!(illicit::get::<String>().is_err());
243/// ```
244pub fn get<E>() -> Result<impl Deref<Target = E> + Debug + 'static, GetFailed>
245where
246 E: Any + Debug + 'static,
247{
248 let key = TypeId::of::<E>();
249 let anon = CURRENT_SCOPE.with(|current| {
250 current.borrow().values.iter().find(|(id, _)| id == &key).map(|(_, a)| a.clone())
251 });
252 if let Some(anon) = anon {
253 Ok(anon.downcast_deref().expect("used type for storage and lookup, should match"))
254 } else {
255 Err(GetFailed::here::<E>())
256 }
257}
258
259/// Returns a reference to a value in the current environment, as
260/// [`get`] does, but panics if the value has not been set.
261///
262/// The panic message includes the stack of current [`Layer`]s
263/// and their contents.
264///
265/// # Examples
266///
267/// ```
268/// let msg = "hello!";
269/// illicit::Layer::new().offer(String::from(msg)).enter(|| {
270/// assert_eq!(&*illicit::expect::<String>(), msg);
271/// });
272/// ```
273///
274/// Panics if the requested type is not in the environment:
275///
276/// ```should_panic
277/// println!("{}", &*illicit::expect::<String>());
278/// ```
279#[track_caller]
280pub fn expect<E>() -> impl Deref<Target = E> + 'static
281where
282 E: Any + Debug + 'static,
283{
284 get().unwrap()
285}
286
287/// Removes the provided type from the current environment for the remainder
288/// of its scope. Parent environments may still possess a reference to
289/// the value.
290///
291/// # Example
292///
293/// ```
294/// let msg = "hello!";
295/// illicit::Layer::new().offer(String::from(msg)).enter(|| {
296/// assert_eq!(&*illicit::expect::<String>(), msg);
297///
298/// illicit::hide::<String>();
299/// assert!(illicit::get::<String>().is_err());
300/// });
301/// ```
302pub fn hide<E: 'static>() {
303 CURRENT_SCOPE.with(|current| {
304 let mut env = current.borrow_mut();
305 let mut without_e = env.values.clone();
306 let excluded_ty = TypeId::of::<E>();
307 without_e.retain(|(ty, _)| ty != &excluded_ty);
308 *env = Layer { values: without_e, depth: env.depth };
309 })
310}
311
312/// A container for the local environment, usually used to represent a pending
313/// addition to it.
314///
315/// The environment is type-indexed, and access is provided through read-only
316/// references. Call [`Layer::offer`] to include new values in the environment
317/// for called functions and [`get`] or [`expect`] to retrieve references to the
318/// offered values.
319///
320/// Aside: one interesting implication of the above is the ability to define
321/// "private scoped global values" which are private to functions which are
322/// nonetheless propagating the values with their control flow. This can be
323/// useful for runtimes to offer themselves execution-local values in functions
324/// which are invoked by external code. It can also be severely abused, like any
325/// implicit state, and should be used with caution.
326///
327/// # Examples
328///
329/// Code always sees the contents of the "bottom-most" `Layer`:
330///
331/// ```
332/// illicit::Layer::new().offer(String::new()).offer(5u16).enter(|| {
333/// assert!(illicit::expect::<String>().is_empty());
334/// assert_eq!(*illicit::expect::<u16>(), 5);
335///
336/// illicit::Layer::new().offer(10u16).enter(|| {
337/// assert!(illicit::expect::<String>().is_empty());
338/// assert_eq!(*illicit::expect::<u16>(), 10);
339/// });
340///
341/// assert!(illicit::expect::<String>().is_empty());
342/// assert_eq!(*illicit::expect::<u16>(), 5);
343/// });
344/// ```
345#[derive(Clone)]
346pub struct Layer {
347 depth: u32,
348 values: Vec<(TypeId, AnonRc)>,
349}
350
351impl Default for Layer {
352 #[track_caller]
353 fn default() -> Self {
354 let mut values = Vec::new();
355 let mut depth = 0;
356
357 CURRENT_SCOPE.with(|current| {
358 let current = current.borrow();
359 depth = current.depth + 1;
360 values = current.values.clone();
361 });
362
363 Self { values, depth }
364 }
365}
366
367impl From<Snapshot> for Layer {
368 fn from(snapshot: Snapshot) -> Self {
369 snapshot.current
370 }
371}
372
373impl Layer {
374 /// Construct a new layer which defaults to the contents of the current one.
375 /// Call [`Layer::offer`] to add values to the new layer before calling
376 /// [`Layer::enter`] to run a closure with access to the new and old
377 /// values.
378 #[track_caller]
379 pub fn new() -> Self {
380 Self::default()
381 }
382
383 /// Adds the new item and returns the modified layer.
384 ///
385 /// The offered type must implement `Debug` to allow [`get`]'s errors
386 /// to display the contents of the illicit environment. It must also satisfy
387 /// the `'static` lifetime because [`get`] is unable to express any
388 /// lifetime constraints at its callsite.
389 #[track_caller]
390 pub fn offer<E>(mut self, v: E) -> Self
391 where
392 E: Debug + 'static,
393 {
394 let anon = AnonRc::new(v, self.depth);
395 let existing = self.values.iter_mut().find(|(id, _)| *id == anon.id());
396
397 if let Some((_, existing)) = existing {
398 *existing = anon;
399 } else {
400 self.values.push((anon.id(), anon));
401 }
402
403 self
404 }
405
406 #[inline(never)]
407 fn make_guard(self) -> impl Drop {
408 CURRENT_SCOPE.with(|parent| {
409 let mut parent = parent.borrow_mut();
410 let parent = replace(&mut *parent, self);
411
412 scopeguard::guard(parent, move |prev| {
413 CURRENT_SCOPE.with(|p| p.replace(prev));
414 })
415 })
416 }
417
418 /// Call `child_fn` with this layer as the local environment.
419 #[inline(always)]
420 pub fn enter<R>(self, child_fn: impl FnOnce() -> R) -> R {
421 let _reset_when_done_please = self.make_guard();
422 child_fn()
423 }
424}
425
426impl Debug for Layer {
427 fn fmt(&self, f: &mut Formatter) -> FmtResult {
428 let is_alternate = f.alternate();
429 let mut s = f.debug_struct("Layer");
430 for (ty, anon) in self.values.iter().map(|(_, v)| (v.ty(), v)).collect::<BTreeMap<_, _>>() {
431 if is_alternate {
432 s.field(ty, &(anon.debug(), anon.location()));
433 } else {
434 s.field(ty, anon.debug());
435 }
436 }
437 s.finish()
438 }
439}
440
441// interior mutations happen only within panic-safe methods
442impl std::panic::UnwindSafe for Layer {}
443impl std::panic::RefUnwindSafe for Layer {}
444
445/// Implemented by types that can offer themselves as context to a child call.
446pub trait AsContext {
447 /// Call `op` within the context of a [`Layer`] containing `self`.
448 fn offer<R>(self, op: impl FnOnce() -> R) -> R;
449}
450
451impl<T> AsContext for T
452where
453 T: Debug + Sized + 'static,
454{
455 fn offer<R>(self, op: impl FnOnce() -> R) -> R {
456 Layer::new().offer(self).enter(op)
457 }
458}
459
460/// A point-in-time representation of the implicit environment.
461///
462/// # Examples
463///
464/// Collecting a `Snapshot` is useful for debugging:
465///
466/// ```
467/// illicit::Layer::new().offer(5u16).enter(|| {
468/// println!("{:#?}", illicit::Snapshot::get());
469/// });
470/// ```
471///
472/// `Snapshot`s can also be converted back into [`Layer`]s for re-use:
473///
474/// ```
475/// let mut snapshot = None;
476/// illicit::Layer::new().offer(5u16).enter(|| {
477/// assert_eq!(*illicit::expect::<u16>(), 5);
478/// snapshot = Some(illicit::Snapshot::get());
479/// });
480/// assert!(illicit::get::<u16>().is_err());
481///
482/// illicit::Layer::from(snapshot.unwrap()).enter(|| {
483/// assert_eq!(*illicit::expect::<u16>(), 5);
484/// });
485/// ```
486#[derive(Clone, Debug)]
487pub struct Snapshot {
488 current: Layer,
489}
490
491impl Snapshot {
492 /// Returns a snapshot of the current context. Suitable for debug printing,
493 /// or can be converted into a [`Layer`] for reuse.
494 pub fn get() -> Self {
495 let mut current: Layer = CURRENT_SCOPE.with(|s| (*s.borrow()).clone());
496
497 current.values.sort_by_key(|(_, anon)| anon.depth());
498
499 Snapshot { current }
500 }
501}
502
503/// A failure to find a particular type in the local context.
504#[derive(Debug)]
505pub struct GetFailed {
506 looked_up: &'static str,
507 current_snapshot: Snapshot,
508}
509
510impl GetFailed {
511 fn here<E: 'static>() -> Self {
512 Self { looked_up: std::any::type_name::<E>(), current_snapshot: Snapshot::get() }
513 }
514}
515
516impl Display for GetFailed {
517 fn fmt(&self, f: &mut Formatter) -> FmtResult {
518 f.write_fmt(format_args!(
519 "expected a `{}` from the environment, did not find it in current env: {:?}",
520 self.looked_up, &self.current_snapshot,
521 ))
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use insta::{assert_display_snapshot, assert_snapshot};
529
530 #[test]
531 fn child_env_replaces_parent_env_values() {
532 let mut first_called = false;
533 let mut second_called = false;
534
535 assert!(get::<u8>().is_err());
536 Layer::new().offer(0u8).enter(|| {
537 let curr_byte = *expect::<u8>();
538 assert_eq!(curr_byte, 0);
539 first_called = true;
540
541 Layer::new().offer(1u8).enter(|| {
542 let curr_byte = *expect::<u8>();
543 assert_eq!(curr_byte, 1);
544 second_called = true;
545 });
546
547 assert!(second_called);
548 assert_eq!(curr_byte, 0);
549 });
550 assert!(first_called);
551 assert!(get::<u8>().is_err());
552 }
553
554 #[test]
555 fn child_sees_parent_env() {
556 assert!(get::<u8>().is_err());
557 Layer::new().offer(0u8).enter(|| {
558 let curr_byte = *expect::<u8>();
559 assert_eq!(curr_byte, 0);
560
561 Layer::new().offer(1u16).enter(|| {
562 let curr_byte = *expect::<u8>();
563 assert_eq!(curr_byte, 0, "must see u8 from enclosing environment");
564
565 let curr_uh_twobyte = *expect::<u16>();
566 assert_eq!(curr_uh_twobyte, 1, "must see locally installed u16");
567 });
568
569 assert_eq!(curr_byte, 0, "must see 0");
570 });
571 assert!(get::<u8>().is_err());
572 }
573
574 #[test]
575 fn removing_from_env() {
576 assert!(get::<u8>().is_err());
577
578 Layer::new().offer(2u8).enter(|| {
579 assert_eq!(*expect::<u8>(), 2, "just added 2u8");
580
581 Layer::new().enter(|| {
582 assert_eq!(*expect::<u8>(), 2, "parent added 2u8");
583 hide::<u8>();
584 assert!(get::<u8>().is_err(), "just removed u8 from Env");
585 });
586
587 assert_eq!(*get::<u8>().unwrap(), 2, "returned to parent Env with 2u8");
588
589 hide::<u8>();
590 assert!(get::<u8>().is_err(), "just removed u8 from Env");
591 })
592 }
593
594 #[test]
595 fn failure_error() {
596 let e = get::<u8>().unwrap_err();
597 assert_display_snapshot!(e);
598 }
599
600 #[test]
601 fn layer_debug_impl() {
602 let snapshot = Layer::new().offer(1u8).enter(Snapshot::get);
603 // assert_debug_snapshot adds #, which prints file paths, which breaks snapshots
604 assert_snapshot!(format!("{:?}", snapshot));
605 }
606}