Skip to content

ADR 0002 single rust core with thin bindings

Karthik P edited this page Oct 31, 2026 · 1 revision

ADR-0002: Single Rust core with thin bindings

All calendar logic lives in one Rust crate, and the Python and npm packages only convert values at the edge and call the core's JSON API.

Last reviewed: 31 Oct 2026

Status

Accepted on 31 Oct 2026. Decided by the research documents: the research proposal ("Ecosystem bindings") and the gap analysis ("Binding and platform risks"), which calls the one-core architecture right. Built in 0.1.0.

Context

The proposal's goal is one engine that gives identical answers in Rust, Python and JavaScript. The existing tools it surveys each live in one ecosystem: ThreeTen-Extra in Java, Fiscal5253 in Perl, fiscalyear and pandas in Python, retail-calendar in JavaScript (see Analysis). Fiscal calendar arithmetic has many edge cases (start days 29 to 31, the extra week, year numbers near 1 January), and every separate implementation is another place for them to go wrong.

The proposal also argues for a batch path: each operation is cheap, so for single dates most of the time goes into crossing the language boundary, and a compiled core taking whole arrays is where it expects the gain. That claim is to be measured by per-binding benchmarks, planned for 0.2 on the Roadmap; it is not yet measured.

Decision drivers

  • Identical results in every language, checkable byte for byte.
  • One place to fix a bug and one place to add a vector.
  • Small maintainer team: the proposal notes that fiscalyear stalled with one maintainer.
  • Reach into Python, JavaScript and Rust users, plus WebAssembly for browsers.

Considered options

Option 1: An independent implementation per language

Pros:

  • No native build step; pure-Python and pure-JS packages install everywhere.
  • Each package can use its ecosystem's idioms throughout.

Cons:

  • Three implementations of every edge case, kept in step by hand.
  • Shared conformance vectors catch drift only for the cases they contain.

Option 2: One Rust core, thin bindings that only convert types (chosen)

Pros:

  • Calendar arithmetic and validation exist once. Bindings cannot disagree on a result because they do not compute one.
  • Rust compiles to native extensions for Python and to WebAssembly for JavaScript.
  • The core's JSON API gives a byte-level comparison point across languages (ADR-0019).

Cons:

  • Python and npm packages need a native or WebAssembly build per platform, which makes the release workflow larger (14 wheels, an sdist, a crate and an npm tarball in 0.1.0).
  • Contributors who only know Python or JavaScript cannot change behaviour without touching Rust.

Option 3: A Rust core with richer bindings that add their own logic

Pros:

  • Bindings could offer language-specific conveniences, such as pandas-native results, without a round trip through JSON.

Cons:

  • Logic in a binding is logic the other bindings lack, which reopens the drift problem of Option 1.

Decision

Option 2. AGENTS.md design rule 9 makes it checkable: bindings call fiscalclock::json and return its JSON unchanged on their *_json methods, and no binding reimplements a computation, reorders keys or reformats values. The repository map forbids calendar arithmetic or validation in bindings/python/ and bindings/js/.

As built in 0.1.0:

  • crates/core is the crate fiscalclock, with json::validate and json::call taking and returning JSON text (crate README).
  • bindings/python is a PyO3 extension plus a Python layer that converts datetime.date values and dataclasses (ADR-0011).
  • bindings/js is a wasm-bindgen module plus a TypeScript layer (ADR-0020).
  • The CI job identical-output in ci.yml compares 14 transcripts (Rust, and every Python and Node.js job on Linux, macOS and Windows) with the Rust transcript byte for byte.

Consequences

Good:

  • A fix or new behaviour lands once and reaches every package.
  • The cross-binding check is a plain file comparison, not a semantic diff.

Bad:

  • Every binding pays a JSON round trip per call. The bulk paths that would amortise it (ADR-0012, typed arrays in JavaScript) are planned for 0.2, not built.
  • The CLI and MCP server planned for 0.4 must also go through the core's JSON API (ADR-0013); the repository map forbids CLI logic that the JSON API cannot reach.

Links

Clone this wiki locally