a C ABI, so this can be embedded rather than spawned

Every desktop shell that reads typikon today runs the binary and parses
its JSON. That costs about 1.5ms and is the right answer for a calendar
that changes once a day, but it is the wrong shape for anything with an
FFI and no business spawning a process on every redraw.

Dates cross as Julian Day Numbers, so no struct layout is part of the
contract. A day is opaque and owns its strings, built once when it is
reckoned: an accessor returns the same borrowed pointer every time, so a
statusline polling one in a loop allocates nothing. The accessors carry
every field of the JSON, so nothing on the other side needs a parser.

The Python bindings and this were resolving a jurisdiction by two copies
of the same logic, and the copy here leaked one per call. Both now share
jurisdictions::resolve_full, which interns the combinations instead.

The header is a promise made in a second file, and a second file drifts:
a test reads both and fails if either has a symbol the other does not,
and does the same for the LuaJIT cdef, where a stale declaration is a
segfault rather than a compile error.
This commit is contained in:
funkpower
2026-09-18 14:46:25 -04:00
parent 452d7fc0a1
commit d90d4c171f
14 changed files with 1523 additions and 98 deletions
+4 -58
View File
@@ -14,8 +14,6 @@ use pyo3::exceptions::{PyValueError, PyOSError};
use pyo3::prelude::*;
use pyo3::types::{PyDate, PyDict, PyList};
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use crate::calendars::Date;
use crate::day;
@@ -44,67 +42,15 @@ fn from_date<'py>(py: Python<'py>, d: Date) -> PyResult<Bound<'py, PyDate>> {
PyDate::new(py, y, m as u8, day as u8)
}
/// jurisdiction key, tradition key, and whether the Typikon's full discipline was
/// asked for — enough to tell one override from another.
type Interned = HashMap<(&'static str, &'static str, bool), &'static Jurisdiction>;
/// Build the jurisdiction a call should reckon by: the named one, or the saved
/// default, with the saved tradition and discipline applied on top.
///
/// This mirrors main.rs deliberately and has to keep mirroring it. A Jurisdiction
/// is no longer just a key — it carries a tradition and a discipline, and the CLI
/// reads all three from config before it reckons anything. A binding that resolved
/// only the key would answer a different fast than the command line does on the
/// same machine for the same date: xerophagy on a Lenten Wednesday reads as
/// wine_oil, which is not a cosmetic difference in a tool whose whole job is to
/// say what may be eaten.
///
/// The overridden jurisdictions are interned rather than leaked per call. main.rs
/// leaks one per run and is then done; a library gets called in a loop, and one
/// leak per call is a slow leak. The set of combinations is bounded by
/// jurisdictions x traditions x discipline, so a map of them is small and never
/// grows past it.
/// The jurisdiction a call should reckon by, resolved the same way the command
/// line resolves it: the named one or the remembered default, with the
/// remembered usage and discipline applied on top.
fn resolve(
jurisdiction: Option<&str>,
tradition: Option<&str>,
typikon: Option<bool>,
) -> PyResult<&'static Jurisdiction> {
let saved = crate::config::saved();
// The environment beats the file, which is the order main.rs uses. Reading
// only the file meant TYPIKON_JURISDICTION moved the command line and left
// the bindings behind, so the two answered for different churches on the
// same machine, which is the exact failure the note above is about.
let (default_name, _) = crate::config::default_jurisdiction();
let name = jurisdiction.map(str::to_owned).or(default_name);
let base = jurisdictions::resolve(name.as_deref())
.map_err(|n| PyValueError::new_err(format!("unknown jurisdiction {n:?}")))?;
let trad = match tradition.map(str::to_owned).or(saved.tradition) {
None => base.tradition,
Some(t) => jurisdictions::Tradition::parse(&t).ok_or_else(|| {
PyValueError::new_err(format!("unknown tradition {t:?}; try slavic or greek"))
})?,
};
let full = typikon.unwrap_or(saved.typikon);
let discipline = if full {
Some(jurisdictions::RuleKind::Orthodox)
} else {
base.discipline
};
if trad == base.tradition && discipline == base.discipline {
return Ok(base);
}
static INTERNED: OnceLock<Mutex<Interned>> = OnceLock::new();
let mut map = INTERNED
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.map_err(|_| PyValueError::new_err("jurisdiction cache poisoned"))?;
Ok(map.entry((base.key, trad.key(), full)).or_insert_with(|| {
Box::leak(Box::new(Jurisdiction { tradition: trad, discipline, ..*base }))
}))
jurisdictions::resolve_full(jurisdiction, tradition, typikon).map_err(PyValueError::new_err)
}
/// One day, fully reckoned.