Skip to main content

nibli_kr/
lib.rs

1//! nibli KR (nibli KR) — the surface-syntax front-end for nibli.
2//!
3//! nibli KR is a predicate-call language (`goes(me, some market).`) that compiles
4//! to the same `nibli_types::ast::AstBuffer` the Lojban parser produces,
5//! reusing nibli-semantics/nibli-reason and every soundness gate unchanged. The language is
6//! specified in repo-root `NIBLI_KR.md`; the implementation program is
7//! tracked in repo-root `TODO.md`.
8//!
9//! PARSER TECHNOLOGY (user decision, 2026-07-12): pest. `src/nibli_kr.pest` is
10//! the EXECUTABLE grammar — the normative form of NIBLI_KR §15 — so the
11//! grammar and the parser cannot drift by construction.
12//!
13//! Pipeline: [`parser`] (pest walker → tree [`ast`], §6/§7 errata as targeted
14//! errors) → [`emit`] — THE single validating walk (single-resolution merge,
15//! 2026-07-17): every dictionary-driven fail-closed check (name resolution →
16//! COMPILE ERROR on unknown words, place checks, linked-args rules,
17//! `it`/`slot` position rules, Name↔pronoun collisions) runs at the site that
18//! lowers the construct into the `AstBuffer` (`$vars` preserved verbatim,
19//! corpus entries to their canonical base with `Converted` swaps). [`resolve`]
20//! is the lookup module both emit and lint share. [`parse_checked`] is the
21//! engine's fail-closed text→AST seam.
22
23pub mod ast;
24pub mod complete;
25pub mod emit;
26pub mod highlight;
27pub mod lint;
28pub mod parser;
29pub mod render;
30pub mod resolve;
31
32#[cfg(feature = "reedline")]
33pub mod complete_reedline;
34
35#[cfg(test)]
36mod shape_tests;
37
38/// The pest PEG grammar source — the normative form of NIBLI_KR §15, embedded so
39/// downstream tooling (e.g. the nibli-formalize LLM prompt) can ground on the
40/// EXACT accepted syntax, in-sync BY CONSTRUCTION: this is the same file the
41/// `#[grammar = "nibli_kr.pest"]` derive consumes, so it can never drift from the
42/// parser.
43pub const GRAMMAR: &str = include_str!("nibli_kr.pest");
44
45use nibli_types::ast::{AstBuffer, ParseResult};
46use nibli_types::error::{NibliError, SyntaxDetail};
47
48fn to_nibli(e: parser::ParseError) -> NibliError {
49    NibliError::Syntax(SyntaxDetail {
50        message: e.message,
51        line: e.line,
52        column: e.column,
53    })
54}
55
56/// FAIL CLOSED: parse + the validating emit walk, or the first (source-order)
57/// error. Feed the result to `nibli_semantics::compile_from_ast`.
58pub fn parse_checked(text: &str) -> Result<AstBuffer, NibliError> {
59    let statements = parser::parse_statements(text).map_err(to_nibli)?;
60    emit::emit(text, &statements).map_err(to_nibli)
61}
62
63/// Per-statement recovery variant (the `ParseResult` contract): every
64/// statement that parses AND emits lands in the buffer (a failing statement's
65/// partial nodes roll back); every failure is reported, first error per
66/// statement. `errors` non-empty ⇒ the buffer is PARTIAL — callers wanting
67/// fail-closed behavior use [`parse_checked`].
68pub fn parse_text(text: &str) -> ParseResult {
69    let (statements, parse_errors) = parser::parse_text_with_errors(text);
70    let (buffer, emit_errors) = emit::emit_recovering(text, &statements);
71    let errors = parse_errors
72        .into_iter()
73        .chain(emit_errors)
74        .map(|e| nibli_types::ast::ParseError {
75            message: e.message,
76            line: e.line,
77            column: e.column,
78        })
79        .collect();
80    ParseResult { buffer, errors }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn parse_text_recovers_per_statement_with_rollback() {
89        // Bad middle statement: its partial nodes truncate back out; the
90        // survivors form a structurally intact buffer (compiles + renders —
91        // no dangling indices after the rollback).
92        let r = parse_text("dog(Rex). zzq(me). goes(me, some market).");
93        assert_eq!(r.errors.len(), 1, "{:?}", r.errors);
94        assert!(
95            r.errors[0].message.contains("unknown predicate"),
96            "{:?}",
97            r.errors
98        );
99        assert_eq!(r.buffer.roots.len(), 2, "two survivors");
100        let rendered = render::render(&r.buffer).unwrap();
101        assert!(rendered.contains("dog(Rex)"), "{rendered}");
102        assert!(rendered.contains("goes(me, some market)"), "{rendered}");
103        assert!(!rendered.contains("zzq"), "{rendered}");
104        nibli_semantics::compile_from_ast(r.buffer).unwrap();
105    }
106
107    #[test]
108    fn parse_text_resets_walk_state_after_a_failed_statement() {
109        // Statement 1 fails INSIDE a block rel-clause body (mid-walk state
110        // set); statement 2's bare `it` must still hit the position rule —
111        // a polluted `block_it_var`/`in_clause_body` would let it through.
112        let r = parse_text("every dog where zzq(it, you) $d: big($d). big(it).");
113        assert_eq!(r.errors.len(), 2, "{:?}", r.errors);
114        assert!(
115            r.errors[0].message.contains("unknown predicate"),
116            "{:?}",
117            r.errors
118        );
119        assert!(
120            r.errors[1].message.contains("where/also clause body"),
121            "{:?}",
122            r.errors
123        );
124        assert!(r.buffer.roots.is_empty(), "no survivors");
125        assert!(r.buffer.sentences.is_empty(), "rollback left nodes behind");
126    }
127}