//! The accounting kernel: the arithmetic every bookkeeping invariant rests //! on, in one place. //! //! Each function here is small, pure, and generic over [`Amount`]. The //! engine runs it on [`Decimal`]; the Kani harnesses in `proofs.rs` run the //! very same code on machine integers and prove it correct for every input //! within their bounds; and the Lean model in `verify/lean` proves the //! laws it implements hold for books of any size. See //! `docs/verification.md` for which proof covers which law. //! //! The laws, in the order they appear (each traced to its authority in //! `docs/spec/rules.md`): //! //! * a line's signed amount is `+amount` for a debit, `-amount` for a credit //! (B-2); //! * an entry is acceptable when it has two or more lines, every amount is //! positive and in range, and its debits equal its credits exactly (B-1, //! B-2, B-21); //! * an acceptable entry nets to zero, account by account summed (B-1); //! * a reversal flips every side, so it is acceptable exactly when the //! original is, and the two together net every account to zero (B-9); //! * an account's balance is its movement read on its normal side, so the //! balances satisfy the accounting equation whenever movements net to //! zero (B-5, B-7); //! * a trial balance puts each movement in exactly one column, so its //! columns differ by the net of all movements (B-6); //! * a closing line zeroes the account it closes (B-11); //! * a payment to a vendor splits, line by line, into what counts toward //! its 1099, what a card or payment network paid, and what is unknown, //! and money moved between the company's own sources nets out (B-19); //! * an allocation's parts sum to the whole, to the last unit, by largest //! remainder (B-13); //! * a cumulative allocation hands out exactly the new amount, and leaves //! each running total one allocation of everything allocated, so it never //! drifts a unit from its exact share (B-13); //! * an entry's cash is applied to the sections of the statement of cash //! flows moving against it, in a fixed order, each up to its own //! movement: the parts sum to the entry's cash, what no cash settled sums //! to zero, and a reversal's parts are the original's negated (B-14); //! * what a member or lender paid or received for the company, with no //! cash account moving, is split among the kinds it settled, netting to //! zero, and mirrored by a reversal (B-14 rule 11); //! * a party's part of an entry is signed as a line on the party's own //! account would be, a reversal's is its original's negated, and what of //! it counts as money or as property never exceeds it (B-35); //! * a span prorated by days (depreciation, B-15; daily schedules, B-16) //! splits into parts that sum to its total, each but the last its days //! times the monthly rate within one unit, the last the remainder; //! * a catch-up run brings the books to the schedule without ever carrying //! more than it says at a month end already posted (B-15), and a //! disposal trues what the books carry up or down to the schedule; //! * an asset's basis figures follow from its basis rule and facts: the //! §723 basis is the contributor's adjusted basis, the depreciable basis //! the cost, that basis, or the lesser of it and fair market value, and //! the §704(c) built-in gain is fair market value less the §723 basis //! (B-25). use crate::assets::BasisRule; use crate::{AccountType, Side}; use rust_decimal::Decimal; /// What the kernel needs of a money amount: exact, ordered, and checked /// arithmetic that reports overflow instead of rounding or wrapping. pub trait Amount: Copy + Ord { /// Zero. const ZERO: Self; /// `self + rhs`, or `None` when it cannot be represented exactly. fn checked_add(self, rhs: Self) -> Option; /// `-self`, or `None` when it cannot be represented. fn checked_neg(self) -> Option; /// True when the amount is small and precise enough for a journal line /// (see [`MAX_LINE_AMOUNT`] and [`MAX_LINE_SCALE`] for [`Decimal`]). fn in_line_range(self) -> bool; /// `self - rhs`, or `None` when it cannot be represented exactly. fn checked_sub(self, rhs: Self) -> Option { self.checked_add(rhs.checked_neg()?) } } /// The largest amount one journal line may carry: one quadrillion units of /// the base currency. Far beyond any real transaction, and far enough /// below [`Decimal::MAX`] that no realistic sum of lines can overflow. pub const MAX_LINE_AMOUNT: Decimal = Decimal::from_parts(0xA4C6_8000, 0x0003_8D7E, 0, false, 0); /// The most decimal places a journal line may carry. Cents need two; /// foreign-currency lines (amount times rate) need more; ten keeps every /// sum of lines exact as long as the total stays under 7.9 quintillion. pub const MAX_LINE_SCALE: u32 = 10; impl Amount for Decimal { const ZERO: Self = Decimal::ZERO; fn checked_add(self, rhs: Self) -> Option { Decimal::checked_add(self, rhs) } fn checked_neg(self) -> Option { Some(-self) } fn in_line_range(self) -> bool { self.abs() <= MAX_LINE_AMOUNT && self.normalize().scale() <= MAX_LINE_SCALE } } impl Amount for i64 { const ZERO: Self = 0; fn checked_add(self, rhs: Self) -> Option { i64::checked_add(self, rhs) } fn checked_neg(self) -> Option { i64::checked_neg(self) } fn in_line_range(self) -> bool { // Scaled down with the same headroom as Decimal's range: lines // this size can be summed many times over without overflow. self.unsigned_abs() <= (1 << 40) } } /// Why the kernel rejected an entry's lines. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LineError { /// Fewer than two lines. TooFewLines, /// A line with a zero amount. ZeroAmount, /// A line with a negative amount: direction is the side's job. NegativeAmount, /// A line too large or too precise for a journal line. OutOfRange, /// Debits and credits differ. Unbalanced { /// Sum of debit lines. debits: T, /// Sum of credit lines. credits: T, }, } /// The other side. pub fn flip(side: Side) -> Side { match side { Side::Debit => Side::Credit, Side::Credit => Side::Debit, } } /// A line's signed amount: positive for a debit, negative for a credit. pub fn signed(side: Side, amount: T) -> Option { match side { Side::Debit => Some(amount), Side::Credit => amount.checked_neg(), } } /// Total debits and total credits of some lines. pub fn totals(lines: impl IntoIterator) -> Option<(T, T)> { let mut debits = T::ZERO; let mut credits = T::ZERO; for (side, amount) in lines { match side { Side::Debit => debits = debits.checked_add(amount)?, Side::Credit => credits = credits.checked_add(amount)?, } } Some((debits, credits)) } /// The net of some lines: debits minus credits. pub fn net(lines: impl IntoIterator) -> Option { let mut total = T::ZERO; for (side, amount) in lines { total = total.checked_add(signed(side, amount)?)?; } Some(total) } /// Check an entry's lines: two or more, every amount positive and within /// line range, debits equal to credits. Accounts and currencies are the /// caller's to check; this is the arithmetic. pub fn check_lines(lines: &[(Side, T)]) -> Result<(), LineError> { if lines.len() < 2 { return Err(LineError::TooFewLines); } for &(_, amount) in lines { if amount == T::ZERO { return Err(LineError::ZeroAmount); } if amount < T::ZERO { return Err(LineError::NegativeAmount); } if !amount.in_line_range() { return Err(LineError::OutOfRange); } } let (debits, credits) = totals(lines.iter().copied()).ok_or(LineError::OutOfRange)?; if debits != credits { return Err(LineError::Unbalanced { debits, credits }); } Ok(()) } /// Net movement per account (debits minus credits), in order of first /// appearance. pub fn movements( lines: impl IntoIterator, ) -> Option> { let lines: Vec<_> = lines.into_iter().collect(); let Some(&(first, _, _)) = lines.first() else { return Some(Vec::new()); }; // At most one account per line. let mut out = vec![(first, T::ZERO); lines.len()]; let n = movements_into(&lines, &mut out)?; out.truncate(n); Some(out) } /// [`movements`] into a caller's buffer, without allocating: returns how /// many accounts it wrote, or `None` on overflow or if `out` is too small. pub fn movements_into( lines: &[(K, Side, T)], out: &mut [(K, T)], ) -> Option { let mut n = 0; for &(account, side, amount) in lines { let delta = signed(side, amount)?; match out[..n].iter_mut().find(|(k, _)| *k == account) { Some((_, total)) => *total = total.checked_add(delta)?, None => { *out.get_mut(n)? = (account, delta); n += 1; } } } Some(n) } /// The lines of a reversing entry: every side flipped, accounts and /// amounts unchanged. pub fn reversal(lines: &[(K, Side, T)]) -> Vec<(K, Side, T)> { lines.iter().map(|&line| reverse_line(line)).collect() } /// One line of a reversing entry: the side flipped, account and amount /// unchanged. pub fn reverse_line((account, side, amount): (K, Side, T)) -> (K, Side, T) { (account, flip(side), amount) } /// An account's balance read on its normal side: positive is a normal /// balance, negative a contra balance. pub fn normal_balance(account_type: AccountType, movement: T) -> Option { match account_type.normal_side() { Side::Debit => Some(movement), Side::Credit => movement.checked_neg(), } } /// A movement in trial-balance columns: `(debit, credit)`, one of them /// zero, both non-negative. pub fn columns(movement: T) -> Option<(T, T)> { if movement > T::ZERO { Some((movement, T::ZERO)) } else if movement == T::ZERO { // Never a negative zero: money never prints "-0". Some((T::ZERO, T::ZERO)) } else { Some((T::ZERO, movement.checked_neg()?)) } } /// The line that zeroes an account with this movement: the opposite side, /// for the same amount. `None` for a zero movement (nothing to close). pub fn closing_line(movement: T) -> Option<(Side, T)> { if movement > T::ZERO { Some((Side::Credit, movement)) } else if movement < T::ZERO { Some((Side::Debit, movement.checked_neg()?)) } else { None } } /// The line that credits `credit` to an account (a negative credit is a /// debit). `None` for zero. This is [`closing_line`]: crediting `c` and /// zeroing a movement of `c` are the same line. pub fn credit_line(credit: T) -> Option<(Side, T)> { closing_line(credit) } // --------------------------------------------------------------------------- // 1099 payments: where each payment line goes (rules.md B-19) // --------------------------------------------------------------------------- /// A line's role in a payment to a vendor, from its account. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PayRole { /// One of the books' cash accounts: bank, cash on hand. Cash, /// One of the books' financing liabilities (what members or lenders /// paid on the company's behalf), or an account a member's card or /// cash, or a payment platform, pays through: how a payment through /// it was made, only the entry's method says. Financing, /// The account of a company card (a business card's liability). CardAccount, /// None of these (an expense, payables, prepaid): not a payment line. Other, } /// What an entry's payment-method tag says about one of its lines. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Tag { /// The entry has no payment method recorded. None, /// The method pays through this line's account and is a card or a /// payment network. ThisCard, /// The method pays through this line's account and is neither (the /// company's bank, a member's own cash or check). ThisOther, /// The method pays through another account: it doesn't describe this /// line. Elsewhere, } /// Where a payment line's amount goes on the 1099 worksheet. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Bucket { /// Counts toward the payee's 1099. Counts, /// Paid by card or payment network: Form 1099-K's, never 1099-NEC or /// 1099-MISC's. Card, /// Can't be classified until the payment method is recorded. Undetermined, } impl Bucket { /// Every bucket, in [`split_payment`]'s order. pub const ALL: [Bucket; 3] = [Bucket::Counts, Bucket::Card, Bucket::Undetermined]; /// Its place in [`split_payment`]'s array. pub const fn index(self) -> usize { match self { Bucket::Counts => 0, Bucket::Card => 1, Bucket::Undetermined => 2, } } } /// The bucket a payment line goes in, from its role and the entry's tag. /// /// ```text /// GOAL: every payment line to a vendor counts toward its 1099, is a card /// or payment-network payment (Form 1099-K), or is unknown; never /// guessed. /// SOURCE: docs/spec/rules.md B-19 (Treas. Reg. §1.6041-1(a)(1)(iv): a /// transaction reportable under §6050W is not reported under /// §6041; §1.6041-1(e)(1): a member writing a check at the /// company's direction doesn't make the member the payor; /// sources/irs-1099-payment-method.md); DECISION D-40 (owner, /// 2026-10-20: a bank payment with no method recorded counts). /// INVARIANTS (first match wins): /// 1. The tag names this line's account: Card for a card or network, /// else Counts. /// 2. Otherwise the role decides: Cash counts (presumed check, ACH or /// transfer, D-40); Financing is Undetermined (a member's card or /// their check, unknown until tagged); CardAccount is Card; Other is /// no payment line (None). /// ``` pub fn payment_bucket(role: PayRole, tag: Tag) -> Option { match (tag, role) { (Tag::ThisCard, _) => Some(Bucket::Card), (Tag::ThisOther, _) => Some(Bucket::Counts), (Tag::None | Tag::Elsewhere, PayRole::Cash) => Some(Bucket::Counts), (Tag::None | Tag::Elsewhere, PayRole::Financing) => Some(Bucket::Undetermined), (Tag::None | Tag::Elsewhere, PayRole::CardAccount) => Some(Bucket::Card), (Tag::None | Tag::Elsewhere, PayRole::Other) => None, } } /// What an entry paid, per bucket: credits minus debits of the lines in /// each (a line in no bucket is not a payment line). `None` on overflow. pub fn split_payment(lines: &[(Option, Side, T)]) -> Option<[T; 3]> { let mut out = [T::ZERO; 3]; for &(bucket, side, amount) in lines { if let Some(b) = bucket { let i = b.index(); out[i] = match side { Side::Credit => out[i].checked_add(amount)?, Side::Debit => out[i].checked_sub(amount)?, }; } } Some(out) } /// Net one entry's buckets ([`split_payment`]): money moved between the /// company's own payment sources pays no one. /// /// ```text /// GOAL: a card bill paid from the bank, a member reimbursed, or a loan /// paid down never counts toward a vendor's 1099. /// SOURCE: docs/spec/rules.md B-19 (26 U.S.C. §6041(a): payments made to /// the payee; P3 review R1, 2026-10-20). /// INVARIANTS: /// 1. The buckets' sum P is kept exactly. /// 2. No bucket ends opposite in sign to P. /// 3. Buckets all of one sign (zeros aside) come back unchanged. /// SCENARIOS: /// 1. IF the signs differ and P = 0 (Dr 2050 / Cr 1000, the card bill): /// SHOULD be [0, 0, 0]. /// 2. IF the signs differ and exactly one bucket has P's sign: SHOULD /// give it P and the others 0. /// 3. IF two buckets have P's sign: SHOULD put P in Undetermined /// BECAUSE which source the other side offsets is unknown (split /// the entry). /// ``` pub fn net_buckets(b: [T; 3]) -> Option<[T; 3]> { let positive = b.iter().any(|&x| x > T::ZERO); let negative = b.iter().any(|&x| x < T::ZERO); if !(positive && negative) { return Some(b); } let p = b[0].checked_add(b[1])?.checked_add(b[2])?; let mut out = [T::ZERO; 3]; if p == T::ZERO { return Some(out); } let mut taker = Bucket::Undetermined.index(); let mut takers = 0; for (i, &x) in b.iter().enumerate() { if x != T::ZERO && (x > T::ZERO) == (p > T::ZERO) { taker = i; takers += 1; } } if takers != 1 { taker = Bucket::Undetermined.index(); } out[taker] = p; Some(out) } /// Make rounded parts sum to the whole: the last part becomes `whole` /// minus the others, absorbing every unit of rounding. Returns `None` on /// overflow, leaving `parts` unchanged. pub fn plug_last(whole: T, parts: &mut [T]) -> Option<()> { let Some((last, rest)) = parts.split_last_mut() else { return Some(()); }; let mut others = T::ZERO; for &p in rest.iter() { others = others.checked_add(p)?; } *last = whole.checked_sub(others)?; Some(()) } /// Largest-remainder apportionment: make rounded-down `parts` sum to /// `whole`, handing out the shortfall one `unit` at a time, largest /// `remainders` first (ties to the earlier part). Any final fraction of a /// unit (when `whole` isn't a whole number of units) goes to the first /// part in that order. /// /// ```text /// GOAL: split a whole into parts, each a whole number of units where /// possible, summing to the whole exactly, without favoring anyone. /// SOURCE: docs/spec/rules.md B-13 (shares: 26 U.S.C. §704(a); cent /// rounding: DECISION, owner 2026-10-13), B-21 (exact totals). /// INVARIANTS: /// 1. MUST make the parts sum to `whole` exactly. /// 2. MUST change each part by zero or one unit (the first in order /// may also take the sub-unit fraction). /// 3. NEVER gives a unit to a part while a part with a strictly larger /// remainder goes without. /// SCENARIOS: /// 1. IF the shortfall is negative, or `n` units or more (the parts /// weren't rounded down from shares of `whole`): /// - SHOULD return None, leaving `parts` unchanged /// BECAUSE apportioning only corrects rounding. /// 2. IF $100.00 splits three equal ways: /// - SHOULD be 33.34, 33.33, 33.33 /// - NEVER the same partner absorbing every odd cent by position /// BECAUSE the cent goes to the largest remainder, ties to the first /// (owner, 2026-10-13). /// ``` /// /// Allocation-free and O(n^2): it ranks each part by counting the parts /// ahead of it, so the model checker can prove it outright. pub fn apportion(whole: T, parts: &mut [T], remainders: &[T], unit: T) -> Option<()> { if parts.len() != remainders.len() || unit <= T::ZERO { return None; } if parts.is_empty() { // Nothing to give the whole to. return (whole == T::ZERO).then_some(()); } let mut shortfall = whole; for &p in parts.iter() { shortfall = shortfall.checked_sub(p)?; } if shortfall < T::ZERO { return None; } // Whole units short, by subtraction: no division in `Amount`. A // shortfall of a unit per part or more isn't rounding; refuse it. let mut units = 0; while shortfall >= unit { shortfall = shortfall.checked_sub(unit)?; units += 1; if units == parts.len() { return None; } } // A part's rank: the parts ahead of it (larger remainder, or equal and // earlier). let rank = |i: usize| { (0..remainders.len()) .filter(|&j| remainders[j] > remainders[i] || (remainders[j] == remainders[i] && j < i)) .count() }; for (i, part) in parts.iter_mut().enumerate() { let r = rank(i); let mut add = T::ZERO; if r < units { add = unit; } if r == 0 { add = add.checked_add(shortfall)?; } *part = part.checked_add(add)?; } Some(()) } /// Cumulative apportionment: what one close gives each part when the /// running `total` is apportioned afresh ([`apportion`]) and what earlier /// closes gave each part (`given`) is taken back. `parts` and /// `remainders` are the rounded-down shares of `total` and what rounding /// took from each, as for [`apportion`]. /// /// ```text /// GOAL: members' running totals that never drift a unit from their /// exact shares, whatever earlier closes did. /// SOURCE: docs/spec/rules.md B-13 (shares: 26 U.S.C. §704(a); capital /// accounts carry the allocations: Treas. Reg. /// §1.704-1(b)(2)(iv)(b); cent rounding: DECISION, owner /// 2026-10-13; counted from the first close: D-10, 2026-10-16). /// INVARIANTS: /// 1. MUST make the parts sum to `total − Σ given` exactly. /// 2. MUST leave given[i] + parts[i] equal to what `apportion(total, …)` /// gives part i: a running total is one apportionment, so it carries /// one apportionment's rounding, never a sum of them. /// 3. On None, `parts` MUST be unchanged. /// SCENARIOS: /// 1. IF the lengths differ, the unit isn't positive, `total`, the /// unit, a part or a `given` is out of line range, or apportion /// refuses: SHOULD return None, leaving `parts` unchanged /// BECAUSE in range, the subtraction can't overflow, so a refusal is /// known before anything changes. /// 2. IF 0.03 was closed as A 0.03 / B 0 and the running total is now /// 0.04 (50/50, floors 0.02 / 0.02): SHOULD give A −0.01, B +0.02 /// BECAUSE 0.04 apportions 0.02 each. /// ``` /// /// Allocation-free, with no bulk copy (docs/verification.md, "Trusting /// the checker"), so the model checker can prove it outright. pub fn apportion_cumulative( total: T, parts: &mut [T], remainders: &[T], unit: T, given: &[T], ) -> Option<()> { if parts.len() != given.len() || !total.in_line_range() || !unit.in_line_range() { return None; } if parts.iter().chain(given).any(|a| !a.in_line_range()) { return None; } apportion(total, parts, remainders, unit)?; // Each part is now under three line ranges and each `given` within // one: the subtraction can't fail (Kani apportion_cumulative_is_exact). for (part, &g) in parts.iter_mut().zip(given) { *part = part.checked_sub(g)?; } Some(()) } /// Where a line falls on the statement of cash flows (rules.md B-14 rule /// 2): a cash account, or one of the three sections. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum Flow { /// A cash account: what the statement explains the change in. Cash, /// Operating activities. Operating, /// Investing activities. Investing, /// Financing activities. Financing, } impl Flow { /// The three sections, in the order cash is applied to them (rules.md /// B-14 rule 3): operating, investing, financing. pub const SECTIONS: [Flow; 3] = [Flow::Operating, Flow::Investing, Flow::Financing]; } /// One entry's effect on the statement of cash flows. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct EntryFlows { /// Net movement of the entry's cash lines, debit-positive: + is cash /// in. pub cash: T, /// Cash per section, `[operating, investing, financing]`, + is in. /// Sums to `cash`. pub flows: [T; 3], /// What no cash settled, per section, debit-positive: the section's /// movement plus its cash. Sums to zero. pub noncash: [T; 3], } /// The cash one section takes from what is left of an entry's cash /// (`remaining`), given the section's net movement (debit-positive): only /// a section moving against the cash takes any, and never more than its /// own movement. Cash in (`remaining > 0`) settles credits (`movement < /// 0`); cash out settles debits. pub fn take(remaining: T, movement: T) -> Option { let against = movement.checked_neg()?; Some(if remaining > T::ZERO && movement < T::ZERO { remaining.min(against) } else if remaining < T::ZERO && movement > T::ZERO { remaining.max(against) } else { T::ZERO }) } /// Apply `cash` to `movements` in order, each taking [`take`] of what is /// left: `applied[k]` is the cash of part `k`. `None` when something is /// left over (the movements against the cash are smaller than it), the /// slices differ in length, or on overflow. /// /// ```text /// GOAL: split an entry's cash among the parts it settled, without /// inventing or losing any. /// SOURCE: rules.md B-14 rule 3 (ASC 230-10-50-5: only the cash portion /// of a part-cash transaction is a cash flow; the order is a /// CONVENTION, the ASC is silent). /// INVARIANTS: /// 1. On success the parts sum to `cash` exactly. /// 2. Each part has the sign of `cash` (or is zero) and is no larger /// than its own movement, which runs against the cash. /// 3. Applying `-cash` to the negated movements gives the negated parts /// (a reversal mirrors its original). /// SCENARIOS: /// 1. IF the movements against the cash sum to at least the cash: /// - SHOULD succeed (a balanced entry always does) /// 2. IF they don't: SHOULD return None, never a part out of thin air. /// ``` pub fn apply_cash(cash: T, movements: &[T], applied: &mut [T]) -> Option<()> { if movements.len() != applied.len() { return None; } let mut remaining = cash; for (out, &movement) in applied.iter_mut().zip(movements) { let part = take(remaining, movement)?; *out = part; remaining = remaining.checked_sub(part)?; } (remaining == T::ZERO).then_some(()) } /// An entry's cash flows (rules.md B-14 rule 3): the net movement of its /// cash lines, applied to the sections moving against it in the order /// operating, investing, financing, each up to its own movement; what is /// left of a section's movement is the part no cash settled. `None` when /// the lines don't net to zero (never for a posted entry) or on overflow. /// It is [`entry_movements`] then [`flows_of`], so each is proved on its /// own (docs/verification.md, law 10). pub fn entry_flows(lines: &[(Flow, Side, T)]) -> Option> { let (cash, moved) = entry_movements(lines)?; flows_of(cash, moved) } /// An entry's net movement on its cash lines and on each section's, /// `(cash, [operating, investing, financing])`, debit-positive. `None` /// on overflow. pub fn entry_movements(lines: &[(Flow, Side, T)]) -> Option<(T, [T; 3])> { let mut cash = T::ZERO; let mut moved = [T::ZERO; 3]; for &(flow, side, amount) in lines { let delta = signed(side, amount)?; let slot = match flow { Flow::Cash => &mut cash, Flow::Operating => &mut moved[0], Flow::Investing => &mut moved[1], Flow::Financing => &mut moved[2], }; *slot = slot.checked_add(delta)?; } Some((cash, moved)) } /// The cash flows of an entry whose cash lines moved `cash` and whose /// sections moved `moved` ([`entry_flows`]). `None` when they don't net /// to zero, or on overflow. pub fn flows_of(cash: T, moved: [T; 3]) -> Option> { // Balanced lines only: every part below rests on it. let mut total = cash; for &m in &moved { total = total.checked_add(m)?; } if total != T::ZERO { return None; } let mut flows = [T::ZERO; 3]; apply_cash(cash, &moved, &mut flows)?; let mut noncash = [T::ZERO; 3]; for ((n, m), f) in noncash.iter_mut().zip(moved).zip(flows) { *n = m.checked_add(f)?; } Some(EntryFlows { cash, flows, noncash, }) } /// The credits and the debits among some movements (debit-positive): /// `(Σ max(-m, 0), Σ max(m, 0))`. fn credits_and_debits(movements: &[T]) -> Option<(T, T)> { let (mut credits, mut debits) = (T::ZERO, T::ZERO); for &m in movements { if m < T::ZERO { credits = credits.checked_sub(m)?; } else { debits = debits.checked_add(m)?; } } Some((credits, debits)) } /// Which way money a member or a lender handled for the company went /// (rules.md B-14 rule 11, D-42). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum OnBehalf { /// Paid for the company: sources credited (borrowed, contributed) /// against uses debited (spent). Paid, /// Received for the company: sources debited (repaid, distributed) /// against uses credited (collected). Received, } impl OnBehalf { /// The other way: what an entry's reversal does. pub fn mirror(self) -> Self { match self { OnBehalf::Paid => OnBehalf::Received, OnBehalf::Received => OnBehalf::Paid, } } } /// What a member or a lender paid, or received, for the company with no /// cash account moving: a constructive receipt and disbursement (rules.md /// B-14 rule 11, D-42). `sources` are what is left unsettled of the /// entry's kinds that can pay or receive for the company (a financing /// liability, a member's equity), `uses` of the kinds they can pay or /// receive for, each debit-positive. [`OnBehalf::Paid`]: `amount = /// min(source credits, use debits)`, cash in to the sources' credits and /// out of the uses' debits (borrowed or contributed, then spent); /// [`OnBehalf::Received`]: `amount = min(source debits, use credits)`, /// cash in to the uses' credits and out of the sources' debits (collected, /// then repaid or distributed). `from[i]` is source `i`'s cash (+ in) and /// `to[j]` use `j`'s, each applied in order by [`apply_cash`]. `None` when /// the slices differ in length, or on overflow. /// /// ```text /// GOAL: show money a member or lender handled for the company as the /// company's own, borrowed (or contributed) and spent, without /// inventing or losing a cent. /// SOURCE: rules.md B-14 rule 11 (D-42; ASC 230 is silent, PwC Viewpoint /// FSP 6.9.3.1, Deloitte DART Roadmap 7.2, BDO Blueprint 3.4). /// INVARIANTS: /// 1. The parts net to zero: Σ from + Σ to = 0, so the entry's cash /// flows still sum to its cash. /// 2. Each part settles its own movement, toward zero and never past it /// (zero, or the opposite sign and no larger). /// 3. The other way, minus the sources and minus the uses give minus /// the parts (a reversal mirrors its original). /// 4. As much is settled as can be: afterwards, paid, no source credit /// is left beside a use debit; received, no source debit beside a use /// credit. /// ``` pub fn constructive( way: OnBehalf, sources: &[T], uses: &[T], from: &mut [T], to: &mut [T], ) -> Option<()> { let (source_credits, source_debits) = credits_and_debits(sources)?; let (use_credits, use_debits) = credits_and_debits(uses)?; // The cash the sources take (+ in): what was paid, or minus what was // received; the uses take the opposite. let cash = match way { OnBehalf::Paid => source_credits.min(use_debits), OnBehalf::Received => source_debits.min(use_credits).checked_neg()?, }; apply_cash(cash, sources, from)?; apply_cash(cash.checked_neg()?, uses, to) } /// How much of an entry is an exchange inside investing and financing /// that no cash settled (rules.md B-14 rule 7, ASC 230-10-50-4: debt /// converted to equity, one asset traded for another). `debits` and /// `credits` are the net debits and net credits of the entry's investing /// and financing balance-sheet accounts (each account netted first; both /// non-negative); `inflow` and `outflow` are the cash those two sections /// took in and paid out (both non-negative; constructive cash included, /// rule 11). Cash in settles credits and cash out settles debits; what's /// left of the smaller side was exchanged: /// `max(0, min(debits - outflow, credits - inflow))`. pub fn exchanged(debits: T, credits: T, inflow: T, outflow: T) -> Option { let d = debits.checked_sub(outflow)?; let c = credits.checked_sub(inflow)?; Some(d.min(c).max(T::ZERO)) } /// Which way an entry moved value with its parties (rules.md B-35). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Direction { /// The company paid the party, or gave it value: a line on the /// party's own account would be a debit. Paid, /// The company received from the party: a line on its own account /// would be a credit. Received, /// Neither: nothing moved with the party yet (a bill, an invoice, a /// fee charged on its own card, a distribution still owed), or the /// entry moved nothing with anyone (a close, a reclassification). /// Counted in no total. Neither, } impl Direction { /// The other way: paid and received swap; neither stays neither. pub fn flip(self) -> Self { match self { Direction::Paid => Direction::Received, Direction::Received => Direction::Paid, Direction::Neither => Direction::Neither, } } /// The direction of a signed amount: paid above zero, received below, /// neither at zero. pub fn of(amount: T) -> Self { if amount > T::ZERO { Direction::Paid } else if amount < T::ZERO { Direction::Received } else { Direction::Neither } } /// Its wire name: `paid`, `received`, or `neither`. pub fn key(self) -> &'static str { match self { Direction::Paid => "paid", Direction::Received => "received", Direction::Neither => "neither", } } } /// What an entry's parties split, and what of it counts in their totals /// (rules.md B-35). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct PartyTarget { /// The whole, signed as a line on the parties' own account would be /// (debit-positive): positive, the company paid or gave; negative, it /// received. pub amount: T, /// Which way value moved. pub direction: Direction, /// The part of `amount` that moved as money (of a member's /// contribution, what the K-1 worksheet counts as cash; of a /// distribution, the money it paid): counted on both bases. pub cash: T, /// The part of `amount` that moved as property: counted on basis /// "all" only. pub in_kind: T, } /// The magnitude of `a`, or `None` past the type's range. fn magnitude(a: T) -> Option { if a < T::ZERO { a.checked_neg() } else { Some(a) } } /// The magnitude `m` with the sign of `like`, never a negative zero (a /// zero `m` is the plain zero, whatever sign arithmetic left on it). fn with_sign(like: T, m: T) -> Option { if m == T::ZERO { Some(T::ZERO) } else if like < T::ZERO { m.checked_neg() } else { Some(m) } } /// The smaller of `a` and `b`. fn least(a: T, b: T) -> T { if a < b { a } else { b } } /// What an entry's parties split and what of it counts (rules.md B-35 /// item 3, every case but a close and a reversal, which the caller /// handles: a close is its parties' own-account movement with direction /// neither, a reversal is [`reverse_target`] of its original's). /// /// ```text /// GOAL: one signed whole for an entry's parties, debit-positive as a line /// on their own account, with its direction and what of it moved as /// money or as property, so a property contribution and its /// reversal never read alike, and a promise is never counted as if /// it were paid. /// SOURCE: rules.md B-35 (B-2 signs; DECISION, owner, 2026-10-20: a /// member's entry counts what moved their capital, money is cash, /// cards, the members' financing and loans, and a liability credit /// never counts as paying that liability's own creditor; Helvering /// v. Price: a promise to pay is not payment). /// INPUTS (each signed debit-positive over the entry's lines): /// capital: Some((e, property)) when every party named is a member: e /// the movement of equity other than retained earnings; property what /// property accounts took in (their debits, when e < 0) or gave up /// (their credits, when e > 0). /// net: the money paid (+) or received (−) (parties::entry_flow, a /// named party's own liabilities not counted as money). /// own: the movement of the parties' own accounts (payables, /// receivables, liabilities owed to a party named). /// debits: the entry's total debits. /// INVARIANTS: /// 1. capital with e < 0 (a contribution): amount = e; |in_kind| = /// min(|e|, property⁺); cash the rest (what the K-1 worksheet counts /// as cash: money, or what the member paid for the company). /// 2. capital with e > 0 (a distribution): amount = e; cash = min(e, /// net⁺), the money it paid; in_kind = min(e − cash, property⁺), the /// property it gave; the rest (credited to a liability that isn't /// money for the entry: a distribution declared, still owed) counts /// as nothing until it is paid. /// 3. In 1 and 2, direction is the sign of e, or neither when nothing /// counts. /// 4. Else net ≠ 0: amount = cash = net, in_kind = 0, direction its /// sign. /// 5. Else amount = own when it isn't zero, else debits; neither. /// 6. cash and in_kind each have amount's sign or are zero (never a /// negative zero), |cash| + |in_kind| ≤ |amount|, and direction is /// neither exactly when both are zero. /// 7. Odd, but for the debits fallback and a member's entry whose cash /// isn't exactly its money (an expense booked straight to capital, a /// debt converted, a distribution still owed): negating e, net and /// own (property comes from the other side of the lines, so it keeps /// its size) gives reverse_target of the result. /// ``` /// /// `None` past the type's range. pub fn party_target( capital: Option<(T, T)>, net: T, own: T, debits: T, ) -> Option> { if let Some((e, property)) = capital && e != T::ZERO { let whole = magnitude(e)?; let property = if property > T::ZERO { property } else { T::ZERO }; let (cash, in_kind) = if e < T::ZERO { let in_kind = least(property, whole); (whole.checked_sub(in_kind)?, in_kind) } else { let paid = if net > T::ZERO { least(net, whole) } else { T::ZERO }; (paid, least(property, whole.checked_sub(paid)?)) }; let counted = cash.checked_add(in_kind)?; return Some(PartyTarget { amount: e, direction: if counted == T::ZERO { Direction::Neither } else { Direction::of(e) }, cash: with_sign(e, cash)?, in_kind: with_sign(e, in_kind)?, }); } if net != T::ZERO { return Some(PartyTarget { amount: net, direction: Direction::of(net), cash: net, in_kind: T::ZERO, }); } Some(PartyTarget { amount: if own != T::ZERO { own } else { debits }, direction: Direction::Neither, cash: T::ZERO, in_kind: T::ZERO, }) } /// A reversal's target from its original's (rules.md B-35 item 3; B-9: a /// correction is the opposite entry): every amount negated (never to a /// negative zero), paid and received swapped. An involution. `None` past /// the type's range. pub fn reverse_target(t: PartyTarget) -> Option> { let neg = |a: T| { if a == T::ZERO { Some(T::ZERO) } else { a.checked_neg() } }; Some(PartyTarget { amount: neg(t.amount)?, direction: t.direction.flip(), cash: neg(t.cash)?, in_kind: neg(t.in_kind)?, }) } // --------------------------------------------------------------------------- // Proration by days // --------------------------------------------------------------------------- /// Exact integer arithmetic the proration by days needs: money as a whole /// number of its smallest step (`i128` in the engine; `i64` and, for the /// harnesses whose properties relate a product to a quotient, `i32` under /// Kani). Kept apart from [`Amount`]: proration multiplies and divides, /// which no journal line ever does. pub trait Units: Copy + Ord { /// Zero. const ZERO: Self; /// One. const ONE: Self; /// `n`, exactly, or `None` when it doesn't fit. fn from_u32(n: u32) -> Option; /// `self + rhs`, or `None` on overflow. fn checked_add(self, rhs: Self) -> Option; /// `self - rhs`, or `None` on overflow. fn checked_sub(self, rhs: Self) -> Option; /// `self * rhs`, or `None` on overflow. fn checked_mul(self, rhs: Self) -> Option; /// `(self / rhs, self % rhs)` for `self >= 0` and `rhs > 0`; `None` /// otherwise. fn checked_div_rem(self, rhs: Self) -> Option<(Self, Self)>; } impl Units for i32 { const ZERO: Self = 0; const ONE: Self = 1; fn from_u32(n: u32) -> Option { i32::try_from(n).ok() } fn checked_add(self, rhs: Self) -> Option { i32::checked_add(self, rhs) } fn checked_sub(self, rhs: Self) -> Option { i32::checked_sub(self, rhs) } fn checked_mul(self, rhs: Self) -> Option { i32::checked_mul(self, rhs) } fn checked_div_rem(self, rhs: Self) -> Option<(Self, Self)> { if self < 0 || rhs <= 0 { return None; } Some((self / rhs, self % rhs)) } } impl Units for i64 { const ZERO: Self = 0; const ONE: Self = 1; fn from_u32(n: u32) -> Option { Some(i64::from(n)) } fn checked_add(self, rhs: Self) -> Option { i64::checked_add(self, rhs) } fn checked_sub(self, rhs: Self) -> Option { i64::checked_sub(self, rhs) } fn checked_mul(self, rhs: Self) -> Option { i64::checked_mul(self, rhs) } fn checked_div_rem(self, rhs: Self) -> Option<(Self, Self)> { if self < 0 || rhs <= 0 { return None; } Some((self / rhs, self % rhs)) } } impl Units for i128 { const ZERO: Self = 0; const ONE: Self = 1; fn from_u32(n: u32) -> Option { Some(i128::from(n)) } fn checked_add(self, rhs: Self) -> Option { i128::checked_add(self, rhs) } fn checked_sub(self, rhs: Self) -> Option { i128::checked_sub(self, rhs) } fn checked_mul(self, rhs: Self) -> Option { i128::checked_mul(self, rhs) } fn checked_div_rem(self, rhs: Self) -> Option<(Self, Self)> { if self < 0 || rhs <= 0 { return None; } Some((self / rhs, self % rhs)) } } /// lcm(28, 29, 30, 31): every month's length divides it, so a day of any /// month is a whole number of `MONTH_L`ths of a month, and proration needs /// no fractions. pub const MONTH_L: u32 = 377_580; /// One calendar month's share of a span of days: `days` of the month's /// `month_days`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct MonthPart { /// Days of the span in the month, 1 to `month_days`. pub days: u32, /// Days in the month, 28 to 31. pub month_days: u32, } impl MonthPart { /// Whether it is a share a calendar month can have: 28 to 31 days in /// the month, 1 to all of them in the span. pub fn is_valid(self) -> bool { (28..=31).contains(&self.month_days) && (1..=self.month_days).contains(&self.days) } } /// A part's weight in `MONTH_L`ths of a month: `days × (MONTH_L / /// month_days)`, exactly. `None` for a part no month has. pub fn part_weight(p: MonthPart) -> Option { if !p.is_valid() { return None; } T::from_u32(p.days)?.checked_mul(T::from_u32(MONTH_L / p.month_days)?) } /// `num / den` rounded half away from zero to a multiple of `unit` (`num` /// not negative; `den` and `unit` positive). fn round_to_unit(num: T, den: T, unit: T) -> Option { let step = den.checked_mul(unit)?; let (q, r) = num.checked_div_rem(step)?; // Up when the remainder is half a step or more: compared as r >= // step - r, so nothing is doubled and nothing can overflow. let q = if r >= step.checked_sub(r)? { q.checked_add(T::ONE)? } else { q }; q.checked_mul(unit) } /// The exact cumulative charge of `total` (in units) spread over `months` /// months by day, through `k` days of `part`, after earlier parts weighing /// `before` `MONTH_L`ths; `last` marks the span's last part. Rounded half /// away from zero (half up: nothing here is negative) to a multiple of /// `unit`, or `total` itself when that rounds above it (a total that isn't /// a whole number of units); and the last part's last day is `total`. /// /// ```text /// GOAL: how much of a total a span of days has used up by a given day, /// each day of a month bearing the same share, the whole total /// reached on the span's last day and never before. /// SOURCE: docs/spec/rules.md B-15 (straight line, partial months by days; /// ASC 360-10-35-4: "systematic and rational", as equitably as /// possible to the periods served), B-16 (daily schedules, D-11), /// B-21 (exact totals). /// INVARIANTS (W = months × MONTH_L, m = part.month_days, d = part.days): /// 1. Not the last part: round(total × (before + k × MONTH_L / m) / W): /// the monthly rate total/months times the days held over the days /// in each month. /// 2. The last part: round(total × (before × d + (W − before) × k) / /// (W × d)): what the earlier parts leave of the total, C = total × /// before / W, plus the remainder R = total − C spread evenly over /// its d days. It is never "plugged": through an earlier day of the /// last month it is prorated by days. /// 3. It is non-decreasing in (part, k), never above total, continuous /// at part boundaries ((i, days_i) equals (i + 1, 0)), and exactly /// total at (last, d). /// SCENARIOS: /// 1. IF total is negative, months is 0, unit isn't positive, the part /// is no month's (see MonthPart::is_valid), k > d, before is negative /// or above W, a part but the last is charged through a day past W /// (before + k × MONTH_L / m > W; prorate_by_days charges every part /// but the last through its last day, so it refuses a part ending past /// W), or any product overflows: /// - SHOULD return None /// - NEVER panic /// BECAUSE a figure the books can't compute is refused (rules.md /// B-28), and symbolic parts are not a calendar. /// 2. IF 12,000.00 over 12 months from Jan 1, through Dec 15 (the last /// part, 31 days, before = 11 months): /// - SHOULD be 11,000.00 + 1,000.00 × 15/31 = 11,483.87 /// - NEVER 12,000.00 (the old plug) /// BECAUSE the final month is charged by its days like any other. /// ``` pub fn prorated_through( total: T, months: u32, before: T, part: MonthPart, k: u32, last: bool, unit: T, ) -> Option { if total < T::ZERO || months == 0 || unit <= T::ZERO || !part.is_valid() || k > part.days || before < T::ZERO { return None; } let whole = T::from_u32(months)?.checked_mul(T::from_u32(MONTH_L)?)?; if before > whole { return None; } let charged = if last { if k == part.days { // Exactly the total, even one that isn't a whole number of // units (an asset an earlier version took in tenths of a cent). return Some(total); } let d = T::from_u32(part.days)?; let left = whole.checked_sub(before)?; let elapsed = before .checked_mul(d)? .checked_add(left.checked_mul(T::from_u32(k)?)?)?; round_to_unit(total.checked_mul(elapsed)?, whole.checked_mul(d)?, unit)? } else { let rate = T::from_u32(MONTH_L / part.month_days)?; let elapsed = before.checked_add(T::from_u32(k)?.checked_mul(rate)?)?; if elapsed > whole { return None; } round_to_unit(total.checked_mul(elapsed)?, whole, unit)? }; // A total that isn't a whole number of units can round above itself // on the day before the last; the cap keeps every part non-negative. Some(charged.min(total)) } /// Split `total` (in units) over a span of `months` months cut into /// calendar-month `parts`: `out[i]` is the cumulative charge through the /// end of part `i` ([`prorated_through`]) less the one before it, and the /// last part is `total` less all the others. `out` must be as long as /// `parts`; on `None` its contents mean nothing. /// /// ```text /// GOAL: one month's posting of a span spread by day (depreciation, /// daily prepaid and deferred-revenue schedules). /// SOURCE: docs/spec/rules.md B-15, B-16 (D-11), B-21; plan/served.md P3 /// defect 9 (one shared prorate_by_days, Kani: parts sum to the /// total, each is days × rate within one unit, the last absorbs /// the remainder). /// INVARIANTS: /// 1. The parts sum to total exactly. /// 2. Every part is >= 0. /// 3. Each part but the last is within one unit of total × days_i / /// (months × month_days_i). /// 4. The last part is total − Σ others. /// SCENARIOS: /// 1. IF there are no parts, out isn't as long as parts, or prorated_ /// through refuses a part (the weights of all but the last above W, /// among others): SHOULD return None, never panic. /// 2. IF 1,200.00 over 12 months from Mar 15 (parts 17/31, eleven full /// months, then 14 days): /// - SHOULD be 54.84, eleven × 100.00, then 45.16 /// BECAUSE each day bears the same share, and the last part takes /// what the others leave. /// ``` /// /// Allocation-free and O(n): a loop over fixed slices, so the model /// checker can prove it outright. pub fn prorate_by_days( total: T, months: u32, parts: &[MonthPart], unit: T, out: &mut [T], ) -> Option<()> { if out.len() != parts.len() { return None; } let (&last, rest) = parts.split_last()?; let mut before = T::ZERO; let mut charged = T::ZERO; for (o, &p) in out.iter_mut().zip(rest) { let through = prorated_through(total, months, before, p, p.days, false, unit)?; *o = through.checked_sub(charged)?; charged = through; before = before.checked_add(part_weight(p)?)?; } // The last part is checked too (its weight before it within the span), // and its whole is the total, so it takes the remainder. let through = prorated_through(total, months, before, last, last.days, true, unit)?; *out.last_mut()? = through.checked_sub(charged)?; Some(()) } // --------------------------------------------------------------------------- // Catching up and truing up // --------------------------------------------------------------------------- /// What a run catches up: the largest amount, at least zero, that can be /// added to what the books carry at every month end from the run's on /// without carrying more than the schedule says there: /// `max(0, min_i(scheduled[i] − posted[i]))`, where `scheduled[i]` and /// `posted[i]` are cumulative at successive month ends starting with the /// run's own. `None` when the slices differ in length or are empty, or on /// overflow. /// /// ```text /// GOAL: a skipped month is caught up in the month the run is made, and a /// run never overtakes the schedule at a month end already posted. /// SOURCE: plan/served.md P3 "Settled in design" (2026-10-20): a month of /// depreciation skipped is caught up in the month the run is made /// (an immaterial miss; a material one is restated, ASC 250); /// docs/spec/rules.md B-15. /// INVARIANTS: /// 1. The result is >= 0. /// 2. Whenever every posted[i] <= scheduled[i], posted[i] + result <= /// scheduled[i] for every i: the run never carries the books past the /// schedule at any month end it reaches. /// 3. The result is 0 or some scheduled[i] − posted[i]: with one month /// (no run after this one), it is everything the schedule says is due /// through it. /// ``` pub fn catch_up(scheduled: &[T], posted: &[T]) -> Option { if scheduled.len() != posted.len() { return None; } let mut least: Option = None; for (&s, &p) in scheduled.iter().zip(posted) { let gap = s.checked_sub(p)?; least = Some(match least { Some(l) if l <= gap => l, _ => gap, }); } let least = least?; Some(if least > T::ZERO { least } else { T::ZERO }) } /// [`catch_up`] of every suffix at once: `out[i]` is `catch_up(&scheduled[i..], /// &posted[i..])`, what a run at each month end posts. One pass from the /// end, O(n), so a viewer can say of every month of a long life whether /// anything is due for it. `None` when the slices differ in length, or on /// overflow. pub fn catch_ups(scheduled: &[T], posted: &[T], out: &mut [T]) -> Option<()> { if scheduled.len() != posted.len() || out.len() != scheduled.len() { return None; } let mut least: Option = None; for ((&s, &p), o) in scheduled.iter().zip(posted).zip(out.iter_mut()).rev() { let gap = s.checked_sub(p)?; let l = match least { Some(l) if l <= gap => l, _ => gap, }; least = Some(l); *o = if l > T::ZERO { l } else { T::ZERO }; } Some(()) } /// Bring what the books carry (`posted`) to `target`: `(post, take_back)`, /// both >= 0, one of them zero, with `posted + post − take_back = target`. /// This is [`columns`] of `target − posted`: raising the books by `post` /// and lowering them by `take_back` are a debit and a credit of the gap. /// /// ```text /// GOAL: a disposal clears exactly what the schedule says was used up /// through its day, whatever the runs posted. /// SOURCE: docs/spec/rules.md B-15 (a disposed asset depreciates through /// its disposal day); Ledger::dispose_asset INVARIANT 1 (gain = /// proceeds − book value at the disposal date). /// ``` pub fn true_up(target: T, posted: T) -> Option<(T, T)> { columns(target.checked_sub(posted)?) } /// An asset's three basis figures ([`basis_figures`]). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BasisFigures { /// The partnership's basis in contributed property, IRC §723: the /// contributor's adjusted basis. `None` for a purchase. pub tax_basis_723: Option, /// The basis tax depreciation starts from. pub depreciable_basis: T, /// Fair market value less the §723 basis at contribution, IRC §704(c): /// positive a built-in gain, negative a built-in loss. `None` for a /// purchase (not property contributed). pub built_in_gain_704c: Option, } /// An asset's basis figures from its rule and facts: `cost` the book cost /// (a purchase's basis), `adjusted` the contributing member's adjusted /// basis and `fmv` the fair market value at contribution (both ignored for /// a purchase). /// /// ```text /// GOAL: the three tax figures of an asset, each named for what it is, /// from one function the register, the tools and the viewer share. /// SOURCE: docs/spec/rules.md B-25: IRC §1012(a) (cost); IRC §723 (the /// contributor's adjusted basis carries over); Treas. Reg. /// §1.168(i)-4(b)(1) (property converted from personal use: "the /// lesser of its fair market value or its adjusted depreciable /// basis"; DECISION D-41, owner 2026-10-20); Treas. Reg. /// §1.704-3(a)(3)(ii) (built-in gain: "the excess of the /// property's book value over the contributing partner's adjusted /// tax basis upon contribution", book value being fair market /// value at contribution, §1.704-3(a)(3)(i)). /// INVARIANTS: /// 1. Purchase: depreciable = cost; no §723 basis, no built-in gain. /// 2. Contribution: tax_basis_723 = adjusted; built_in_gain_704c = /// fmv − adjusted, negative exactly when adjusted > fmv (a built-in /// loss, IRC §704(c)(1)(C)). /// 3. Carryover723: depreciable = adjusted. /// 4. PersonalUseConversion: depreciable = min(fmv, adjusted), at most /// each of them and equal to one. /// 5. NEVER wraps: None when a figure can't be represented. /// ``` pub fn basis_figures( rule: BasisRule, cost: T, adjusted: T, fmv: T, ) -> Option> { Some(match rule { BasisRule::Purchase => BasisFigures { tax_basis_723: None, depreciable_basis: cost, built_in_gain_704c: None, }, BasisRule::Carryover723 => BasisFigures { tax_basis_723: Some(adjusted), depreciable_basis: adjusted, built_in_gain_704c: Some(fmv.checked_sub(adjusted)?), }, BasisRule::PersonalUseConversion => BasisFigures { tax_basis_723: Some(adjusted), depreciable_basis: if fmv < adjusted { fmv } else { adjusted }, built_in_gain_704c: Some(fmv.checked_sub(adjusted)?), }, }) } /// A project's unrecovered cost from its tagged revenue and expenses to /// date: what it would take to be repaid in full (rules.md B-37 item 4). /// /// ```text /// GOAL: the "if repaid" figure of work done on spec, from one function /// the tools, the viewer and bookspec share. /// SOURCE: docs/spec/rules.md B-37 item 4 (ASC 450-30-25-1: a hoped-for /// payment is a gain contingency, never recorded before it is /// received; so this is a figure, never a receivable). /// INVARIANTS: /// 1. Never below zero. /// 2. expenses - revenue when expenses >= revenue; zero otherwise. /// 3. Never above max(expenses, 0) when revenue >= 0 (expenses can be /// negative: a reimbursement credited to the expense above its cost). /// 4. NEVER wraps: None when the difference can't be represented. /// ``` pub fn unrecovered_cost(revenue: T, expenses: T) -> Option { let gap = expenses.checked_sub(revenue)?; Some(if gap > T::ZERO { gap } else { T::ZERO }) } #[cfg(test)] mod tests { use super::*; /// rules.md B-14 rule 3, by hand. Equipment 1,000 for 300 cash and a /// 700 note: investing takes the 300, the note's 700 and the rest of /// the equipment's are the noncash part. #[test] fn part_cash_part_noncash_takes_only_the_cash() { use Flow::*; let f = entry_flows(&[ (Investing, Side::Debit, 1000i64), (Cash, Side::Credit, 300), (Financing, Side::Credit, 700), ]) .unwrap(); assert_eq!(f.cash, -300); assert_eq!(f.flows, [0, -300, 0]); assert_eq!(f.noncash, [0, 700, -700]); // A fee withheld from a loan: Dr cash 9,900, Dr fee 100 / Cr loan // 10,000. Operating moves with the cash (a debit), so takes none. let f = entry_flows(&[ (Cash, Side::Debit, 9900i64), (Operating, Side::Debit, 100), (Financing, Side::Credit, 10000), ]) .unwrap(); assert_eq!(f.flows, [0, 0, 9900]); assert_eq!(f.noncash, [100, 0, -100]); } /// Lines that don't net to zero are refused (review item 12): no part /// is made up for them. #[test] fn unbalanced_lines_have_no_flows() { use Flow::*; assert_eq!( entry_flows(&[(Operating, Side::Debit, 5i64), (Investing, Side::Debit, 5)]), None ); assert_eq!( entry_flows(&[(Cash, Side::Debit, 5i64), (Investing, Side::Credit, 4)]), None ); // Too little against the cash: nothing is invented. let mut out = [0i64; 2]; assert_eq!(apply_cash(10, &[-3, 4], &mut out), None); assert_eq!(apply_cash(10, &[-3], &mut out), None); } /// The exchange measure (rules.md B-14 rule 7) on the review's /// hand-checked cases. #[test] fn exchanged_matches_the_hand_checked_cases() { // Debt to equity, 500. assert_eq!(exchanged(500i64, 500, 0, 0), Some(500)); // Part-cash purchase with the seller's note: D 1,000, C 700, cash // out 300. assert_eq!(exchanged(1000i64, 700, 0, 300), Some(700)); // Note received for equipment: D 700, C 1,000, cash in 300. assert_eq!(exchanged(700i64, 1000, 300, 0), Some(700)); // Trade-in with 1,500 boot: D 2,000, C 1,000, cash out 1,500. assert_eq!(exchanged(2000i64, 1000, 0, 1500), Some(500)); // A cash sale: nothing debited, cash in 500. assert_eq!(exchanged(0i64, 1000, 500, 0), Some(0)); // A cash purchase: cash out covers every debit. assert_eq!(exchanged(1000i64, 0, 0, 1000), Some(0)); // Equipment a lender paid for (rule 11): 1,000 borrowed in, 1,000 // spent out, nothing exchanged. assert_eq!(exchanged(1000i64, 1000, 1000, 1000), Some(0)); } /// rules.md B-14 rule 11 (D-42), by hand. #[test] fn constructive_pays_and_receives_for_the_company() { use OnBehalf::{Paid, Received}; // A member pays a 59 expense (Dr expense / Cr due to members): the // source's 59 credit pays the use's 59 debit. let (mut from, mut to) = ([0i64; 1], [0i64; 1]); constructive(Paid, &[-59], &[59], &mut from, &mut to).unwrap(); assert_eq!((from, to), ([59], [-59])); // Its reversal, the other way, mirrors it. constructive(Received, &[59], &[-59], &mut from, &mut to).unwrap(); assert_eq!((from, to), ([-59], [59])); // The reversal's shape read as a payment settles nothing. constructive(Paid, &[59], &[-59], &mut from, &mut to).unwrap(); assert_eq!((from, to), ([0], [0])); // A 100 credit from the source against 60 of uses (the other 40 // against something else): only 60 is paid; uses take in order. let (mut from, mut to) = ([0i64; 2], [0i64; 2]); constructive(Paid, &[-100, 0], &[25, 35], &mut from, &mut to).unwrap(); assert_eq!((from, to), ([60, 0], [-25, -35])); // Two sources, one use smaller than both: the first source pays // first. constructive(Paid, &[-30, -30], &[40, 0], &mut from, &mut to).unwrap(); assert_eq!((from, to), ([30, 10], [-40, 0])); // Each way settles only its own side. constructive(Paid, &[-50, 20], &[50, -20], &mut from, &mut to).unwrap(); assert_eq!((from, to), ([50, 0], [-50, 0])); constructive(Received, &[-50, 20], &[50, -20], &mut from, &mut to).unwrap(); assert_eq!((from, to), ([0, -20], [0, 20])); // Nothing against: nothing moves. constructive(Paid, &[-50, 0], &[-10, 0], &mut from, &mut to).unwrap(); assert_eq!((from, to), ([0, 0], [0, 0])); // Mismatched lengths are refused. let one = [-1i64]; assert_eq!( constructive(Paid, &one, &[1], &mut [0i64; 2], &mut [0]), None ); assert_eq!( constructive(Paid, &one, &[1], &mut [0i64], &mut [0; 2]), None ); assert_eq!(Paid.mirror(), Received); assert_eq!(Received.mirror(), Paid); } fn part(days: u32, month_days: u32) -> MonthPart { MonthPart { days, month_days } } /// Every month length divides MONTH_L, and it is the least such. #[test] fn month_l_is_the_lcm_of_month_lengths() { for m in 28..=31 { assert_eq!(MONTH_L % m, 0, "{m}"); } for smaller in 1..MONTH_L { assert!((28..=31).any(|m| smaller % m != 0), "{smaller}"); } assert_eq!(part_weight::(part(31, 31)), Some(i64::from(MONTH_L))); assert_eq!(part_weight::(part(1, 28)), Some(13_485)); assert_eq!(part_weight::(part(0, 30)), None); assert_eq!(part_weight::(part(32, 31)), None); assert_eq!(part_weight::(part(27, 27)), None); } /// Spec scenario 2 of prorated_through, and the brief's 1,200.00 from /// March 15 (prorate_by_days scenario 2), in cents. #[test] fn the_final_month_is_prorated_and_a_mid_month_span_splits_by_day() { let l = i128::from(MONTH_L); let dec15 = prorated_through(1_200_000i128, 12, 11 * l, part(31, 31), 15, true, 1); assert_eq!(dec15, Some(1_148_387)); assert_eq!( prorated_through(1_200_000i128, 12, 11 * l, part(31, 31), 31, true, 1), Some(1_200_000) ); let mut parts = vec![part(17, 31)]; parts.extend([30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 28].map(|m| part(m, m))); parts.push(part(14, 31)); let mut out = vec![0i128; parts.len()]; prorate_by_days(120_000i128, 12, &parts, 1, &mut out).unwrap(); let mut want = vec![5_484i128]; want.extend([10_000; 11]); want.push(4_516); assert_eq!(out, want); } /// Half a unit rounds away from zero (up, for the non-negative amounts /// here): 0.01 over 2 months from the 1st gives 0.005 after one month. #[test] fn half_a_unit_rounds_up() { let mut out = [0i64; 2]; prorate_by_days(1, 2, &[part(31, 31), part(28, 28)], 1, &mut out).unwrap(); assert_eq!(out, [1, 0]); // Just under half a unit stays down: 0.01 over 3 months. let mut out = [0i64; 3]; prorate_by_days( 1, 3, &[part(31, 31), part(28, 28), part(31, 31)], 1, &mut out, ) .unwrap(); assert_eq!(out, [0, 1, 0]); } /// R4(a): a total that isn't a whole number of units (100.006 in /// thousandths, rounded to cents) never makes the last part negative. #[test] fn a_total_finer_than_its_unit_never_gives_a_negative_part() { let l = i64::from(MONTH_L); let mut out = [0i64; 2]; prorate_by_days(100_006, 2, &[part(31, 31), part(28, 28)], 10, &mut out).unwrap(); assert_eq!(out, [50_000, 50_006]); // 30 of 31 days of a one-month span: 100.006 × 30/31 = 96.780 // exactly, already whole cents. assert_eq!( prorated_through(100_006i64, 1, 0, part(31, 31), 30, true, 10), Some(96_780) ); // One day short of a 4,000-month span: 100.006 less 0.0008 is // 100.0052, which rounds to 100.01, past the total: capped there, // so the day left gets 0, never -0.004. let w = 4_000 * l; assert_eq!( prorated_through(100_006i64, 4_000, w - l, part(30, 31), 30, false, 10), Some(100_006) ); } /// Spec scenario 1 of prorated_through (R4(b), R4(c)): malformed input /// is refused, never a panic. #[test] fn malformed_proration_is_refused() { let l = i64::from(MONTH_L); let p = part(10, 30); assert_eq!(prorated_through(-1i64, 1, 0, p, 1, false, 1), None); assert_eq!(prorated_through(1i64, 0, 0, p, 1, false, 1), None); assert_eq!(prorated_through(1i64, 1, 0, p, 1, false, 0), None); assert_eq!(prorated_through(1i64, 1, 0, p, 11, false, 1), None); assert_eq!(prorated_through(1i64, 1, -1, p, 1, false, 1), None); assert_eq!(prorated_through(1i64, 1, l + 1, p, 1, true, 1), None); assert_eq!(prorated_through(1i64, 1, 0, part(0, 30), 0, false, 1), None); assert_eq!( prorated_through(1i64, 1, 0, part(31, 30), 1, false, 1), None ); // A part but the last that ends past the span. assert_eq!(prorated_through(1i64, 1, l, part(1, 31), 1, false, 1), None); let mut out = [0i64; 3]; assert_eq!( prorate_by_days( 100, 1, &[part(31, 31), part(31, 31), part(5, 30)], 1, &mut out ), None ); assert_eq!(prorate_by_days(100i64, 1, &[], 1, &mut []), None); assert_eq!( prorate_by_days(100i64, 1, &[part(31, 31)], 1, &mut []), None ); assert_eq!( prorated_through(i64::MAX, 600, 0, part(31, 31), 31, false, 1), None ); } /// prorated_through over every span of 1 to 3 months starting on any /// day of a common year and a leap year, walked day by day, for a /// spread of totals and both units: it never falls (within a month and /// across a month's end: the charge through a month's last day is the /// next month's through none of its days), never exceeds the total, and /// is the total on the last day. Exhaustive over those spans; the /// relation between two divisions is too costly for Kani's solver /// (docs/verification.md), and Lean proves the law for the model. /// Not under Miri, which looks for undefined behavior: this is safe /// integer arithmetic the other kernel tests already take through it, /// and walked exhaustively under the interpreter it held `just miri` /// for over ten minutes. #[test] #[cfg_attr( miri, ignore = "exhaustive arithmetic: the other kernel tests cover it under Miri" )] fn the_charge_never_falls_and_is_continuous_over_real_calendars() { use chrono::NaiveDate; let mut start = NaiveDate::from_ymd_opt(2027, 1, 1).unwrap(); let stop = NaiveDate::from_ymd_opt(2029, 1, 1).unwrap(); let mut spans = 0; while start < stop { for months in 1..=3u32 { let end = start .checked_add_months(chrono::Months::new(months)) .unwrap(); let parts: Vec<_> = crate::calendar::month_parts(start, end).collect(); for total in [0i64, 1, 3, 7, 10, 999, 100_005, 65_536] { for unit in [1i64, 10] { let mut before = 0i64; let mut last_charge = 0i64; for p in &parts { for k in 0..=p.part.days { let c = prorated_through( total, months, before, p.part, k, p.last, unit, ) .unwrap(); assert!( c >= last_charge && c <= total, "{start} + {months}: {c} after {last_charge}" ); last_charge = c; } before += part_weight::(p.part).unwrap(); } assert_eq!(last_charge, total, "{start} + {months}"); } } spans += 1; } start = start.succ_opt().unwrap(); } assert_eq!(spans, 731 * 3); } /// catch_up: everything due through the run's month when no run comes /// after it; never past a later month end already posted. #[test] fn a_catch_up_never_overtakes_a_posted_month_end() { // Jan run, Feb skipped, a Feb run with nothing after it: Feb's. assert_eq!(catch_up(&[200i64], &[100]), Some(100)); // March already caught February up: a Feb run posts nothing. assert_eq!(catch_up(&[200i64, 300], &[100, 300]), Some(0)); // A run carried past the schedule (an old tie cent): nothing. assert_eq!(catch_up(&[200i64], &[201]), Some(0)); // January reversed after March ran: January posts it back. assert_eq!(catch_up(&[100i64, 200, 300], &[0, 100, 200]), Some(100)); assert_eq!(catch_up::(&[], &[]), None); assert_eq!(catch_up(&[1i64], &[1, 2]), None); } /// catch_ups is catch_up of every suffix: positive and distinct ones /// (a catch_ups that wrote zeros would fail), and a month carried past /// the schedule (zero from it back). #[test] fn catch_ups_is_every_suffixs_catch_up() { let (s, p) = ([100i64, 200, 300, 400], [0i64, 100, 150, 250]); let mut out = [0i64; 4]; catch_ups(&s, &p, &mut out).unwrap(); for i in 0..4 { assert_eq!(Some(out[i]), catch_up(&s[i..], &p[i..]), "{i}"); } assert_eq!(out, [100, 100, 150, 150]); let (s, p) = ([100i64, 200, 300, 400], [0i64, 300, 200, 400]); catch_ups(&s, &p, &mut out).unwrap(); for i in 0..4 { assert_eq!(Some(out[i]), catch_up(&s[i..], &p[i..]), "{i}"); } assert_eq!(out, [0, 0, 0, 0]); let mut none: [i64; 0] = []; assert_eq!(catch_ups::(&[], &[], &mut none), Some(())); assert_eq!(catch_ups(&s, &p, &mut [0i64; 3]), None); } /// true_up is the gap's columns. #[test] fn a_true_up_reaches_the_target() { assert_eq!(true_up(5_500i64, 5_000), Some((500, 0))); assert_eq!(true_up(5_500i64, 6_000), Some((0, 500))); assert_eq!(true_up(5_500i64, 5_500), Some((0, 0))); } /// basis_figures INVARIANTS 1-4, by hand: a 1,440.00 laptop the member /// holds at 1,688.40 (personal use): §723 1,688.40, depreciable /// 1,440.00, built-in loss 248.40; as business property, depreciable /// 1,688.40; a purchase at 900.00: depreciable 900.00 and nothing else. #[test] fn basis_figures_by_rule() { let (fmv, adj) = (144_000i64, 168_840i64); let p = basis_figures(BasisRule::PersonalUseConversion, fmv, adj, fmv).unwrap(); assert_eq!( p, BasisFigures { tax_basis_723: Some(adj), depreciable_basis: fmv, built_in_gain_704c: Some(-24_840), } ); let c = basis_figures(BasisRule::Carryover723, fmv, adj, fmv).unwrap(); assert_eq!(c.depreciable_basis, adj); assert_eq!(c.built_in_gain_704c, Some(-24_840)); // Value above basis: depreciable is the basis, a built-in gain. let g = basis_figures(BasisRule::PersonalUseConversion, 1000, 400, 1000).unwrap(); assert_eq!( (g.depreciable_basis, g.built_in_gain_704c), (400, Some(600)) ); let b = basis_figures(BasisRule::Purchase, 90_000i64, 7, 8).unwrap(); assert_eq!( b, BasisFigures { tax_basis_723: None, depreciable_basis: 90_000, built_in_gain_704c: None, } ); assert_eq!( basis_figures(BasisRule::Carryover723, 0, i64::MAX, i64::MIN), None ); } #[test] fn max_line_amount_is_one_quadrillion() { assert_eq!(MAX_LINE_AMOUNT, Decimal::from(1_000_000_000_000_000u64)); assert!(MAX_LINE_AMOUNT.in_line_range()); assert!(!(MAX_LINE_AMOUNT + Decimal::ONE).in_line_range()); assert!("0.0000000001".parse::().unwrap().in_line_range()); assert!(!"0.00000000001".parse::().unwrap().in_line_range()); // Trailing zeros are not precision. assert!("1.000000000000".parse::().unwrap().in_line_range()); } /// Regression (Kani `apportion_is_exact`, 2026-10-13): a shortfall of /// two units and a bit, with one part, was accepted and handed almost /// two units to that part. Spec scenario 1: more than one unit short /// per part is refused. #[test] fn apportion_refuses_a_shortfall_of_a_unit_per_part_or_more() { let mut parts = [0i64]; assert_eq!(apportion(1805, &mut parts, &[0], 903), None); assert_eq!(parts, [0]); let mut parts = [0i64, 0]; assert_eq!(apportion(1806, &mut parts, &[0, 0], 903), None); // Just under: accepted. let mut parts = [0i64, 0]; assert_eq!(apportion(1805, &mut parts, &[1, 0], 903), Some(())); assert_eq!(parts, [1805, 0]); } /// The i64 model's line range: up to 2^40 either way, inclusive. #[test] fn i64_line_range_is_two_to_the_fortieth() { assert!((1i64 << 40).in_line_range()); assert!((-(1i64 << 40)).in_line_range()); assert!(!((1i64 << 40) + 1).in_line_range()); assert!(!i64::MIN.in_line_range()); } /// B-9: a reversal flips every side and keeps accounts and amounts, so /// with the original it nets every account to zero. #[test] fn a_reversal_flips_every_line_and_nets_to_zero() { let lines = [ (1u8, Side::Debit, 70i64), (2, Side::Credit, 50), (3, Side::Credit, 20), ]; let rev = reversal(&lines); assert_eq!(rev.len(), lines.len()); for (o, r) in lines.iter().zip(&rev) { assert_eq!((r.0, r.1, r.2), (o.0, flip(o.1), o.2)); } let both = movements(lines.iter().chain(&rev).copied()).unwrap(); assert_eq!(both, vec![(1, 0), (2, 0), (3, 0)]); } /// Spec scenario 1's preconditions: parts and remainders pair up, and /// the unit is positive; otherwise nothing is apportioned. #[test] fn apportion_refuses_mismatched_inputs_or_a_nonpositive_unit() { let mut parts = [0i64, 0]; assert_eq!(apportion(0, &mut parts, &[0], 1), None); assert_eq!(parts, [0, 0]); let mut none: [i64; 0] = []; assert_eq!(apportion(0, &mut none, &[], 0), None); assert_eq!(apportion(0, &mut none, &[], -1), None); } /// apportion_cumulative scenario 1: mismatched lengths, a bad unit, an /// input out of line range, or a shortfall apportion refuses: None, /// and the parts untouched. #[test] fn apportion_cumulative_refuses_mismatched_or_out_of_range_inputs() { let big = (1i64 << 40) + 1; for (total, parts, rems, unit, given) in [ (4i64, [2i64, 2], [0i64, 0], 1i64, &[0i64][..]), (4, [2, 2], [0, 0], 0, &[0, 0][..]), (4, [2, 2], [0, 0], big, &[0, 0][..]), (big, [2, 2], [0, 0], 1, &[0, 0][..]), (4, [big, 2], [0, 0], 1, &[0, 0][..]), (4, [2, 2], [0, 0], 1, &[big, 0][..]), // Three units short of two parts: not rounding. (7, [2, 2], [0, 0], 1, &[0, 0][..]), ] { let mut p = parts; assert_eq!( apportion_cumulative(total, &mut p, &rems, unit, given), None, "{total} {parts:?} {unit} {given:?}" ); assert_eq!(p, parts); } // Remainders that don't pair with the parts: apportion's refusal. let mut p = [2i64, 2]; assert_eq!(apportion_cumulative(4, &mut p, &[0], 1, &[0, 0]), None); assert_eq!(p, [2, 2]); } /// apportion_cumulative scenario 2 (cents, i64): three cents closed /// one at a time gave A all three; the fourth cent's close apportions /// the running 4 cents (2 / 2) and takes back what A and B had. #[test] fn apportion_cumulative_trues_up_a_legacy_split() { let mut parts = [2i64, 2]; apportion_cumulative(4, &mut parts, &[0, 0], 1, &[3, 0]).unwrap(); assert_eq!(parts, [-1, 2]); // A running total of 5: floors 2 / 2 with equal remainders, so the // tied unit goes to the first (3 / 2), less what each had (3 / 1). let mut parts = [2i64, 2]; apportion_cumulative(5, &mut parts, &[5, 5], 1, &[3, 1]).unwrap(); assert_eq!(parts, [0, 1]); } /// With no parts, only a zero whole is fully apportioned. #[test] fn apportion_to_no_parts_accepts_only_zero() { let mut none: [i64; 0] = []; assert_eq!(apportion(0, &mut none, &[], 1), Some(())); assert_eq!(apportion(5, &mut none, &[], 1), None); } /// A zero movement's columns are both a plain zero, never "-0" (found /// by mutation triage, 2026-10-13: it returned a negative-zero credit). #[test] fn a_zero_movement_has_no_negative_zero_column() { let (d, c) = columns(Decimal::ZERO).unwrap(); assert_eq!( (d.to_string(), c.to_string()), ("0".to_string(), "0".to_string()) ); } #[test] fn credit_line_sides() { assert_eq!(credit_line(5i64), Some((Side::Credit, 5))); assert_eq!(credit_line(-5i64), Some((Side::Debit, 5))); assert_eq!(credit_line(0i64), None); } }