Client: consider borrow limits in max-swap (#316)

This commit is contained in:
Christian Kamm 2022-12-06 09:25:24 +01:00 committed by GitHub
parent a5a015e19f
commit e0c403dcb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 226 additions and 123 deletions

View File

@ -373,6 +373,9 @@ impl<'a> LiquidateHelper<'a> {
let health_cache = health_cache::new(&self.client.context, self.account_fetcher, &liqor)
.expect("always ok");
let source_bank = self.client.first_bank(source)?;
let target_bank = self.client.first_bank(target)?;
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
let target_price = health_cache.token_info(target).unwrap().prices.oracle;
// TODO: This is where we could multiply in the liquidation fee factors
@ -380,8 +383,10 @@ impl<'a> LiquidateHelper<'a> {
let amount = health_cache
.max_swap_source_for_health_ratio(
source,
target,
&liqor,
&source_bank,
source_price,
&target_bank,
oracle_swap_price,
self.liqor_min_health_ratio,
)

View File

@ -61,6 +61,14 @@ pub enum MangoError {
BankBorrowLimitReached,
#[msg("bank net borrows has reached limit - this is an intermittent error - the limit will reset regularly")]
BankNetBorrowsLimitReached,
#[msg("token position does not exist")]
TokenPositionDoesNotExist,
}
impl MangoError {
pub fn error_code(&self) -> u32 {
(*self).into()
}
}
pub trait Contextable {
@ -133,6 +141,16 @@ macro_rules! error_msg {
};
}
/// Creates an Error with a particular message, using format!() style arguments
///
/// Example: error_msg!("index {} not found", index)
#[macro_export]
macro_rules! error_msg_typed {
($code:ident, $($arg:tt)*) => {
error!(MangoError::$code).context(format!($($arg)*))
};
}
/// Like anchor's require!(), but with a customizable message
///
/// Example: require!(condition, "the condition on account {} was violated", account_key);
@ -146,4 +164,5 @@ macro_rules! require_msg {
}
pub use error_msg;
pub use error_msg_typed;
pub use require_msg;

View File

@ -1,6 +1,6 @@
use super::{OracleConfig, TokenIndex, TokenPosition};
use crate::accounts_zerocopy::KeyedAccountReader;
use crate::error::{Contextable, MangoError};
use crate::error::*;
use crate::state::{oracle, StablePriceModel};
use crate::util;
use crate::util::checked_math as cm;
@ -271,7 +271,7 @@ impl Bank {
allow_dusting: bool,
now_ts: u64,
) -> Result<bool> {
self.update_net_borrows(-native_amount, now_ts, None)?;
self.update_net_borrows(-native_amount, now_ts);
let opening_indexed_position = position.indexed_position;
let result = self.deposit_internal(position, native_amount, allow_dusting)?;
self.update_cumulative_interest(position, opening_indexed_position);
@ -487,7 +487,10 @@ impl Bank {
// net borrows requires updating in only this case, since other branches of the method deal with
// withdraws and not borrows
self.update_net_borrows(native_amount, now_ts, oracle_price)?;
self.update_net_borrows(native_amount, now_ts);
if let Some(oracle_price) = oracle_price {
self.check_net_borrows(oracle_price)?;
}
Ok((true, loan_origination_fee))
}
@ -548,12 +551,7 @@ impl Bank {
/// Update the bank's net_borrows fields.
///
/// If oracle_price is set, also do a net borrows check and error if the threshold is exceeded.
pub fn update_net_borrows(
&mut self,
native_amount: I80F48,
now_ts: u64,
oracle_price: Option<I80F48>,
) -> Result<()> {
pub fn update_net_borrows(&mut self, native_amount: I80F48, now_ts: u64) {
let in_new_window =
now_ts >= self.last_net_borrows_window_start_ts + self.net_borrows_window_size_ts;
@ -565,24 +563,23 @@ impl Bank {
} else {
cm!(self.net_borrows_in_window + native_amount.checked_to_num().unwrap())
};
}
if native_amount < 0 || self.net_borrows_limit_quote < 0 {
pub fn check_net_borrows(&self, oracle_price: I80F48) -> Result<()> {
if self.net_borrows_limit_quote < 0 {
return Ok(());
}
if let Some(oracle_price) = oracle_price {
let price = oracle_price.max(self.stable_price());
let net_borrows_quote = price
.checked_mul_int(self.net_borrows_in_window.into())
.unwrap();
if net_borrows_quote > self.net_borrows_limit_quote {
return err!(MangoError::BankNetBorrowsLimitReached).with_context(|| {
format!(
"net_borrows_in_window ({:?}) exceeds net_borrows_limit_quote ({:?}) for last_net_borrows_window_start_ts ({:?}) ",
self.net_borrows_in_window, self.net_borrows_limit_quote, self.last_net_borrows_window_start_ts
)
});
}
let price = oracle_price.max(self.stable_price());
let net_borrows_quote = price
.checked_mul_int(self.net_borrows_in_window.into())
.unwrap();
if net_borrows_quote > self.net_borrows_limit_quote {
return Err(error_msg_typed!(BankNetBorrowsLimitReached,
"net_borrows_in_window ({:?}) exceeds net_borrows_limit_quote ({:?}) for last_net_borrows_window_start_ts ({:?}) ",
self.net_borrows_in_window, self.net_borrows_limit_quote, self.last_net_borrows_window_start_ts
));
}
Ok(())

View File

@ -303,10 +303,13 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
#[inline]
fn bank_index(&self, token_index: TokenIndex) -> Result<usize> {
Ok(*self
.token_index_map
.get(&token_index)
.ok_or_else(|| error_msg!("token index {} not found", token_index))?)
Ok(*self.token_index_map.get(&token_index).ok_or_else(|| {
error_msg_typed!(
TokenPositionDoesNotExist,
"token index {} not found",
token_index
)
})?)
}
#[inline]

View File

@ -362,7 +362,13 @@ impl HealthCache {
self.token_infos
.iter()
.position(|t| t.token_index == token_index)
.ok_or_else(|| error_msg!("token index {} not found", token_index))
.ok_or_else(|| {
error_msg_typed!(
TokenPositionDoesNotExist,
"token index {} not found",
token_index
)
})
}
/// Changes the cached user account token balance.
@ -609,7 +615,13 @@ pub(crate) fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex
infos
.iter()
.position(|ti| ti.token_index == token_index)
.ok_or_else(|| error_msg!("token index {} not found", token_index))
.ok_or_else(|| {
error_msg_typed!(
TokenPositionDoesNotExist,
"token index {} not found",
token_index
)
})
}
/// Generate a HealthCache for an account and its health accounts.

View File

@ -43,45 +43,43 @@ impl HealthCache {
}
}
/// Return a copy of the current cache where a swap between two banks was executed.
///
/// Errors:
/// - If there are no existing token positions for the source or target index.
/// - If the withdraw fails due to the net borrow limit.
fn cache_after_swap(
&self,
account: &MangoAccountValue,
source_bank: &Bank,
source_oracle_price: I80F48,
target_bank: &Bank,
amount: I80F48,
price: I80F48,
) -> Self {
let mut source_position = account
.token_position(source_bank.token_index)
.map(|v| v.clone())
.unwrap_or_default();
let mut target_position = account
.token_position(target_bank.token_index)
.map(|v| v.clone())
.unwrap_or_default();
) -> Result<Self> {
use std::time::SystemTime;
let now_ts = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("system time after epoch start")
.as_secs();
let mut source_position = account.token_position(source_bank.token_index)?.clone();
let mut target_position = account.token_position(target_bank.token_index)?.clone();
let target_amount = cm!(amount * price);
let mut source_bank = source_bank.clone();
source_bank
.withdraw_with_fee(&mut source_position, amount, 0, I80F48::ZERO)
.unwrap();
source_bank.withdraw_with_fee(&mut source_position, amount, now_ts, source_oracle_price)?;
let mut target_bank = target_bank.clone();
target_bank
.deposit(&mut target_position, target_amount, 0)
.unwrap();
target_bank.deposit(&mut target_position, target_amount, now_ts)?;
let mut resulting_cache = self.clone();
resulting_cache
.adjust_token_balance(&source_bank, -amount)
.unwrap();
resulting_cache
.adjust_token_balance(&target_bank, target_amount)
.unwrap();
resulting_cache
resulting_cache.adjust_token_balance(&source_bank, -amount)?;
resulting_cache.adjust_token_balance(&target_bank, target_amount)?;
Ok(resulting_cache)
}
/// How much source native tokens may be swapped for target tokens while staying
/// How many source native tokens may be swapped for target tokens while staying
/// above the min_ratio health ratio.
///
/// `price`: The amount of target native you receive for one source native. So if we
@ -89,11 +87,14 @@ impl HealthCache {
/// should be 500000 native_SOL for a native_BTC. Because 1 BTC gives you 500 SOL
/// so 1e6 native_BTC gives you 500e9 native_SOL.
///
/// Positions for the source and deposit token index must already exist in the account.
///
/// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here
pub fn max_swap_source_for_health_ratio(
&self,
account: &MangoAccountValue,
source_bank: &Bank,
source_oracle_price: I80F48,
target_bank: &Bank,
price: I80F48,
min_ratio: I80F48,
@ -113,8 +114,11 @@ impl HealthCache {
return Ok(I80F48::ZERO);
}
// Fail if the health cache (or consequently the account) don't have existing
// positions for the source and target token index.
let source_index = find_token_info_index(&self.token_infos, source_bank.token_index)?;
let target_index = find_token_info_index(&self.token_infos, target_bank.token_index)?;
let source = &self.token_infos[source_index];
let target = &self.token_infos[target_index];
@ -127,14 +131,37 @@ impl HealthCache {
let final_health_slope = -source.init_liab_weight * source.prices.liab(health_type)
+ target.init_asset_weight * target.prices.asset(health_type) * price;
if final_health_slope >= 0 {
// TODO: not true if weights scaled with deposits/borrows
return Ok(I80F48::MAX);
}
let cache_after_swap = |amount: I80F48| {
self.cache_after_swap(account, source_bank, target_bank, amount, price)
let cache_after_swap = |amount: I80F48| -> Result<Option<HealthCache>> {
let maybe_cache = self.cache_after_swap(
account,
source_bank,
source_oracle_price,
target_bank,
amount,
price,
);
match maybe_cache {
Ok(cache) => Ok(Some(cache)),
// Special case net borrow errors: We want to be able to find a good
// swap amount even if the max swap is limited by the net borrow limit.
Err(Error::AnchorError(err))
if err.error_code_number
== MangoError::BankNetBorrowsLimitReached.error_code() =>
{
Ok(None)
}
Err(err) => Err(err),
}
};
let health_ratio_after_swap = |amount| {
Ok(cache_after_swap(amount)?
.map(|c| c.health_ratio(HealthType::Init))
.unwrap_or(I80F48::MIN))
};
let health_ratio_after_swap =
|amount| cache_after_swap(amount).health_ratio(HealthType::Init);
// There are two key slope changes: Assume source.balance > 0 and target.balance < 0.
// When these values flip sign, the health slope decreases, but could still be positive.
@ -149,13 +176,12 @@ impl HealthCache {
.balance_native
.max(source_for_zero_target_balance)
.max(I80F48::ZERO);
let point0_ratio = health_ratio_after_swap(point0_amount);
let point0_ratio = health_ratio_after_swap(point0_amount)?;
let (point1_ratio, point1_health) = {
let cache = cache_after_swap(point1_amount);
(
cache.health_ratio(HealthType::Init),
cache.health(HealthType::Init),
)
let cache = cache_after_swap(point1_amount)?;
cache
.map(|c| (c.health_ratio(HealthType::Init), c.health(HealthType::Init)))
.unwrap_or((I80F48::MIN, I80F48::MIN))
};
let amount =
@ -184,14 +210,12 @@ impl HealthCache {
return Ok(I80F48::ZERO);
}
let zero_health_amount = point1_amount - point1_health / final_health_slope;
let zero_health_ratio = health_ratio_after_swap(zero_health_amount);
binary_search(
point1_amount,
point1_ratio,
zero_health_amount,
zero_health_ratio,
min_ratio,
I80F48::ZERO,
I80F48::from_num(0.1),
health_ratio_after_swap,
)?
} else if point0_ratio >= min_ratio {
@ -200,9 +224,8 @@ impl HealthCache {
point0_amount,
point0_ratio,
point1_amount,
point1_ratio,
min_ratio,
I80F48::ZERO,
I80F48::from_num(0.1),
health_ratio_after_swap,
)?
} else {
@ -211,13 +234,13 @@ impl HealthCache {
I80F48::ZERO,
initial_ratio,
point0_amount,
point0_ratio,
min_ratio,
I80F48::ZERO,
I80F48::from_num(0.1),
health_ratio_after_swap,
)?
};
assert!(amount >= 0);
Ok(amount)
}
@ -263,15 +286,15 @@ impl HealthCache {
return Ok(i64::MAX);
}
let cache_after_trade = |base_lots: i64| {
let cache_after_trade = |base_lots: i64| -> Result<HealthCache> {
let mut adjusted_cache = self.clone();
adjusted_cache.perp_infos[perp_info_index].base_lots += direction * base_lots;
adjusted_cache.perp_infos[perp_info_index].quote -=
I80F48::from(direction) * I80F48::from(base_lots) * base_lot_size * price;
adjusted_cache
Ok(adjusted_cache)
};
let health_ratio_after_trade =
|base_lots: i64| cache_after_trade(base_lots).health_ratio(HealthType::Init);
|base_lots: i64| Ok(cache_after_trade(base_lots)?.health_ratio(HealthType::Init));
let health_ratio_after_trade_trunc =
|base_lots: I80F48| health_ratio_after_trade(base_lots.round_to_zero().to_num());
@ -285,7 +308,7 @@ impl HealthCache {
let (case1_start, case1_start_ratio) = if has_case2 {
let case1_start = initial_base_lots.abs();
let case1_start_ratio = health_ratio_after_trade(case1_start);
let case1_start_ratio = health_ratio_after_trade(case1_start)?;
(case1_start, case1_start_ratio)
} else {
(0, initial_ratio)
@ -305,7 +328,7 @@ impl HealthCache {
// Need to figure out how many lots to trade to reach zero health (zero_health_amount).
// We do this by looking at the starting health and the health slope per
// traded base lot (final_health_slope).
let start_cache = cache_after_trade(case1_start);
let start_cache = cache_after_trade(case1_start)?;
let start_health = start_cache.health(HealthType::Init);
if start_health <= 0 {
return Ok(0);
@ -323,14 +346,13 @@ impl HealthCache {
let zero_health_amount = case1_start_i80f48
- start_health_uncapped / final_health_slope / base_lot_size
+ I80F48::ONE;
let zero_health_ratio = health_ratio_after_trade_trunc(zero_health_amount);
let zero_health_ratio = health_ratio_after_trade_trunc(zero_health_amount)?;
assert!(zero_health_ratio <= 0);
binary_search(
case1_start_i80f48,
case1_start_ratio,
zero_health_amount,
zero_health_ratio,
min_ratio,
I80F48::ONE,
health_ratio_after_trade_trunc,
@ -341,7 +363,6 @@ impl HealthCache {
I80F48::ZERO,
initial_ratio,
case1_start_i80f48,
case1_start_ratio,
min_ratio,
I80F48::ONE,
health_ratio_after_trade_trunc,
@ -356,15 +377,16 @@ fn binary_search(
mut left: I80F48,
left_value: I80F48,
mut right: I80F48,
right_value: I80F48,
target_value: I80F48,
min_step: I80F48,
fun: impl Fn(I80F48) -> I80F48,
fun: impl Fn(I80F48) -> Result<I80F48>,
) -> Result<I80F48> {
let max_iterations = 20;
let target_error = I80F48!(0.1);
let right_value = fun(right)?;
require_msg!(
(left_value - target_value).signum() * (right_value - target_value).signum() != I80F48::ONE,
(left_value <= target_value && right_value >= target_value)
|| (left_value >= target_value && right_value <= target_value),
"internal error: left {} and right {} don't contain the target value {}",
left_value,
right_value,
@ -375,8 +397,8 @@ fn binary_search(
return Ok(left);
}
let new = I80F48::from_num(0.5) * (left + right);
let new_value = fun(new);
let error = new_value - target_value;
let new_value = fun(new)?;
let error = new_value.saturating_sub(target_value);
if error > 0 && error < target_error {
return Ok(new);
}
@ -419,13 +441,20 @@ mod tests {
};
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let account = MangoAccountValue::from_bytes(&buffer).unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
account.ensure_token_position(0).unwrap();
account.ensure_token_position(1).unwrap();
account.ensure_token_position(2).unwrap();
let group = Pubkey::new_unique();
let (mut bank0, _) = mock_bank_and_oracle(group, 0, 1.0, 0.1, 0.1);
let (mut bank1, _) = mock_bank_and_oracle(group, 1, 5.0, 0.2, 0.2);
let (mut bank2, _) = mock_bank_and_oracle(group, 2, 5.0, 0.3, 0.3);
let banks = [bank0.data(), bank1.data(), bank2.data()];
let banks = [
bank0.data().clone(),
bank1.data().clone(),
bank2.data().clone(),
];
let health_cache = HealthCache {
token_infos: vec![
@ -456,8 +485,9 @@ mod tests {
health_cache
.max_swap_source_for_health_ratio(
&account,
banks[0],
banks[1],
&banks[0],
I80F48::from(1),
&banks[1],
I80F48::from_num(2.0 / 3.0),
I80F48::from_num(50.0)
)
@ -473,7 +503,8 @@ mod tests {
source: TokenIndex,
target: TokenIndex,
ratio: f64,
price_factor: f64| {
price_factor: f64,
banks: [Bank; 3]| {
let source_price = &c.token_infos[source as usize].prices;
let source_bank = &banks[source as usize];
let target_price = &c.token_infos[target as usize].prices;
@ -484,37 +515,49 @@ mod tests {
.max_swap_source_for_health_ratio(
&account,
source_bank,
source_price.oracle,
target_bank,
swap_price,
I80F48::from_num(ratio),
)
.unwrap();
if source_amount == I80F48::MAX {
return (f64::MAX, f64::MAX);
return (f64::MAX, f64::MAX, f64::MAX, f64::MAX);
}
let after_swap = c.cache_after_swap(
&account,
source_bank,
target_bank,
source_amount,
swap_price,
);
let ratio_for_amount = |amount| {
c.cache_after_swap(
&account,
source_bank,
source_price.oracle,
target_bank,
I80F48::from(amount),
swap_price,
)
.map(|c| c.health_ratio(HealthType::Init).to_num::<f64>())
.unwrap_or(f64::MIN)
};
// With the binary search error, we can guarantee just +-1
(
source_amount.to_num::<f64>(),
after_swap.health_ratio(HealthType::Init).to_num::<f64>(),
source_amount.to_num(),
ratio_for_amount(source_amount),
ratio_for_amount(source_amount.saturating_sub(I80F48::ONE)),
ratio_for_amount(source_amount.saturating_add(I80F48::ONE)),
)
};
let check_max_swap_result = |c: &HealthCache,
source: TokenIndex,
target: TokenIndex,
ratio: f64,
price_factor: f64| {
let (source_amount, actual_ratio) =
find_max_swap_actual(c, source, target, ratio, price_factor);
price_factor: f64,
banks: [Bank; 3]| {
let (source_amount, actual_ratio, minus_ratio, plus_ratio) =
find_max_swap_actual(c, source, target, ratio, price_factor, banks);
println!(
"checking {source} to {target} for price_factor: {price_factor}, target ratio {ratio}: actual ratio: {actual_ratio}, amount: {source_amount}",
"checking {source} to {target} for price_factor: {price_factor}, target ratio {ratio}: actual ratios: {minus_ratio}/{actual_ratio}/{plus_ratio}, amount: {source_amount}",
);
assert!((ratio - actual_ratio).abs() < 1.0);
assert!(actual_ratio >= ratio);
assert!(minus_ratio < ratio || actual_ratio < minus_ratio);
assert!(plus_ratio < ratio);
};
{
@ -525,15 +568,15 @@ mod tests {
for price_factor in [0.1, 0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check_max_swap_result(&health_cache, 0, 1, target, price_factor);
check_max_swap_result(&health_cache, 1, 0, target, price_factor);
check_max_swap_result(&health_cache, 0, 2, target, price_factor);
check_max_swap_result(&health_cache, 0, 1, target, price_factor, banks);
check_max_swap_result(&health_cache, 1, 0, target, price_factor, banks);
check_max_swap_result(&health_cache, 0, 2, target, price_factor, banks);
}
}
// At this unlikely price it's healthy to swap infinitely
assert_eq!(
find_max_swap_actual(&health_cache, 0, 1, 50.0, 1.5).0,
find_max_swap_actual(&health_cache, 0, 1, 50.0, 1.5, banks).0,
f64::MAX
);
}
@ -547,10 +590,10 @@ mod tests {
for price_factor in [0.1, 0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check_max_swap_result(&health_cache, 0, 1, target, price_factor);
check_max_swap_result(&health_cache, 1, 0, target, price_factor);
check_max_swap_result(&health_cache, 0, 2, target, price_factor);
check_max_swap_result(&health_cache, 2, 0, target, price_factor);
check_max_swap_result(&health_cache, 0, 1, target, price_factor, banks);
check_max_swap_result(&health_cache, 1, 0, target, price_factor, banks);
check_max_swap_result(&health_cache, 0, 2, target, price_factor, banks);
check_max_swap_result(&health_cache, 2, 0, target, price_factor, banks);
}
}
}
@ -561,7 +604,7 @@ mod tests {
adjust_by_usdc(&mut health_cache, 0, -50.0);
adjust_by_usdc(&mut health_cache, 1, 100.0);
// possible even though the init ratio is <100
check_max_swap_result(&health_cache, 1, 0, 100.0, 1.0);
check_max_swap_result(&health_cache, 1, 0, 100.0, 1.0, banks);
}
{
@ -574,13 +617,14 @@ mod tests {
// swapping with a high ratio advises paying back all liabs
// and then swapping even more because increasing assets in 0 has better asset weight
let init_ratio = health_cache.health_ratio(HealthType::Init);
let (amount, actual_ratio) = find_max_swap_actual(&health_cache, 1, 0, 100.0, 1.0);
let (amount, actual_ratio, _, _) =
find_max_swap_actual(&health_cache, 1, 0, 100.0, 1.0, banks);
println!(
"init {}, after {}, amount {}",
init_ratio, actual_ratio, amount
);
assert!(actual_ratio / 2.0 > init_ratio);
assert!((amount - 100.0 / 3.0).abs() < 1.0);
assert!((amount as f64 - 100.0 / 3.0).abs() < 1.0);
}
{
@ -593,9 +637,22 @@ mod tests {
let init_ratio = health_cache.health_ratio(HealthType::Init);
assert!(init_ratio > 3 && init_ratio < 4);
check_max_swap_result(&health_cache, 0, 1, 1.0, 1.0);
check_max_swap_result(&health_cache, 0, 1, 3.0, 1.0);
check_max_swap_result(&health_cache, 0, 1, 4.0, 1.0);
check_max_swap_result(&health_cache, 0, 1, 1.0, 1.0, banks);
check_max_swap_result(&health_cache, 0, 1, 3.0, 1.0, banks);
check_max_swap_result(&health_cache, 0, 1, 4.0, 1.0, banks);
}
{
// check with net borrow limits
println!("test 5");
let mut health_cache = health_cache.clone();
adjust_by_usdc(&mut health_cache, 1, 100.0);
let mut banks = banks.clone();
banks[0].net_borrows_limit_quote = 50;
// The net borrow limit restricts the amount that can be swapped
// (tracking happens without decimals)
assert!(find_max_swap_actual(&health_cache, 0, 1, 1.0, 1.0, banks).0 < 51.0);
}
}

View File

@ -8,9 +8,7 @@ use fixed::types::I80F48;
use solana_program::program_memory::sol_memmove;
use static_assertions::const_assert_eq;
use crate::error::Contextable;
use crate::error::MangoError;
use crate::error_msg;
use crate::error::*;
use super::dynamic_account::*;
use super::BookSideOrderTree;
@ -437,7 +435,13 @@ impl<
self.all_token_positions()
.enumerate()
.find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| (p, raw_index)))
.ok_or_else(|| error_msg!("position for token index {} not found", token_index))
.ok_or_else(|| {
error_msg_typed!(
TokenPositionDoesNotExist,
"position for token index {} not found",
token_index
)
})
}
pub fn token_position(&self, token_index: TokenIndex) -> Result<&TokenPosition> {
@ -584,7 +588,13 @@ impl<
.all_token_positions()
.enumerate()
.find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| raw_index))
.ok_or_else(|| error_msg!("position for token index {} not found", token_index))?;
.ok_or_else(|| {
error_msg_typed!(
TokenPositionDoesNotExist,
"position for token index {} not found",
token_index
)
})?;
Ok((self.token_position_mut_by_raw_index(raw_index), raw_index))
}