Visitar URL original
refactor: single-source the posting weight arithmetic in booking by robcohen · Pull Request #1736 · rustledger/rustledger · GitHub
Skip to content

refactor: single-source the posting weight arithmetic in booking - #1736

Merged
robcohen merged 4 commits into
mainfrom
refactor/weight-single-source
Jul 10, 2026
Merged

robcohen merged 4 commits into
mainfrom
refactor/weight-single-source

Conversation

@robcohen

@robcohen robcohen commented Jul 9, 2026 •

Copy link
Copy Markdown
Member

What

Single-sources the Beancount posting-weight arithmetic. The generic ladder in rustledger-booking (shared by the residual and interpolation since #1700) is now exposed as two public Decimal functions, and the two copies of the rule — the BQL weight column and the currency_accounts plugin — delegate to it.

Why

Phase-1 correctness sweep, residual/weight family. The weight rule existed three times, and the copies had already drifted from the canonical:

Same drift category as #1648/#1726: consumers re-deriving a rule the pipeline already owns.

How

New public surface in rustledger-booking, each a thin Decimal instantiation of the existing generic (WeightNum) implementation — no second copy of the arithmetic exists:

residual_weight's inline price arm now goes through the same generic helper. The query keeps its fallback order (cost → price → units, Null without units) and drops its now-unused txn_date parameter; the plugin keeps its DTO parsing, with the exhaustive compile-fail match preserved as the CostNumberData → CostNumber mapping.

Edge-behavior deltas in the copies (all toward the canonical): query's zero-units @@ weight is now 0 instead of +total; plugin's negative-magnitude totals are no longer abs'd (the validator rejects negative cost totals upstream, so this is unreachable in the pipeline).

Deep-review addenda (post-publication):

  • A fourth zero-units delta: booking skips the Total → PerUnitFromTotal rewrite for zero-unit postings, so 0 X {{100 USD}} reaches the executor as raw Total. Old code: CostSpec::resolve's zero-units guard returned None → weight fell through to price/units (0 in EUR or X). New code: weight is Amount(0, USD) — same magnitude, different currency bucket for a zero. This matches the residual validator's bucketing (and removes a case where the old currency_accounts path could inject a neutralizing posting the Late balance check then rejected on a valid ledger).
  • Correction: the "currency-less specs fall through" bullet describes behavior that was ALREADY true on main (resolve starts with self.currency.clone()?) — it's a preserved invariant, not a change.

New deps: rustledger-query and rustledger-plugin now depend on rustledger-booking (both already sit above it in the build graph via the loader; no cycles).

Testing

  • New: test_postings_table_weight_uses_preserved_total pins SELECT weight = exactly 100.00 USD for the PerUnitFromTotal shape (pre-fix: 99.99…); booking unit tests cover all four CostNumber variants and the @/@@ sign rules including zero units.
  • Existing booking (110), query (401 integration + parity), and plugin (131) suites all green — the residual arithmetic is byte-identical (same generic functions), and the BQL compat: weight() sign flip on total_annotation.beancount (1 mismatch) #1052 sign tests still pass through the delegation.
  • clippy --all-features --all-targets -D warnings, fmt, and cargo doc --all-features -D warnings clean.

🤖 Generated with Claude Code

The Beancount posting-weight rule (cost beats price; preserved totals;
@@ sign follows units) existed three times: the canonical generic
ladder in rustledger-booking (shared by the residual and interpolation
since #1700), a copy in the query executor's compute_posting_weight
(the BQL weight column), and a copy in the currency_accounts plugin's
weight_of closure. The copies had already sprouted real divergences
from the canonical:

- query recomputed total-cost weights as units x per_unit via
  CostSpec::resolve, reintroducing the division-then-multiplication
  precision loss the booking ladder deliberately avoids by using the
  preserved total (#1106/#1113): SELECT weight for a booked
  {{100.00 USD}} over 3 units showed 99.99999999999999999999999999
  where rledger check balances with 100.00 exactly.
- the plugin took abs() of totals and @@ prices before re-signing,
  silently "fixing" negative magnitudes instead of following the
  canonical total x signum arithmetic.

Expose the ladder from booking as two public Decimal functions backed
by the existing generic (WeightNum) implementations — no second copy:

- cost_number_weight(units, &CostNumber): the four-variant cost
  arithmetic (PerUnit multiply, Total/PerUnitFromTotal preserved total
  x signum, Compound N*a+b).
- price_weight(units, price, PriceKind): @ = |units| x price x signum,
  @@ = price x signum (the #1052 credit-side flip; zero units weigh
  zero for both kinds).

residual_weight's price arm now uses the same generic helper, so the
validator, BQL weight column, and currency_accounts neutralization all
compute weights through one ladder. The query's fallback order and the
plugin's DTO parsing (with its exhaustive compile-fail match, now the
CostNumberData -> CostNumber mapping) are unchanged.

New tests pin the exact-total weight through SELECT weight, all four
CostNumber variants, and the price sign rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@robcohen
robcohen requested a review from a team as a code owner July 9, 2026 18:10
Copilot AI review requested due to automatic review settings July 9, 2026 18:10
@github-actions

github-actions Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

Tool Time Memory
rustledger 35.9ms ± 0.8ms 23.6 MB
beancount 828.2ms ± 69.7ms 50.7 MB
Comparison 23.1x faster 2.1x less

vs main branch (2026-07-10)

Metric main this PR Change
Time 33.7ms 35.9ms +6.5% 🟡
10K transactions · 5 runs, 2 warmup · hyperfine
What do these numbers mean?
  • Time: Wall-clock time to parse and validate the ledger (± std dev)
  • Memory: Peak resident set size (RSS)
  • vs main: Compared to latest nightly benchmark on main branch

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR single-sources Beancount posting-weight arithmetic by exposing canonical Decimal helpers from rustledger-booking and updating downstream consumers (BQL weight and the currency_accounts plugin) to delegate to those helpers, preventing drift from the balance validator’s residual logic.

Changes:

  • Exposes rustledger_booking::cost_number_weight and rustledger_booking::price_weight built on the existing generic weight ladder, and routes residual price-weighting through the same helper.
  • Refactors BQL posting-weight computation to use the booking helpers (and removes the now-unused txn_date parameter), plus adds a regression test for preserved-total (PerUnitFromTotal) behavior.
  • Refactors currency_accounts plugin weight computation to delegate to the booking helpers and adds new crate dependencies accordingly.

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
supply-chain/imports.lock Updates recorded import metadata for crossbeam-epoch 0.9.20.
supply-chain/config.toml Removes crossbeam-epoch safe-to-deploy exemptions (0.9.19/0.9.20).
crates/rustledger-query/tests/bql_integration_test.rs Adds regression test asserting preserved-total weight precision for #postings.weight.
crates/rustledger-query/src/executor/system_tables.rs Updates #postings weight column to call the refactored compute_posting_weight.
crates/rustledger-query/src/executor/mod.rs Refactors compute_posting_weight to delegate to booking helpers and documents the ladder/fallback order.
crates/rustledger-query/src/executor/evaluation.rs Keeps default-FROM weight accessor in sync with postings-table weight logic.
crates/rustledger-query/Cargo.toml Adds rustledger-booking dependency to reuse canonical weight arithmetic.
crates/rustledger-plugin/src/native/plugins/currency_accounts.rs Delegates plugin weight arithmetic to booking helpers after DTO parsing.
crates/rustledger-plugin/Cargo.toml Adds rustledger-booking dependency for shared weight arithmetic.
crates/rustledger-booking/src/lib.rs Introduces public Decimal weight helpers and factors shared generic implementations.
Cargo.lock Updates lockfile for new inter-crate dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions

github-actions Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

✅ Compatibility Test Results

Metric Result
Check Compatibility 100%
BQL Compatibility 100%
Full AST Match 100%
Total Files 811
What's tested
  • Check: Exit codes and error counts match bean-check
  • BQL: Query results match bean-query
  • Files: ~800 test files downloaded from 10+ beancount repositories

Full results available in workflow artifacts

robcohen and others added 2 commits July 10, 2026 02:37
The weight arithmetic reads only .total for PerUnitFromTotal, so the
per_unit consistency invariant is irrelevant at this site; enforcing
it would need an error channel the infallible weight closure doesn't
have. Wire-pair consistency is enforced at the ingress boundary
(ffi-wasi input_entry_to_directive uses try_new).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.46457% with 21 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ger-plugin/src/native/plugins/currency_accounts.rs 19.23% 21 Missing ⚠️

📢 Thoughts on this report? Let us know!

@robcohen
robcohen merged commit 3a87131 into main Jul 10, 2026
56 of 57 checks passed
@robcohen
robcohen deleted the refactor/weight-single-source branch July 10, 2026 09:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants