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}