use nibli_types::ast::{
AbstractionKind, Argument, AstBuffer, Conversion, Determiner, Marker, ModalTag, Predicate,
Pronoun, Proposition, RelClause, RelClauseKind, Sentence, SentenceConnective,
};
use nibli_types::ast::{Connective, DeonticMood, Tense};
use crate::ast::{
AbsKind, Arg, Claim, ClauseBody, Det, KeyTerm, PredSeq, PredUnit, Predication, RelKind, Restr,
RestrKind, Statement, Tag, Term,
};
use crate::parser::{ParseError, err_at};
use crate::resolve::{PredInfo, ResolvedEntry, label_index, lookup, lookup_compound};
use nibli_types::ast::BlockQuant;
fn emit_name(entry: &ResolvedEntry) -> (String, Option<u8>) {
match entry {
ResolvedEntry::Atomic(e) => (
e.swap.map(|s| s.base).unwrap_or(e.name).to_owned(),
e.swap.map(|s| s.with),
),
ResolvedEntry::Compound(c) => (c.relation.to_owned(), None),
}
}
pub fn emit(input: &str, statements: &[Statement]) -> Result<AstBuffer, ParseError> {
let mut emitter = Emitter::new(input);
for statement in statements {
let root = emitter.statement(statement)?;
emitter.buffer.roots.push(root);
}
Ok(emitter.buffer)
}
pub(crate) fn emit_recovering(
input: &str,
statements: &[Statement],
) -> (AstBuffer, Vec<ParseError>) {
let mut emitter = Emitter::new(input);
let mut errors = Vec::new();
for statement in statements {
let snapshot = emitter.snapshot();
match emitter.statement(statement) {
Ok(root) => emitter.buffer.roots.push(root),
Err(e) => {
emitter.truncate(snapshot);
emitter.reset_walk_state();
errors.push(e);
}
}
}
(emitter.buffer, errors)
}
struct Emitter<'a> {
input: &'a str,
buffer: AstBuffer,
statement_start: usize,
in_clause_body: bool,
in_property: bool,
block_it_var: Option<String>,
}
impl<'a> Emitter<'a> {
fn new(input: &'a str) -> Self {
Emitter {
input,
buffer: AstBuffer {
predicates: Vec::new(),
arguments: Vec::new(),
sentences: Vec::new(),
roots: Vec::new(),
},
statement_start: 0,
in_clause_body: false,
in_property: false,
block_it_var: None,
}
}
fn statement(&mut self, statement: &Statement) -> Result<u32, ParseError> {
self.statement_start = statement.span.start;
self.claim(&statement.claim, statement.span.start)
}
fn snapshot(&self) -> (usize, usize, usize, usize) {
(
self.buffer.predicates.len(),
self.buffer.arguments.len(),
self.buffer.sentences.len(),
self.buffer.roots.len(),
)
}
fn truncate(
&mut self,
(predicates, arguments, sentences, roots): (usize, usize, usize, usize),
) {
self.buffer.predicates.truncate(predicates);
self.buffer.arguments.truncate(arguments);
self.buffer.sentences.truncate(sentences);
self.buffer.roots.truncate(roots);
}
fn reset_walk_state(&mut self) {
self.in_clause_body = false;
self.in_property = false;
self.block_it_var = None;
}
fn fail(&self, at: usize, message: impl Into<String>) -> ParseError {
err_at(self.input, at, message)
}
fn refill_error(&self, at: usize, index: usize, info: &PredInfo) -> ParseError {
self.fail(
at,
format!(
"place x{} of {:?} is filled twice (NIBLI_KR §5 fail-closed)",
index + 1,
info.surface
),
)
}
fn push_predicate(&mut self, s: Predicate) -> u32 {
self.buffer.predicates.push(s);
(self.buffer.predicates.len() - 1) as u32
}
fn push_argument(&mut self, s: Argument) -> u32 {
self.buffer.arguments.push(s);
(self.buffer.arguments.len() - 1) as u32
}
fn push_sentence(&mut self, s: Sentence) -> u32 {
self.buffer.sentences.push(s);
(self.buffer.sentences.len() - 1) as u32
}
fn var_particle(&mut self, name: &str, _at: usize) -> Result<String, ParseError> {
Ok(format!("${name}"))
}
fn resolved(&self, word: &str, at: usize) -> Result<PredInfo, ParseError> {
lookup(word).map_err(|m| self.fail(at, m))
}
fn resolved_head(&self, seq: &PredSeq, at: usize) -> Result<PredInfo, ParseError> {
match seq.0.last().expect("pred_seq is non-empty") {
PredUnit::Word(parts) if parts.len() > 1 => {
lookup_compound(parts).map_err(|m| self.fail(at, m))
}
PredUnit::Word(parts) => self.resolved(&parts[0], at),
PredUnit::Group(inner) => self.resolved_head(inner, at),
}
}
fn claim(&mut self, claim: &Claim, at: usize) -> Result<u32, ParseError> {
match claim {
Claim::Prenex { vars, body } => {
let mut lowered = Vec::new();
for v in vars {
lowered.push(self.var_particle(v, at)?);
}
let body_idx = self.claim(body, at)?;
Ok(self.push_sentence(Sentence::Prenex((lowered, body_idx))))
}
Claim::DetBlock {
det,
restr,
var,
body,
} => self.det_block(*det, restr, var, body, at),
Claim::Impl(a, b) => {
let l = self.claim(a, at)?;
let r = self.claim(b, at)?;
Ok(self.push_sentence(Sentence::Connected((SentenceConnective::Implies, l, r))))
}
Claim::Iff(a, b) => self.afterthought(Connective::Iff, a, b, at),
Claim::Xor(a, b) => self.afterthought(Connective::Xor, a, b, at),
Claim::Or(a, b) => self.afterthought(Connective::Or, a, b, at),
Claim::And(a, b) => self.afterthought(Connective::And, a, b, at),
Claim::Not(inner) => self.simple(inner, true, None, None, at),
Claim::Prefixed {
deontic,
tense,
atom,
} => {
let (negated, inner) = match atom.as_ref() {
Claim::Not(inner) => (true, inner.as_ref()),
other => (false, other),
};
self.simple(inner, negated, *tense, *deontic, at)
}
simple @ (Claim::Equality(..) | Claim::Predication(_)) => {
self.simple(simple, false, None, None, at)
}
}
}
fn afterthought(
&mut self,
conn: Connective,
a: &Claim,
b: &Claim,
at: usize,
) -> Result<u32, ParseError> {
let l = self.claim(a, at)?;
let r = self.claim(b, at)?;
Ok(self.push_sentence(Sentence::Connected((
SentenceConnective::Afterthought(conn),
l,
r,
))))
}
fn simple(
&mut self,
claim: &Claim,
negated: bool,
tense: Option<Tense>,
deontic: Option<DeonticMood>,
at: usize,
) -> Result<u32, ParseError> {
let proposition = match claim {
Claim::Predication(p) => self.predication_proposition(p, negated, tense, deontic)?,
Claim::Equality(lhs, rhs) => {
let relation = self.push_predicate(Predicate::Root("equals".into()));
let head = self.term(lhs, at)?;
let tail = self.term(rhs, at)?;
Proposition {
relation,
terms: vec![head, tail],
x1_present: true,
negated,
tense,
deontic,
}
}
other => unreachable!("simple() over a compound claim: {other:?}"),
};
Ok(self.push_sentence(Sentence::Simple(proposition)))
}
fn predication_proposition(
&mut self,
p: &Predication,
negated: bool,
tense: Option<Tense>,
deontic: Option<DeonticMood>,
) -> Result<Proposition, ParseError> {
let relation = self.pred_seq(&p.seq, p.span.start)?;
let info = self.resolved_head(&p.seq, p.span.start)?;
let mut x1_term: Option<u32> = None;
let mut rest_terms = Vec::new();
let mut filled = [false; 5];
let mut next_positional = 0usize;
for arg in &p.args {
match &arg.label {
None => {
let index = next_positional;
next_positional += 1;
if index >= info.arity as usize {
return Err(self.fail(
arg.span.start,
format!(
"too many arguments for {:?} (arity {})",
info.surface, info.arity
),
));
}
if filled[index] {
return Err(self.refill_error(arg.span.start, index, &info));
}
filled[index] = true;
let idx = self.term(&arg.term, arg.span.start)?;
if index == 0 {
x1_term = Some(idx);
} else {
rest_terms.push(idx);
}
}
Some(label) => {
let place = label_index(&info, label).ok_or_else(|| {
self.fail(
arg.span.start,
format!(
"unknown place label {label:?} for {:?} (arity {}; dictionary \
labels or raw x1..x{} only)",
info.surface, info.arity, info.arity
),
)
})?;
if filled[place] {
return Err(self.refill_error(arg.span.start, place, &info));
}
filled[place] = true;
let inner = self.term(&arg.term, arg.span.start)?;
rest_terms.push(self.push_argument(Argument::Tagged((place as u8, inner))));
}
}
}
for tag in &p.tags {
let info = if tag.pred.len() > 1 {
lookup_compound(&tag.pred).map_err(|m| self.fail(tag.span.start, m))?
} else {
self.resolved(&tag.pred[0], tag.span.start)?
};
if info.arity < 2 {
return Err(self.fail(
tag.span.start,
format!(
"modal tag predicate {:?} has arity {} — `via` predicates \
need arity >= 2 to link the tagged term (NIBLI_KR §5)",
info.surface, info.arity
),
));
}
let (word, _) = emit_name(&info.entry);
let modal_predicate = self.push_predicate(Predicate::Root(word));
let inner = self.term(&tag.term, tag.span.start)?;
rest_terms.push(
self.push_argument(Argument::ModalTagged((ModalTag(modal_predicate), inner))),
);
}
let x1_present = x1_term.is_some();
let terms: Vec<u32> = x1_term.into_iter().chain(rest_terms).collect();
Ok(Proposition {
relation,
terms,
x1_present,
negated,
tense,
deontic,
})
}
fn det_block(
&mut self,
det: Det,
restr: &Restr,
var: &str,
body: &Claim,
at: usize,
) -> Result<u32, ParseError> {
if det == Det::The {
let snapshot = self.snapshot();
self.term(
&Term::Det {
det: Det::The,
restr: restr.clone(),
},
at,
)?;
self.truncate(snapshot);
let desugared = substitute_var_in_claim(body, var, restr);
return self.claim(&desugared, at);
}
let particle = self.var_particle(var, at)?;
let quant_kind = match det {
Det::Exactly(n) => Some(BlockQuant::ExactCount(n)),
Det::ExactlyThe(n) => Some(BlockQuant::ExactCountDefinite(n)),
Det::EveryThe => Some(BlockQuant::UniversalDefinite),
_ => None,
};
let restr_core = match quant_kind {
Some(_) => self.restr_predicate(restr)?,
None => self.restr_proposition_sentence(restr, &particle)?,
};
let mut where_sents = Vec::new();
let mut also_sents = Vec::new();
for rc in &restr.rel_clauses {
let s = self.block_clause_sentence(rc, &particle)?;
match rc.kind {
RelKind::Where => where_sents.push(s),
RelKind::Also => also_sents.push(s),
}
}
if let Some(kind) = quant_kind {
let clause = match where_sents.len() {
0 => None,
_ => {
let first = where_sents[0];
Some(self.and_chain(first, &where_sents[1..]))
}
};
let body_idx = self.claim(body, at)?;
let matrix = self.and_chain(body_idx, &also_sents);
return Ok(self.push_sentence(Sentence::Quantified((
kind, particle, restr_core, clause, matrix,
))));
}
match det {
Det::Every => {
let antecedent = self.and_chain(restr_core, &where_sents);
let body_idx = self.claim(body, at)?;
let matrix = self.and_chain(body_idx, &also_sents);
let impl_idx = self.push_sentence(Sentence::Connected((
SentenceConnective::Implies,
antecedent,
matrix,
)));
Ok(self.push_sentence(Sentence::Prenex((vec![particle], impl_idx))))
}
Det::Some => {
let with_wheres = self.and_chain(restr_core, &where_sents);
let body_idx = self.claim(body, at)?;
let with_body = self.push_sentence(Sentence::Connected((
SentenceConnective::And,
with_wheres,
body_idx,
)));
Ok(self.and_chain(with_body, &also_sents))
}
_ => unreachable!("quantified kinds returned above; The desugared"),
}
}
fn and_chain(&mut self, mut base: u32, extra: &[u32]) -> u32 {
for &s in extra {
base = self.push_sentence(Sentence::Connected((SentenceConnective::And, base, s)));
}
base
}
fn block_clause_sentence(
&mut self,
rc: &crate::ast::RelClause,
particle: &str,
) -> Result<u32, ParseError> {
match &rc.body {
ClauseBody::Bare { negated, seq } => {
let mut relation = self.pred_seq(seq, rc.span.start)?;
if *negated {
relation = self.push_predicate(Predicate::Negated(relation));
}
let head = self.push_argument(Argument::Variable(particle.to_owned()));
Ok(self.push_sentence(Sentence::Simple(Proposition {
relation,
terms: vec![head],
x1_present: true,
negated: false,
tense: None,
deontic: None,
})))
}
ClauseBody::Full(claim) => {
let prev = self.block_it_var.replace(particle.to_owned());
let idx = self.claim(claim, rc.span.start);
self.block_it_var = prev;
idx
}
}
}
fn restr_proposition_sentence(
&mut self,
restr: &Restr,
particle: &str,
) -> Result<u32, ParseError> {
let relation = self.restr_predicate(restr)?;
let head = self.push_argument(Argument::Variable(particle.to_owned()));
Ok(self.push_sentence(Sentence::Simple(Proposition {
relation,
terms: vec![head],
x1_present: true,
negated: false,
tense: None,
deontic: None,
})))
}
fn pred_seq(&mut self, seq: &PredSeq, at: usize) -> Result<u32, ParseError> {
let mut ids: Vec<u32> = Vec::new();
for unit in &seq.0 {
ids.push(self.pred_unit(unit, at)?);
}
let mut acc = ids.pop().expect("pred_seq non-empty");
while let Some(modifier) = ids.pop() {
acc = self.push_predicate(Predicate::Pair((modifier, acc)));
}
Ok(acc)
}
fn pred_unit(&mut self, unit: &PredUnit, at: usize) -> Result<u32, ParseError> {
match unit {
PredUnit::Group(inner) => {
let inner_idx = self.pred_seq(inner, at)?;
Ok(self.push_predicate(Predicate::Grouped(inner_idx)))
}
PredUnit::Word(parts) => {
if parts.len() > 1 {
let info = lookup_compound(parts)
.map_err(|m| self.fail(at, format!("internal (post-resolve): {m}")))?;
let (word, _) = emit_name(&info.entry);
return Ok(self.push_predicate(Predicate::Root(word)));
}
let word = &parts[0];
let info = self.resolved(word, at)?;
let (word, swap) = emit_name(&info.entry);
let root = self.push_predicate(Predicate::Root(word));
Ok(match swap {
None => root,
Some(p) => self.push_predicate(Predicate::Converted((conversion_for(p), root))),
})
}
}
}
fn term(&mut self, term: &Term, at: usize) -> Result<u32, ParseError> {
let argument = match term {
Term::Unspecified => Argument::Unspecified,
Term::Witness => Argument::Marker(Marker::Witness),
Term::Number(n) => Argument::Number(*n),
Term::Str(s) => Argument::QuotedLiteral(s.clone()),
Term::Var(v) => Argument::Variable(self.var_particle(v, at)?),
Term::Key(KeyTerm::It) => match &self.block_it_var {
Some(v) => Argument::Variable(v.clone()),
None if self.in_clause_body => Argument::Marker(Marker::It),
None => {
return Err(self.fail(
self.statement_start,
"`it` (the relativized entity) is only meaningful inside a \
where/also clause body, or as a named bound-place marker in \
restrictor linked args (NIBLI_KR §7)",
));
}
},
Term::Key(KeyTerm::Slot) => {
if !self.in_property {
return Err(self.fail(
self.statement_start,
"`slot` (the open place) is only meaningful inside a \
`property { … }` body (NIBLI_KR §3)",
));
}
Argument::Marker(Marker::Slot)
}
Term::Key(k) => Argument::Pronoun(keyterm_pronoun(*k)),
Term::Name { name, rel_clauses } => {
if !name.contains('_')
&& crate::resolve::PRONOUN_COLLISION_NAMES
.contains(&name.to_lowercase().as_str())
{
return Err(self.fail(
self.statement_start,
format!(
"the name `{name}` collides with the pronoun `{}` — after \
lowering both would denote the same constant; pick a \
different name (NIBLI_KR §3)",
name.to_lowercase()
),
));
}
let mut idx =
self.push_argument(Argument::Name(name.to_lowercase().replace('_', " ")));
for rc in rel_clauses {
let clause = self.rel_clause(rc)?;
idx = self.push_argument(Argument::Restricted((idx, clause)));
}
return Ok(idx);
}
Term::Abstraction { kind, body } => {
let saved = self.in_property;
if *kind == AbsKind::Property {
self.in_property = true;
}
let body_idx = self.claim(body, at);
self.in_property = saved;
let predicate = self
.push_predicate(Predicate::Abstraction((abstraction_kind(*kind), body_idx?)));
Argument::Description((Determiner::Indefinite, predicate))
}
Term::Det { det, restr } => {
let predicate = self.restr_predicate(restr)?;
let base = match det {
Det::Some => Argument::Description((Determiner::Indefinite, predicate)),
Det::The => Argument::Description((Determiner::Definite, predicate)),
Det::Every => Argument::Description((Determiner::Every, predicate)),
Det::EveryThe => Argument::Description((Determiner::EveryThe, predicate)),
Det::Exactly(n) => {
Argument::QuantifiedDescription((*n, Determiner::Indefinite, predicate))
}
Det::ExactlyThe(n) => {
Argument::QuantifiedDescription((*n, Determiner::Definite, predicate))
}
};
let mut idx = self.push_argument(base);
for rc in &restr.rel_clauses {
let clause = self.rel_clause(rc)?;
idx = self.push_argument(Argument::Restricted((idx, clause)));
}
return Ok(idx);
}
};
Ok(self.push_argument(argument))
}
fn restr_predicate(&mut self, restr: &Restr) -> Result<u32, ParseError> {
let at = restr.span.start;
let core = match &restr.kind {
RestrKind::Selected { pred, label } => {
let info = self.resolved(pred, at)?;
let (word, alias_swap) = emit_name(&info.entry);
let mut idx = self.push_predicate(Predicate::Root(word));
if let Some(p) = alias_swap {
idx = self.push_predicate(Predicate::Converted((conversion_for(p), idx)));
}
let place = label_index(&info, label).ok_or_else(|| {
self.fail(
at,
format!(
"unknown selector place {label:?} for {pred:?} (arity {}; \
dictionary labels or raw x1..x{} only)",
info.arity, info.arity
),
)
})? + 1;
if place > 1 {
idx = self
.push_predicate(Predicate::Converted((conversion_for(place as u8), idx)));
}
idx
}
RestrKind::Seq { seq, linked_args } => {
let mut idx = self.pred_seq(seq, at)?;
if !linked_args.is_empty() {
let info = self.resolved_head(seq, at)?;
let mut filled = [false; 5];
let mut it_place: Option<usize> = None;
let mut placed: Vec<(usize, u32)> = Vec::new();
let mut next_positional = 1usize; for arg in linked_args {
let is_it = matches!(arg.term, Term::Key(KeyTerm::It));
let index = match &arg.label {
None => {
if is_it {
return Err(self.fail(
arg.span.start,
"mark the bound place with a NAMED `it` (e.g. `x2: it` \
or `loved: it`) — a positional `it` is ambiguous",
));
}
let i = next_positional;
next_positional += 1;
if i >= info.arity as usize {
return Err(self.fail(
arg.span.start,
format!(
"too many linked arguments for {:?} (arity {}; \
positional links fill x2 onward — the bound \
variable takes x1)",
info.surface, info.arity
),
));
}
i
}
Some(label) => label_index(&info, label).ok_or_else(|| {
self.fail(
arg.span.start,
format!(
"unknown place label {label:?} for {:?} (arity {})",
info.surface, info.arity
),
)
})?,
};
if filled[index] {
return Err(self.fail(
arg.span.start,
format!(
"place x{} of {:?} is filled twice",
index + 1,
info.surface
),
));
}
filled[index] = true;
if is_it {
if it_place.is_some() {
return Err(self.fail(
arg.span.start,
"at most one `it` may mark the bound place of a restrictor",
));
}
it_place = Some(index);
} else {
let term_idx = self.term(&arg.term, arg.span.start)?;
placed.push((index + 1, term_idx));
}
}
if it_place.is_none() && filled[0] {
return Err(self.fail(
at,
"these linked arguments fill x1, but the bound variable needs it — \
mark the bound place explicitly with `it` (e.g. `x2: it`)",
));
}
let bound_place = it_place.map(|i| i + 1).unwrap_or(1);
if bound_place > 1 {
idx = self.push_predicate(Predicate::Converted((
conversion_for(bound_place as u8),
idx,
)));
}
let mut by_converted: Vec<(usize, u32)> = placed
.into_iter()
.map(|(q, t)| (if q == 1 { bound_place } else { q }, t))
.collect();
by_converted.sort_by_key(|(p, _)| *p);
let max_place = by_converted.last().map(|(p, _)| *p).unwrap_or(1);
let mut be_args: Vec<u32> = Vec::new();
let mut iter = by_converted.into_iter().peekable();
for place in 2..=max_place {
if iter.peek().map(|(p, _)| *p) == Some(place) {
let (_, term_idx) = iter.next().unwrap();
be_args.push(term_idx);
} else {
be_args.push(self.push_argument(Argument::Unspecified));
}
}
idx = self.push_predicate(Predicate::WithArgs((idx, be_args)));
}
idx
}
};
Ok(if restr.negated {
self.push_predicate(Predicate::Negated(core))
} else {
core
})
}
fn rel_clause(&mut self, rc: &crate::ast::RelClause) -> Result<RelClause, ParseError> {
let kind = match rc.kind {
RelKind::Where => RelClauseKind::Restrictive,
RelKind::Also => RelClauseKind::Incidental,
};
let suspended = self.block_it_var.take();
let body_sentence = match &rc.body {
ClauseBody::Bare { negated, seq } => {
self.pred_seq(seq, rc.span.start).map(|relation| {
let head = self.push_argument(Argument::Marker(Marker::It));
self.push_sentence(Sentence::Simple(Proposition {
relation,
terms: vec![head],
x1_present: true,
negated: *negated,
tense: None,
deontic: None,
}))
})
}
ClauseBody::Full(claim) => {
let saved = self.in_clause_body;
self.in_clause_body = true;
let result = self.claim(claim, rc.span.start);
self.in_clause_body = saved;
result
}
};
self.block_it_var = suspended;
Ok(RelClause {
kind,
body_sentence: body_sentence?,
})
}
}
fn substitute_var_in_claim(claim: &Claim, var: &str, the_restr: &Restr) -> Claim {
let sub = |c: &Claim| Box::new(substitute_var_in_claim(c, var, the_restr));
match claim {
Claim::Prenex { vars, body } => {
if vars.iter().any(|v| v == var) {
claim.clone() } else {
Claim::Prenex {
vars: vars.clone(),
body: sub(body),
}
}
}
Claim::DetBlock {
det,
restr,
var: inner_var,
body,
} => Claim::DetBlock {
det: *det,
restr: substitute_var_in_restr(restr, var, the_restr),
var: inner_var.clone(),
body: if inner_var == var {
body.clone() } else {
sub(body)
},
},
Claim::Impl(a, b) => Claim::Impl(sub(a), sub(b)),
Claim::Iff(a, b) => Claim::Iff(sub(a), sub(b)),
Claim::Xor(a, b) => Claim::Xor(sub(a), sub(b)),
Claim::Or(a, b) => Claim::Or(sub(a), sub(b)),
Claim::And(a, b) => Claim::And(sub(a), sub(b)),
Claim::Not(inner) => Claim::Not(sub(inner)),
Claim::Prefixed {
deontic,
tense,
atom,
} => Claim::Prefixed {
deontic: *deontic,
tense: *tense,
atom: sub(atom),
},
Claim::Equality(a, b) => Claim::Equality(
substitute_var_in_term(a, var, the_restr),
substitute_var_in_term(b, var, the_restr),
),
Claim::Predication(p) => Claim::Predication(Predication {
seq: p.seq.clone(),
args: p
.args
.iter()
.map(|a| Arg {
label: a.label.clone(),
term: substitute_var_in_term(&a.term, var, the_restr),
span: a.span.clone(),
})
.collect(),
tags: p
.tags
.iter()
.map(|t| Tag {
pred: t.pred.clone(),
term: substitute_var_in_term(&t.term, var, the_restr),
span: t.span.clone(),
})
.collect(),
span: p.span.clone(),
}),
}
}
fn substitute_var_in_term(term: &Term, var: &str, the_restr: &Restr) -> Term {
match term {
Term::Var(v) if v == var => Term::Det {
det: Det::The,
restr: the_restr.clone(),
},
Term::Abstraction { kind, body } => Term::Abstraction {
kind: *kind,
body: Box::new(substitute_var_in_claim(body, var, the_restr)),
},
Term::Name { name, rel_clauses } => Term::Name {
name: name.clone(),
rel_clauses: rel_clauses
.iter()
.map(|rc| substitute_var_in_rel_clause(rc, var, the_restr))
.collect(),
},
Term::Det { det, restr } => Term::Det {
det: *det,
restr: substitute_var_in_restr(restr, var, the_restr),
},
other => other.clone(),
}
}
fn substitute_var_in_restr(restr: &Restr, var: &str, the_restr: &Restr) -> Restr {
Restr {
negated: restr.negated,
kind: match &restr.kind {
RestrKind::Seq { seq, linked_args } => RestrKind::Seq {
seq: seq.clone(),
linked_args: linked_args
.iter()
.map(|a| Arg {
label: a.label.clone(),
term: substitute_var_in_term(&a.term, var, the_restr),
span: a.span.clone(),
})
.collect(),
},
selected @ RestrKind::Selected { .. } => selected.clone(),
},
rel_clauses: restr
.rel_clauses
.iter()
.map(|rc| substitute_var_in_rel_clause(rc, var, the_restr))
.collect(),
span: restr.span.clone(),
}
}
fn substitute_var_in_rel_clause(
rc: &crate::ast::RelClause,
var: &str,
the_restr: &Restr,
) -> crate::ast::RelClause {
crate::ast::RelClause {
kind: rc.kind,
body: match &rc.body {
bare @ ClauseBody::Bare { .. } => bare.clone(),
ClauseBody::Full(claim) => {
ClauseBody::Full(Box::new(substitute_var_in_claim(claim, var, the_restr)))
}
},
span: rc.span.clone(),
}
}
fn conversion_for(place: u8) -> Conversion {
match place {
2 => Conversion::Swap12,
3 => Conversion::Swap13,
4 => Conversion::Swap14,
5 => Conversion::Swap15,
other => unreachable!("conversion place {other} (the place checks bound places to 2..=5)"),
}
}
fn keyterm_pronoun(k: KeyTerm) -> Pronoun {
match k {
KeyTerm::Me => Pronoun::Me,
KeyTerm::You => Pronoun::You,
KeyTerm::We => Pronoun::We,
KeyTerm::WeAll => Pronoun::WeAll,
KeyTerm::WeOthers => Pronoun::WeOthers,
KeyTerm::YouAll => Pronoun::YouAll,
KeyTerm::This => Pronoun::This,
KeyTerm::That => Pronoun::That,
KeyTerm::Yonder => Pronoun::Yonder,
KeyTerm::ItA => Pronoun::ItA,
KeyTerm::ItE => Pronoun::ItE,
KeyTerm::ItI => Pronoun::ItI,
KeyTerm::ItO => Pronoun::ItO,
KeyTerm::ItU => Pronoun::ItU,
KeyTerm::It | KeyTerm::Slot => {
unreachable!("It/Slot are matched by their guarded term() arms")
}
}
}
fn abstraction_kind(kind: AbsKind) -> AbstractionKind {
match kind {
AbsKind::Event => AbstractionKind::Event,
AbsKind::Fact => AbstractionKind::Fact,
AbsKind::Property => AbstractionKind::Property,
AbsKind::Amount => AbstractionKind::Amount,
AbsKind::Concept => AbstractionKind::Concept,
}
}
#[cfg(test)]
mod tests {
use crate::parse_checked;
fn nibli_kr_lb(text: &str) -> String {
let buffer = parse_checked(text).unwrap_or_else(|e| panic!("nibli-kr {text:?}: {e}"));
let lb = nibli_semantics::compile_from_ast(buffer).unwrap_or_else(|e| {
panic!("nibli-semantics rejected nibli-kr buffer for {text:?}: {e}")
});
format!("{lb:?}")
}
fn twins(kr: &str) {
let _ = nibli_kr_lb(kr);
}
#[test]
fn ground_fact_twins() {
twins("person(Adam).");
twins("dog(Rex).");
twins("goes(me, some market).");
twins("loves(me, _).");
twins("removes().");
}
#[test]
fn named_args_equal_positional() {
assert_eq!(
nibli_kr_lb("goes(me, destination: some market)."),
nibli_kr_lb("goes(me, some market)."),
);
assert_eq!(
nibli_kr_lb("goes(destination: some market, goer: me)."),
nibli_kr_lb("goes(me, some market)."),
);
}
#[test]
fn named_args_equal_positional_in_where_body() {
assert_eq!(
nibli_kr_lb("animal(every dog where loves(lover: Alis, loved: it))."),
nibli_kr_lb("animal(every dog where loves(Alis, it))."),
);
assert_eq!(
nibli_kr_lb("animal(every dog where loves(loved: it))."),
nibli_kr_lb("animal(every dog where loves(_, it))."),
);
}
#[test]
fn determiner_twins() {
twins("animal(every dog).");
twins("goes(the dog).");
twins("red(exactly 2 red).");
twins("goes(no dog).");
}
#[test]
fn equality_negation_prefix_twins() {
twins("Kim = Adam.");
twins("~goes(me).");
twins("past dog(Dan).");
}
#[test]
fn operator_twins() {
twins("goes(me) & eats(you).");
twins("goes(me) | eats(you).");
twins("dog(Rex) -> animal(Rex).");
}
#[test]
fn prenex_twins() {
twins("all $x: dog($x) -> animal($x).");
}
#[test]
fn block_every_twin() {
twins("every dog $d: animal($d).");
}
#[test]
fn abstraction_twins() {
twins("desires(me, event { goes(you) }).");
twins("able(me, property { fast(slot) }).");
}
#[test]
fn converted_alias_and_selector_twins() {
twins("permitted(every person where approves).");
twins("permitted(every loves.loved).");
}
#[test]
fn pair_and_linked_arg_twins() {
twins("healthy data(Kanrek).");
twins("permitted(every tends(some data)).");
twins("goes(Adam where dog).");
}
#[test]
fn emitted_buffers_are_semantics_valid() {
for text in [
"some dog $d: big($d) & goes($d).",
"goes(every loves(x2: it)).",
"animal(every dog where loves(lover: Alis, loved: it)).",
"computer+user(me).",
"goes(me) via uses(this).",
"goes(every chemical where increases where thin).",
"goes(some dog where it = Adam).",
"knows(me, fact { dog(Adam) }).",
"must past ~goes(me).",
"[big fast] dog(Rex).",
] {
nibli_kr_lb(text); }
}
#[test]
fn uncurated_compound_fails_closed() {
let e = parse_checked("dog+cat(me).").unwrap_err();
let msg = format!("{e}");
assert!(
msg.contains("unknown compound predicate \"dog+cat\""),
"{msg}"
);
assert!(msg.contains("committed corpus entry"), "{msg}");
}
#[test]
fn compound_emits_relation_ident() {
let b = parse_checked("computer+user(me, this).").unwrap();
assert!(
b.predicates
.iter()
.any(|p| matches!(p, nibli_types::ast::Predicate::Root(w) if w == "computer_user")),
"compound must emit Root(\"computer_user\"): {:?}",
b.predicates
);
}
#[test]
fn quantified_blocks_lower_and_compile() {
for text in [
"exactly 2 dog $d: goes($d).",
"exactly 2 dog $d: big($d) & goes($d).",
"exactly 0 dog $d: goes($d).",
"exactly 2 the dog $d: goes($d).",
"every the dog $d: goes($d).",
"every dog where big $d: goes($d).",
"every dog where owns(it, some data) $d: goes($d).",
"every dog also big $d: goes($d).",
"some dog where big $d: goes($d).",
"exactly 2 dog where big $d: goes($d).",
] {
nibli_kr_lb(text); }
}
#[test]
fn exactly_block_emits_quantified_sentence() {
let b = parse_checked("exactly 2 dog $d: goes($d).").unwrap();
assert!(
b.sentences.iter().any(|s| matches!(
s,
nibli_types::ast::Sentence::Quantified((
nibli_types::ast::BlockQuant::ExactCount(2), v, _, None, _
))
if v == "$d"
)),
"exactly-block must emit Sentence::Quantified(ExactCount(2), $d): {:?}",
b.sentences
);
}
#[test]
fn the_block_desugars_by_substitution() {
let b = parse_checked("the dog $d: big($d) & goes($d).").unwrap();
assert!(
!b.sentences
.iter()
.any(|s| matches!(s, nibli_types::ast::Sentence::Quantified(_))),
"the-block must desugar away, not bind"
);
let definite_count = b
.arguments
.iter()
.filter(|a| {
matches!(
a,
nibli_types::ast::Argument::Description((
nibli_types::ast::Determiner::Definite,
_
))
)
})
.count();
assert_eq!(definite_count, 2, "both $d occurrences become `the dog`");
let rendered = crate::render::render(&b).unwrap();
assert_eq!(rendered, "big(the dog) & goes(the dog).");
}
#[test]
fn where_clause_on_every_block_folds_into_antecedent() {
assert_eq!(
nibli_kr_lb("every dog where big $d: goes($d)."),
nibli_kr_lb("all $d: dog($d) & big($d) -> goes($d)."),
"the where-clause must fold into the rule antecedent"
);
}
#[test]
fn errata_rejects_via_parse_checked() {
for (text, needle) in [
("~past goes(me).", "past ~P"),
("~~goes(me).", "double negation"),
("past (a(A) & b(A)).", "single predication"),
] {
let e = parse_checked(text).unwrap_err();
assert!(format!("{e}").contains(needle), "{text}: {e}");
}
}
mod checks {
use crate::emit::emit;
use crate::parser::{ParseError, parse_statements};
fn ok(input: &str) {
let statements =
parse_statements(input).unwrap_or_else(|e| panic!("parse {input:?}: {e}"));
if let Err(e) = emit(input, &statements) {
panic!("emit failed for {input:?}: {e}");
}
}
fn bad(input: &str) -> ParseError {
let statements =
parse_statements(input).unwrap_or_else(|e| panic!("parse {input:?}: {e}"));
match emit(input, &statements) {
Ok(_) => panic!("expected emit error for {input:?}"),
Err(e) => e,
}
}
#[test]
fn corpus_names_resolve_and_gismu_reject() {
ok("goes(me, destination: some market).");
ok("animal(every dog).");
let e = bad("klama(me, x2: some market).");
assert!(e.message.contains("unknown predicate"), "{e}");
ok("obligated_by(every data, x2: this).");
}
#[test]
fn unknown_names_fail_closed() {
let e = bad("zzq(me).");
assert!(e.message.contains("unknown predicate"), "{e}");
let e = bad("goes(every zzq dog).");
assert!(e.message.contains("unknown predicate"), "{e}");
let e = bad("goes(some dog where zzq).");
assert!(e.message.contains("unknown predicate"), "{e}");
}
#[test]
fn place_checks() {
let e = bad("dog(Adam, you, me).");
assert!(e.message.contains("too many arguments"), "{e}");
let e = bad("goes(me, zzlabel: you).");
assert!(e.message.contains("unknown place label"), "{e}");
let e = bad("goes(me, x1: you).");
assert!(e.message.contains("filled twice"), "{e}");
let e = bad("dog(Adam, x3: you).");
assert!(e.message.contains("unknown place label"), "{e}");
ok("goes(goer: me, destination: some market).");
}
#[test]
fn selector_places() {
ok("permitted(every loves.loved).");
ok("permitted(every loves.x2).");
let e = bad("permitted(every loves.zzplace).");
assert!(e.message.contains("unknown selector place"), "{e}");
}
#[test]
fn linked_args_and_the_bound_place() {
ok("permitted(every tends(some data)).");
ok("permitted(every tends(charge: some data)).");
ok("goes(every loves(x2: it)).");
ok("goes(every loves(loved: it)).");
let e = bad("goes(every loves(x1: you)).");
assert!(e.message.contains("bound variable needs it"), "{e}");
let e = bad("goes(every loves(it)).");
assert!(e.message.contains("NAMED `it`"), "{e}");
let e = bad("goes(every loves(x1: it, x2: it)).");
assert!(e.message.contains("at most one `it`"), "{e}");
}
#[test]
fn many_variables_ok() {
ok("all $x, $y, $z: loves($x, $y) & dog($z).");
ok("dog($a) & dog($b) & dog($c) & dog($d).");
ok("all $x, $y, $z: loves($x, $w).");
}
#[test]
fn name_colliding_with_pronoun_fails() {
let e = bad("goes(Me).");
assert!(e.message.contains("collides with the pronoun `me`"), "{e}");
let e = bad("loves(Adam, You).");
assert!(e.message.contains("collides with the pronoun `you`"), "{e}");
let e = bad("dog(This).");
assert!(
e.message.contains("collides with the pronoun `this`"),
"{e}"
);
let e = bad("dog(YONDER).");
assert!(
e.message.contains("collides with the pronoun `yonder`"),
"{e}"
);
ok("goes(We_All).");
ok("dog(Metis).");
}
#[test]
fn it_and_slot_positions() {
let e = bad("goes(it).");
assert!(e.message.contains("where/also clause body"), "{e}");
let e = bad("goes(slot).");
assert!(e.message.contains("property"), "{e}");
ok("able(me, property { fast(slot) }).");
let e = bad("desires(me, event { fast(slot) }).");
assert!(e.message.contains("property"), "{e}");
ok("goes(some dog where big(it)).");
ok("goes(some dog where desires(me, event { eats(it) })).");
}
#[test]
fn tag_predicates() {
ok("goes(me) via uses(this).");
ok("goes(me) via reason(this) via entails(this).");
let e = bad("goes(me) via person(this).");
assert!(e.message.contains("arity >= 2"), "{e}");
let e = bad("goes(me) via zzq(this).");
assert!(e.message.contains("unknown predicate"), "{e}");
}
#[test]
fn bare_clause_bodies_resolve() {
ok("goes(every person where approves).");
ok("goes(every chemical where increases where thin).");
ok("goes(every healthy data).");
let e = bad("goes(every person where zzq big).");
assert!(e.message.contains("unknown predicate"), "{e}");
}
#[test]
fn error_precedence_place_check_before_term() {
let e = bad("goes(zzlabel: it).");
assert!(e.message.contains("unknown place label"), "{e}");
}
#[test]
fn error_precedence_block_head_before_clause_bodies() {
let e = bad("every zzhead where zzbody $d: goes($d).");
assert!(e.message.contains("zzhead"), "{e}");
}
#[test]
fn the_block_restrictor_validated_when_var_unused() {
let e = bad("the zzz $v: goes(me).");
assert!(e.message.contains("unknown predicate"), "{e}");
}
}
mod typed_split_conformance {
use crate::parse_checked;
use crate::render::render;
use nibli_types::ast::{Argument, Marker, Pronoun};
#[test]
fn every_pronoun_spelling_locks_reserved_surface_and_render() {
for pronoun in Pronoun::ALL {
let spelling = pronoun.as_str();
assert!(
nibli_lexicon::reserved::is_reserved(spelling),
"pronoun spelling {spelling:?} must be a reserved word"
);
let text = format!("goes({spelling}).");
let buffer = parse_checked(&text).unwrap_or_else(|e| panic!("parse {text:?}: {e}"));
assert!(
buffer
.arguments
.iter()
.any(|a| matches!(a, Argument::Pronoun(p) if *p == pronoun)),
"{text:?} must compile to Argument::Pronoun({pronoun:?})"
);
let rendered = render(&buffer).unwrap_or_else(|e| panic!("render {text:?}: {e}"));
assert_eq!(
rendered, text,
"render must re-spell {pronoun:?} identically"
);
}
}
#[test]
fn every_marker_spelling_survives_render_reparse() {
for (marker, probe, reserved_spelling) in [
(
Marker::It,
"animal(every dog where loves(Alis, it)).",
Some("it"),
),
(
Marker::Slot,
"able(me, property { fast(slot) }).",
Some("slot"),
),
(Marker::Witness, "goes(?).", None), ] {
if let Some(word) = reserved_spelling {
assert!(
nibli_lexicon::reserved::is_reserved(word),
"marker spelling {word:?} must be a reserved word"
);
}
let has_marker = |buffer: &nibli_types::ast::AstBuffer| {
buffer
.arguments
.iter()
.any(|a| matches!(a, Argument::Marker(m) if *m == marker))
};
let buffer =
parse_checked(probe).unwrap_or_else(|e| panic!("parse {probe:?}: {e}"));
assert!(
has_marker(&buffer),
"{probe:?} must compile to Argument::Marker({marker:?})"
);
let rendered = render(&buffer).unwrap_or_else(|e| panic!("render {probe:?}: {e}"));
let reparsed = parse_checked(&rendered)
.unwrap_or_else(|e| panic!("reparse of rendered {rendered:?}: {e}"));
assert!(
has_marker(&reparsed),
"rendered {rendered:?} must reparse to Argument::Marker({marker:?})"
);
}
}
#[test]
fn collision_names_are_the_no_underscore_pronoun_subset() {
let no_underscore: Vec<&str> = Pronoun::ALL
.iter()
.map(|p| p.as_str())
.filter(|s| !s.contains('_'))
.collect();
assert_eq!(
crate::resolve::PRONOUN_COLLISION_NAMES.as_slice(),
no_underscore.as_slice(),
"PRONOUN_COLLISION_NAMES must track Pronoun::as_str"
);
}
}
}