Skip to main content

nibli_lexicon/
lib.rs

1//! nibli-lexicon — the COMMITTED English corpus (the dictionary IS Rust source;
2//! see [`corpus`] and the Dictionary Data section of CLAUDE.md).
3//!
4//! Since the committed-corpus milestone there is exactly ONE build mode: the
5//! strongly-typed tables in `src/corpus/` are committed, const-validated at
6//! compile time, and identical in local, CI, and deployed builds. The old
7//! build-time parse of `dictionary-en.json` (and its FULL/FALLBACK dual mode)
8//! is gone — the JSON is now only the INPUT of the `tools/lexigen` regeneration
9//! tool (`just regen-lexicon`).
10//!
11//! The public surface is functions-only (no pub statics), so the internals can
12//! evolve without consumer churn:
13//! - [`alias`] / [`compound`] — KR-input resolution (English names / `a+b`
14//!   compound spellings; gismu do NOT resolve — see the compat note below).
15//! - [`get_arity`] / [`get_gloss`] / [`get_template`] — IR-relation lookups.
16//! - [`canonical_alias`] / [`by_provenance`] — the gismu→English provenance
17//!   bridge (Predilex gates, render overlays, lexigen drift reports).
18//! - [`corpus::corpus_entries`] / [`corpus::corpus_compounds`] — iteration.
19//!
20//! Gismu do not resolve anywhere: the ONLY spellings are the English corpus
21//! names (and `a+b` compound spellings). The source gismu survives solely as
22//! the [`by_provenance`] bridge — provenance metadata, never input.
23
24pub mod corpus;
25pub mod reserved;
26
27pub use corpus::{CompoundEntry, CorpusTier, PredicateEntry, Swap};
28
29use std::collections::BTreeMap;
30use std::sync::LazyLock;
31
32/// Look up an English predicate name (the only resolvable atomic spelling).
33/// `None` means the name is unknown — nibli-kr's resolver FAILS CLOSED (no
34/// arity-2 default; NIBLI_KR §13).
35pub fn alias(name: &str) -> Option<&'static PredicateEntry> {
36    corpus::corpus_predicate(name)
37}
38
39/// Look up a compound predicate by its `a+b` KR spelling.
40pub fn compound(spelling: &str) -> Option<&'static CompoundEntry> {
41    corpus::corpus_compound(spelling)
42}
43
44/// Look up a compound by its IR RELATION ident (`computer_user`) — the
45/// post-emit direction (render/arity/place-name lookups on compound
46/// relations). Linear over the curated compound table, which stays small.
47pub fn compound_by_relation(rel: &str) -> Option<&'static CompoundEntry> {
48    corpus::corpus_compounds().find(|c| c.relation == rel)
49}
50
51/// English place names of an IR relation: an atomic corpus name or a
52/// compound's relation ident. `None` = unknown relation (callers fall back
53/// to positional `x<N>`).
54pub fn relation_places(rel: &str) -> Option<&'static [&'static str]> {
55    alias(rel)
56        .map(|e| e.places)
57        .or_else(|| compound_by_relation(rel).map(|c| c.places))
58}
59
60/// The provenance bridge: gismu → its CANONICAL (unswapped) corpus entry.
61/// Permanent API — the Predilex gates key their Lojban-lemma bounds through it,
62/// and future ontology-row import uses the same mapping.
63pub fn by_provenance(gismu: &str) -> Option<&'static PredicateEntry> {
64    static BY_PROVENANCE: LazyLock<BTreeMap<&'static str, &'static PredicateEntry>> =
65        LazyLock::new(|| {
66            corpus::corpus_entries()
67                .filter(|e| e.swap.is_none())
68                .map(|e| (e.source_gismu, e))
69                .collect()
70        });
71    BY_PROVENANCE.get(gismu).copied()
72}
73
74/// The canonical (plain, unswapped) English name for a gismu — the render
75/// direction. Converted entries are never canonical.
76pub fn canonical_alias(gismu: &str) -> Option<&'static str> {
77    by_provenance(gismu).map(|e| e.name)
78}
79
80/// Arity of an IR relation (English corpus name or compound relation ident).
81pub fn get_arity(word: &str) -> Option<usize> {
82    alias(word)
83        .map(|e| e.arity() as usize)
84        .or_else(|| compound_by_relation(word).map(|c| c.arity() as usize))
85}
86
87/// Primary English gloss of an IR relation.
88pub fn get_gloss(word: &str) -> Option<&'static str> {
89    alias(word)
90        .map(|e| e.gloss)
91        .or_else(|| compound_by_relation(word).map(|c| c.gloss))
92}
93
94/// Curated English place-frame template (`{x1}`..`{x5}` placeholders), e.g.
95/// `get_template("dog")` -> `Some("{x1} is a dog")`. `None` = no curated frame;
96/// callers build a generic frame from [`get_gloss`] + [`get_arity`].
97pub fn get_template(word: &str) -> Option<&'static str> {
98    alias(word)
99        .map(|e| e.template)
100        .or_else(|| compound_by_relation(word).map(|c| c.template))
101        .flatten()
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn english_lookups() {
110        let goes = alias("goes").expect("goes");
111        assert_eq!(goes.arity(), 5);
112        assert_eq!(goes.source_gismu, "klama");
113        assert_eq!(get_arity("goes"), Some(5));
114        assert_eq!(get_arity("dog"), Some(2));
115        assert_eq!(get_gloss("dog"), Some("dog"));
116        assert_eq!(get_template("dog"), Some("{x1} is a dog"));
117        assert_eq!(get_arity("quantity"), Some(3));
118    }
119
120    #[test]
121    fn converted_entries_carry_their_base() {
122        let owned = alias("owned").expect("owned");
123        let swap = owned.swap.expect("owned is converted");
124        assert_eq!(swap.with, 2);
125        assert_eq!(swap.base, "owns");
126        assert_eq!(owned.source_gismu, "ponse");
127        // Canonical direction never returns a converted entry.
128        assert_eq!(canonical_alias("ponse"), Some("owns"));
129    }
130
131    #[test]
132    fn provenance_bridge() {
133        assert_eq!(by_provenance("klama").map(|e| e.name), Some("goes"));
134        assert_eq!(by_provenance("gerku").map(|e| e.name), Some("dog"));
135        assert!(by_provenance("nosuchword").is_none());
136    }
137
138    #[test]
139    fn gismu_never_resolve() {
140        // GISMU-INPUT DEATH: raw gismu are provenance metadata, never input.
141        assert_eq!(get_arity("klama"), None);
142        assert_eq!(get_template("gerku"), None);
143        assert!(alias("klama").is_none());
144        // The bridge is the only gismu-keyed direction.
145        assert_eq!(by_provenance("klama").map(|e| e.name), Some("goes"));
146    }
147
148    #[test]
149    fn compounds_resolve_by_plus_spelling_only() {
150        let cu = compound("computer+user").expect("seed compound");
151        assert_eq!(cu.relation, "computer_user");
152        assert_eq!(cu.arity(), 3);
153        assert!(
154            compound("dog+cat").is_none(),
155            "undefined compounds fail closed"
156        );
157        assert!(
158            alias("computer_user").is_none(),
159            "the relation ident is not an input spelling"
160        );
161    }
162
163    #[test]
164    fn compound_relations_resolve_post_emit() {
165        // The post-emit direction: the IR relation ident reaches the entry's
166        // arity/gloss/places (render + semantics arity for compound relations).
167        assert_eq!(
168            compound_by_relation("computer_user").map(|c| c.name),
169            Some("computer+user")
170        );
171        assert_eq!(get_arity("computer_user"), Some(3));
172        assert!(get_gloss("computer_user").is_some());
173        assert_eq!(relation_places("computer_user").map(|p| p[0]), Some("user"));
174        assert_eq!(relation_places("dog").map(|p| p[0]), Some("dog"));
175        assert!(relation_places("no_such_relation").is_none());
176    }
177
178    #[test]
179    fn no_shipped_name_is_reserved() {
180        for e in corpus::corpus_entries() {
181            assert!(
182                !reserved::is_reserved(e.name),
183                "corpus name {:?} collides with a nibli KR keyword",
184                e.name
185            );
186        }
187    }
188
189    #[test]
190    fn every_place_is_named() {
191        // The type makes this structural; assert the shipped data anyway.
192        for e in corpus::corpus_entries() {
193            assert!(!e.places.is_empty() && e.places.len() <= 5, "{:?}", e.name);
194            for p in e.places {
195                assert!(!p.is_empty(), "{:?} has an unnamed place", e.name);
196            }
197        }
198    }
199}