1use crate::combinators::InvHom;
19use crate::homomorphism::{HomLabel, Homomorphism};
20use crate::score::WeightScorer;
21use crate::{
22 Explicit, FxHashMap, IntersectionHeuristic, Span, StateId, StringDecompositionAutomaton,
23 Symbol, TopDownTa,
24};
25use packed_term_arena::tree::{Tree, TreeArena};
26use std::collections::BTreeMap;
27
28type Bag = BTreeMap<u32, u32>;
30type ReqList = Box<[(u32, u32)]>;
32
33#[derive(Clone, Debug)]
34enum Tok {
35 Word(u32),
36 Child(usize),
37}
38
39struct FlatRule {
40 result: usize,
41 tokens: Vec<Tok>,
42}
43
44fn frontier(arena: &TreeArena<HomLabel>, node: Tree, children: &[StateId], out: &mut Vec<Tok>) {
45 match *arena.get_label(node) {
46 HomLabel::Var(i) => {
47 if let Some(c) = children.get(i) {
48 out.push(Tok::Child(c.index()));
49 }
50 }
51 HomLabel::Symbol(s) => {
52 let kids = arena.get_children(node);
53 if kids.is_empty() {
54 out.push(Tok::Word(s.0));
55 } else {
56 for &k in kids {
57 frontier(arena, k, children, out);
58 }
59 }
60 }
61 }
62}
63
64fn bag_add(acc: &mut Bag, other: &Bag) {
65 for (&k, &v) in other {
66 *acc.entry(k).or_insert(0) += v;
67 }
68}
69
70fn bag_min(a: &Bag, b: &Bag) -> Bag {
72 let mut out = Bag::new();
73 for (&k, &av) in a {
74 if let Some(&bv) = b.get(&k) {
75 let m = av.min(bv);
76 if m > 0 {
77 out.insert(k, m);
78 }
79 }
80 }
81 out
82}
83
84fn meet_update(slot: &mut Option<Bag>, cand: Bag, changed: &mut bool) {
85 let new = match slot.as_ref() {
86 None => cand,
87 Some(cur) => bag_min(cur, &cand),
88 };
89 if slot.as_ref() != Some(&new) {
90 *slot = Some(new);
91 *changed = true;
92 }
93}
94
95fn freeze(slot: &Option<Bag>) -> Option<ReqList> {
96 slot.as_ref()
97 .map(|b| b.iter().map(|(&k, &v)| (k, v)).collect())
98}
99
100pub struct ObligatoryLeafTables {
103 req_left: Vec<Option<ReqList>>,
107 req_right: Vec<Option<ReqList>>,
108}
109
110impl ObligatoryLeafTables {
111 pub fn from_grammar(grammar: &Explicit, hom: &Homomorphism) -> Self {
114 let num_states = grammar.num_states() as usize;
115 let arena = hom.arena();
116
117 let mut flat_rules: Vec<FlatRule> = Vec::new();
119 for rule in grammar.rules() {
120 let Some(term) = hom.get(rule.symbol) else {
121 continue;
122 };
123 if rule
124 .children
125 .iter()
126 .any(|c| c.is_stuck() || c.index() >= num_states)
127 || rule.result.is_stuck()
128 || rule.result.index() >= num_states
129 {
130 continue;
131 }
132 let mut tokens = Vec::new();
133 frontier(arena, term, rule.children, &mut tokens);
134 flat_rules.push(FlatRule {
135 result: rule.result.index(),
136 tokens,
137 });
138 }
139 let mut accepting = Vec::new();
140 grammar.initial_states(&mut |s: StateId| {
141 if !s.is_stuck() && s.index() < num_states {
142 accepting.push(s.index());
143 }
144 });
145
146 let mut mic: Vec<Option<Bag>> = vec![None; num_states];
148 loop {
149 let mut changed = false;
150 for r in &flat_rules {
151 let mut acc = Bag::new();
152 let mut feasible = true;
153 for tok in &r.tokens {
154 match tok {
155 Tok::Word(s) => {
156 *acc.entry(*s).or_insert(0) += 1;
157 }
158 Tok::Child(c) => match &mic[*c] {
159 Some(m) => bag_add(&mut acc, m),
160 None => {
161 feasible = false;
162 break;
163 }
164 },
165 }
166 }
167 if feasible {
168 meet_update(&mut mic[r.result], acc, &mut changed);
169 }
170 }
171 if !changed {
172 break;
173 }
174 }
175
176 let productive: Vec<&FlatRule> = flat_rules
178 .iter()
179 .filter(|r| {
180 r.tokens.iter().all(|t| match t {
181 Tok::Child(c) => mic[*c].is_some(),
182 _ => true,
183 })
184 })
185 .collect();
186
187 let mut req_left: Vec<Option<Bag>> = vec![None; num_states];
189 let mut req_right: Vec<Option<Bag>> = vec![None; num_states];
190 for &a in &accepting {
191 req_left[a] = Some(Bag::new());
192 req_right[a] = Some(Bag::new());
193 }
194 loop {
195 let mut changed = false;
196 for r in &productive {
197 for (pos, tok) in r.tokens.iter().enumerate() {
198 let x = match tok {
199 Tok::Child(c) => *c,
200 _ => continue,
201 };
202 let mut left = Bag::new();
203 let mut right = Bag::new();
204 for (q, t2) in r.tokens.iter().enumerate() {
205 if q == pos {
206 continue;
207 }
208 let side = if q < pos { &mut left } else { &mut right };
209 match t2 {
210 Tok::Word(s) => {
211 *side.entry(*s).or_insert(0) += 1;
212 }
213 Tok::Child(c) => bag_add(side, mic[*c].as_ref().unwrap()),
214 }
215 }
216 if let Some(pr) = req_right[r.result].clone() {
217 let mut cand = pr;
218 bag_add(&mut cand, &right);
219 meet_update(&mut req_right[x], cand, &mut changed);
220 }
221 if let Some(pl) = req_left[r.result].clone() {
222 let mut cand = pl;
223 bag_add(&mut cand, &left);
224 meet_update(&mut req_left[x], cand, &mut changed);
225 }
226 }
227 }
228 if !changed {
229 break;
230 }
231 }
232
233 ObligatoryLeafTables {
234 req_left: req_left.iter().map(freeze).collect(),
235 req_right: req_right.iter().map(freeze).collect(),
236 }
237 }
238
239 pub fn for_sentence<'a, S: WeightScorer>(
243 &'a self,
244 sentence: &[Symbol],
245 scorer: &S,
246 ) -> ObligatoryLeafHeuristic<'a> {
247 let mut positions: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
249 for (i, sym) in sentence.iter().enumerate() {
250 positions.entry(sym.0).or_default().push(i as u32);
251 }
252 ObligatoryLeafHeuristic {
253 tables: self,
254 positions,
255 pass: scorer.one(),
256 prune: scorer.zero(),
257 }
258 }
259}
260
261pub struct ObligatoryLeafHeuristic<'a> {
264 tables: &'a ObligatoryLeafTables,
265 positions: FxHashMap<u32, Vec<u32>>,
266 pass: f64,
267 prune: f64,
268}
269
270impl ObligatoryLeafHeuristic<'_> {
271 #[inline]
273 fn supply_left(&self, t: u32, start: usize) -> usize {
274 self.positions
275 .get(&t)
276 .map_or(0, |p| p.partition_point(|&q| (q as usize) < start))
277 }
278
279 #[inline]
281 fn supply_right(&self, t: u32, end: usize) -> usize {
282 self.positions
283 .get(&t)
284 .map_or(0, |p| p.len() - p.partition_point(|&q| (q as usize) < end))
285 }
286
287 #[inline]
293 fn prunes(&self, left: StateId, span: &Span) -> bool {
294 let idx = left.index();
295 if let Some(Some(req)) = self.tables.req_left.get(idx).map(|o| o.as_deref()) {
296 for &(t, need) in req {
297 if self.supply_left(t, span.start) < need as usize {
298 return true;
299 }
300 }
301 }
302 if let Some(Some(req)) = self.tables.req_right.get(idx).map(|o| o.as_deref()) {
303 for &(t, need) in req {
304 if self.supply_right(t, span.end) < need as usize {
305 return true;
306 }
307 }
308 }
309 false
310 }
311
312 #[inline]
313 fn estimate(&self, left: StateId, span: &Span) -> f64 {
314 if self.prunes(left, span) {
315 self.prune
316 } else {
317 self.pass
318 }
319 }
320}
321
322impl IntersectionHeuristic<StringDecompositionAutomaton> for ObligatoryLeafHeuristic<'_> {
323 #[inline]
324 fn outside_estimate(&self, left: StateId, span: &Span) -> f64 {
325 self.estimate(left, span)
326 }
327
328 #[inline]
329 fn admits(&self, left: StateId, span: &Span) -> bool {
330 !self.prunes(left, span)
331 }
332
333 #[inline]
334 fn estimate_if_admitted(&self, left: StateId, span: &Span) -> Option<f64> {
335 (!self.prunes(left, span)).then_some(self.pass)
336 }
337
338 #[inline]
339 fn memoize_admission(&self) -> bool {
340 true
341 }
342
343 #[inline]
344 fn estimate_after_admission(&self, _left: StateId, _span: &Span) -> f64 {
345 self.pass
346 }
347}
348
349impl IntersectionHeuristic<InvHom<'_, StringDecompositionAutomaton>>
350 for ObligatoryLeafHeuristic<'_>
351{
352 #[inline]
353 fn outside_estimate(&self, left: StateId, span: &Span) -> f64 {
354 self.estimate(left, span)
356 }
357
358 #[inline]
359 fn admits(&self, left: StateId, span: &Span) -> bool {
360 !self.prunes(left, span)
361 }
362
363 #[inline]
364 fn estimate_if_admitted(&self, left: StateId, span: &Span) -> Option<f64> {
365 (!self.prunes(left, span)).then_some(self.pass)
366 }
367
368 #[inline]
369 fn memoize_admission(&self) -> bool {
370 true
371 }
372
373 #[inline]
374 fn estimate_after_admission(&self, _left: StateId, _span: &Span) -> f64 {
375 self.pass
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382 use crate::{ExplicitBuilder, LogProbabilityScorer, ProbabilityScorer};
383
384 fn worked_example() -> (ObligatoryLeafTables, StateId, StateId, StateId) {
391 let mut b = ExplicitBuilder::new();
392 let s = b.new_state();
393 let a = b.new_state();
394 let bb = b.new_state();
395 b.add_rule(Symbol(0), vec![a, bb], s);
396 b.add_rule(Symbol(1), vec![], a);
397 b.add_rule(Symbol(2), vec![], bb);
398 b.add_rule(Symbol(3), vec![], a);
399 b.add_accepting(s);
400 let grammar = b.build();
401
402 let mut hom = Homomorphism::new();
403 let v0 = hom.add_var(0);
404 let v1 = hom.add_var(1);
405 let root = hom.add_symbol(Symbol(99), vec![v0, v1]);
406 hom.add(Symbol(0), 2, root).unwrap();
407 for (gsym, word) in [(1u32, 10u32), (2, 11), (3, 12)] {
408 let leaf = hom.add_symbol(Symbol(word), vec![]);
409 hom.add(Symbol(gsym), 0, leaf).unwrap();
410 }
411
412 let tables = ObligatoryLeafTables::from_grammar(&grammar, &hom);
413 (tables, s, a, bb)
414 }
415
416 #[test]
417 fn from_grammar_matches_worked_example() {
418 let (tables, s, a, bb) = worked_example();
419 assert_eq!(
421 tables.req_right[a.index()].as_deref(),
422 Some(&[(11u32, 1u32)][..])
423 );
424 assert_eq!(tables.req_left[a.index()].as_deref(), Some(&[][..]));
425 assert_eq!(tables.req_left[bb.index()].as_deref(), Some(&[][..]));
427 assert_eq!(tables.req_right[bb.index()].as_deref(), Some(&[][..]));
428 assert_eq!(tables.req_left[s.index()].as_deref(), Some(&[][..]));
429 assert_eq!(tables.req_right[s.index()].as_deref(), Some(&[][..]));
430 }
431
432 #[test]
433 fn prunes_when_required_leaf_missing_on_side() {
434 let (tables, _s, a, _b) = worked_example();
435 let h = tables.for_sentence(&[Symbol(10), Symbol(11)], &LogProbabilityScorer);
437
438 assert_eq!(
440 IntersectionHeuristic::<StringDecompositionAutomaton>::outside_estimate(
441 &h,
442 a,
443 &Span::new(0, 1)
444 ),
445 0.0
446 );
447 assert_eq!(
449 IntersectionHeuristic::<StringDecompositionAutomaton>::outside_estimate(
450 &h,
451 a,
452 &Span::new(0, 2)
453 ),
454 f64::NEG_INFINITY
455 );
456 }
457
458 #[test]
459 fn admits_is_complement_of_prune() {
460 let (tables, _s, a, _b) = worked_example();
461 let h = tables.for_sentence(&[Symbol(10), Symbol(11)], &LogProbabilityScorer);
462 for span in [Span::new(0, 1), Span::new(0, 2), Span::new(1, 2)] {
464 let est = IntersectionHeuristic::<StringDecompositionAutomaton>::outside_estimate(
465 &h, a, &span,
466 );
467 let admitted =
468 IntersectionHeuristic::<StringDecompositionAutomaton>::admits(&h, a, &span);
469 assert_eq!(admitted, est != f64::NEG_INFINITY, "span {span:?}");
470 }
471 assert!(
473 IntersectionHeuristic::<StringDecompositionAutomaton>::admits(&h, a, &Span::new(0, 1))
474 );
475 assert!(
476 !IntersectionHeuristic::<StringDecompositionAutomaton>::admits(&h, a, &Span::new(0, 2))
477 );
478 }
479
480 #[test]
481 fn pass_prune_track_the_scorer() {
482 let (tables, _s, a, _b) = worked_example();
483 let h = tables.for_sentence(&[Symbol(10), Symbol(11)], &ProbabilityScorer);
485 assert_eq!(
486 IntersectionHeuristic::<StringDecompositionAutomaton>::outside_estimate(
487 &h,
488 a,
489 &Span::new(0, 1)
490 ),
491 1.0
492 );
493 assert_eq!(
494 IntersectionHeuristic::<StringDecompositionAutomaton>::outside_estimate(
495 &h,
496 a,
497 &Span::new(0, 2)
498 ),
499 0.0
500 );
501 }
502}