Skip to main content

pfx_core/
words.rs

1use std::collections::{BTreeMap, HashSet};
2use std::fmt;
3
4use crate::sim::{Rng, mix64};
5
6const ATTEMPTS: u64 = 32;
7const CAST_BASE_TRIES: u64 = 256;
8const CAST_TRIES_PER_NAME: u64 = 32;
9const CAST_TRIES_MAX: u64 = 100_000;
10const CAST_TAG: u64 = 0x7f4a_7c15_9e37_79b9;
11const REDRAW_TAG: u64 = 0x2545_f491_4f6c_dd1d;
12const ONCE_TAG: u64 = 0x1b87_3593_cc9e_2d51;
13const STICKY_TAG: u64 = 0x85eb_ca6b_c2b2_ae35;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum ParseReason {
17    UnclosedBrace,
18    StrayCloseBrace,
19    EmptySlot,
20    BadSlotName,
21}
22
23impl fmt::Display for ParseReason {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        f.write_str(match self {
26            Self::UnclosedBrace => "a `{` is never closed (write `{{` for a literal brace)",
27            Self::StrayCloseBrace => "a `}` closes nothing (write `}}` for a literal brace)",
28            Self::EmptySlot => "a slot has no pool name",
29            Self::BadSlotName => "a slot name may hold only letters, digits, `_`, `-` and `.`",
30        })
31    }
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum WordsError {
36    DuplicatePool {
37        pool: String,
38    },
39    EmptyPool {
40        pool: String,
41    },
42    Parse {
43        pool: String,
44        entry: usize,
45        at: usize,
46        reason: ParseReason,
47    },
48    UnknownSlot {
49        pool: String,
50        entry: usize,
51        slot: String,
52    },
53    UnknownModifier {
54        pool: String,
55        entry: usize,
56        slot: String,
57        modifier: String,
58    },
59    Misplaced {
60        pool: String,
61        entry: usize,
62        slot: String,
63        modifier: String,
64    },
65    MissingForm {
66        pool: String,
67        entry: usize,
68        binding: String,
69        form: String,
70    },
71    Recursion {
72        cycle: Vec<String>,
73    },
74    NothingToProduce {
75        pool: String,
76    },
77    UnknownPool {
78        pool: String,
79    },
80    Exhausted {
81        pool: String,
82    },
83    NotEnoughDistinct {
84        pool: String,
85        wanted: usize,
86        got: usize,
87    },
88    NoSuchMember {
89        index: usize,
90        len: usize,
91    },
92    MissingContext {
93        name: String,
94    },
95}
96
97impl fmt::Display for WordsError {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            Self::DuplicatePool { pool } => write!(f, "pool `{pool}` is defined twice"),
101            Self::EmptyPool { pool } => write!(f, "pool `{pool}` has no entries"),
102            Self::Parse {
103                pool,
104                entry,
105                at,
106                reason,
107            } => write!(f, "pool `{pool}`, entry {entry}, byte {at}: {reason}"),
108            Self::UnknownSlot { pool, entry, slot } => write!(
109                f,
110                "pool `{pool}`, entry {entry}: slot `{slot}` names no pool"
111            ),
112            Self::UnknownModifier {
113                pool,
114                entry,
115                slot,
116                modifier,
117            } => write!(
118                f,
119                "pool `{pool}`, entry {entry}, slot `{slot}`: unknown modifier `{modifier}` (cap, title, upper, lower, a, once, sticky, hide)"
120            ),
121            Self::Misplaced {
122                pool,
123                entry,
124                slot,
125                modifier,
126            } => write!(
127                f,
128                "pool `{pool}`, entry {entry}, slot `{slot}`: `{modifier}` only belongs on a slot that draws a pool, not on a recalled one"
129            ),
130            Self::MissingForm {
131                pool,
132                entry,
133                binding,
134                form,
135            } => write!(
136                f,
137                "pool `{pool}`, entry {entry}: `{binding}.{form}` needs every entry of the bound pool to hold a `{{{form}}}` slot"
138            ),
139            Self::Recursion { cycle } => {
140                write!(f, "pools recurse forever: {}", cycle.join(" -> "))
141            }
142            Self::NothingToProduce { pool } => write!(
143                f,
144                "pool `{pool}` can produce nothing once its avoid list and zero weights are applied"
145            ),
146            Self::UnknownPool { pool } => write!(f, "no pool named `{pool}`"),
147            Self::Exhausted { pool } => write!(
148                f,
149                "pool `{pool}` kept producing avoided text; its entries are nearly all avoided"
150            ),
151            Self::NotEnoughDistinct { pool, wanted, got } => write!(
152                f,
153                "pool `{pool}` gave {got} distinct results of the {wanted} asked for"
154            ),
155            Self::NoSuchMember { index, len } => {
156                write!(f, "member {index} asked for, the cast has {len}")
157            }
158            Self::MissingContext { name } => {
159                write!(f, "no context value named `${name}` was given")
160            }
161        }
162    }
163}
164
165impl std::error::Error for WordsError {}
166
167#[derive(Clone, Debug, PartialEq, Eq)]
168pub struct Entry {
169    pub text: String,
170    pub weight: u32,
171}
172
173#[derive(Clone, Debug, PartialEq, Eq)]
174pub struct PoolSpec {
175    pub name: String,
176    pub entries: Vec<Entry>,
177    pub avoid: Vec<String>,
178}
179
180impl PoolSpec {
181    #[must_use]
182    pub fn new<S: AsRef<str>>(name: &str, entries: impl IntoIterator<Item = S>) -> Self {
183        Self {
184            name: name.to_string(),
185            entries: entries
186                .into_iter()
187                .map(|text| Entry {
188                    text: text.as_ref().to_string(),
189                    weight: 1,
190                })
191                .collect(),
192            avoid: Vec::new(),
193        }
194    }
195
196    #[must_use]
197    pub fn weighted<S: AsRef<str>>(
198        name: &str,
199        entries: impl IntoIterator<Item = (S, u32)>,
200    ) -> Self {
201        Self {
202            name: name.to_string(),
203            entries: entries
204                .into_iter()
205                .map(|(text, weight)| Entry {
206                    text: text.as_ref().to_string(),
207                    weight,
208                })
209                .collect(),
210            avoid: Vec::new(),
211        }
212    }
213
214    #[must_use]
215    pub fn avoiding<S: AsRef<str>>(mut self, avoid: impl IntoIterator<Item = S>) -> Self {
216        self.avoid
217            .extend(avoid.into_iter().map(|text| text.as_ref().to_lowercase()));
218        self
219    }
220}
221
222#[derive(Clone, Copy, Debug, PartialEq, Eq)]
223enum Modifier {
224    Cap,
225    Title,
226    Upper,
227    Lower,
228    Article,
229}
230
231#[derive(Clone, Copy, Debug, PartialEq, Eq)]
232enum Draw {
233    Fresh,
234    Once,
235    Sticky,
236}
237
238#[derive(Clone, Debug)]
239enum Part {
240    Text(String),
241    Slot {
242        pool: usize,
243        mods: Vec<Modifier>,
244        bind: Option<usize>,
245        draw: Draw,
246        hide: bool,
247    },
248    Recall {
249        name: String,
250        bind: usize,
251        bound: usize,
252        form: Option<usize>,
253        mods: Vec<Modifier>,
254    },
255    Context {
256        name: String,
257        mods: Vec<Modifier>,
258    },
259}
260
261#[derive(Clone, Debug)]
262struct Compiled {
263    parts: Vec<Part>,
264    literal: bool,
265    binds: usize,
266    source: usize,
267}
268
269#[derive(Clone, Debug)]
270struct Bound {
271    text: String,
272    forms: Vec<(usize, String)>,
273}
274
275struct Run<'a> {
276    base: &'a Rng,
277    trace: Trace,
278    context: &'a [(&'a str, &'a str)],
279}
280
281struct Used {
282    text: HashSet<String>,
283    parts: HashSet<(usize, String)>,
284}
285
286struct Trace {
287    watch: Vec<bool>,
288    any: bool,
289    seen: Vec<(usize, String)>,
290}
291
292impl Trace {
293    fn none() -> Self {
294        Self {
295            watch: Vec::new(),
296            any: false,
297            seen: Vec::new(),
298        }
299    }
300
301    fn note(&mut self, pool: usize, text: &str) {
302        if self.any && self.watch[pool] {
303            self.seen.push((pool, text.to_string()));
304        }
305    }
306}
307
308#[derive(Clone, Debug)]
309struct Pool {
310    name: String,
311    hash: u64,
312    entries: Vec<Compiled>,
313    cumulative: Vec<u64>,
314    avoid: Vec<String>,
315    capacity: u64,
316}
317
318impl Pool {
319    fn total(&self) -> u64 {
320        self.cumulative.last().copied().unwrap_or(0)
321    }
322
323    fn pick(&self, rng: &mut Rng) -> usize {
324        let target = rng.below(self.total());
325        self.cumulative.partition_point(|&c| c <= target)
326    }
327
328    fn avoids(&self, text: &str) -> bool {
329        if self.avoid.is_empty() {
330            return false;
331        }
332        let lower = text.to_lowercase();
333        self.avoid.contains(&lower)
334    }
335}
336
337#[derive(Clone, Debug)]
338pub struct Words {
339    pools: Vec<Pool>,
340    index: BTreeMap<String, usize>,
341    avoid_results: Vec<String>,
342}
343
344fn name_hash(name: &str) -> u64 {
345    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
346    for &b in name.as_bytes() {
347        h ^= b as u64;
348        h = h.wrapping_mul(0x0100_0000_01b3);
349    }
350    mix64(h)
351}
352
353fn step(path: u64, a: u64, b: u64) -> u64 {
354    mix64(mix64(path ^ a).wrapping_add(b))
355}
356
357fn valid_name(name: &str) -> bool {
358    name.chars()
359        .all(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | '.'))
360}
361
362fn parse_modifier(text: &str) -> Option<Modifier> {
363    match text {
364        "cap" => Some(Modifier::Cap),
365        "title" => Some(Modifier::Title),
366        "upper" => Some(Modifier::Upper),
367        "lower" => Some(Modifier::Lower),
368        "a" | "an" => Some(Modifier::Article),
369        _ => None,
370    }
371}
372
373fn compile(
374    text: &str,
375    pool: &str,
376    entry: usize,
377    index: &BTreeMap<String, usize>,
378) -> Result<Compiled, WordsError> {
379    let parse_err = |at: usize, reason: ParseReason| WordsError::Parse {
380        pool: pool.to_string(),
381        entry,
382        at,
383        reason,
384    };
385    let b = text.as_bytes();
386    let mut parts = Vec::new();
387    let mut bound: Vec<(String, usize)> = Vec::new();
388    let mut lit = String::new();
389    let mut i = 0;
390    while i < b.len() {
391        match b[i] {
392            b'{' if b.get(i + 1) == Some(&b'{') => {
393                lit.push('{');
394                i += 2;
395            }
396            b'}' if b.get(i + 1) == Some(&b'}') => {
397                lit.push('}');
398                i += 2;
399            }
400            b'}' => return Err(parse_err(i, ParseReason::StrayCloseBrace)),
401            b'{' => {
402                let close = match text[i + 1..].find(['{', '}']) {
403                    Some(k) if b[i + 1 + k] == b'}' => i + 1 + k,
404                    _ => return Err(parse_err(i, ParseReason::UnclosedBrace)),
405                };
406                let mut fields = text[i + 1..close].split('|');
407                let head = fields.next().unwrap_or("").trim();
408                let (bind_name, slot) = match head.split_once('=') {
409                    Some((n, p)) => (Some(n.trim()), p.trim()),
410                    None => (None, head),
411                };
412                if slot.is_empty() {
413                    return Err(parse_err(i, ParseReason::EmptySlot));
414                }
415                let context = slot.strip_prefix('$');
416                if let Some(name) = context
417                    && (bind_name.is_some() || name.is_empty() || !valid_name(name))
418                {
419                    return Err(parse_err(i, ParseReason::BadSlotName));
420                }
421                if context.is_none() && !valid_name(slot)
422                    || bind_name.is_some_and(|n| n.is_empty() || n.contains('.') || !valid_name(n))
423                {
424                    return Err(parse_err(i, ParseReason::BadSlotName));
425                }
426                let unknown = |name: &str| WordsError::UnknownSlot {
427                    pool: pool.to_string(),
428                    entry,
429                    slot: name.to_string(),
430                };
431                let recall = if bind_name.is_some() || context.is_some() {
432                    None
433                } else if let Some(at) = bound.iter().rposition(|(n, _)| n == slot) {
434                    Some((at, None))
435                } else if let Some((head, form)) = slot.split_once('.') {
436                    match bound.iter().rposition(|(n, _)| n == head) {
437                        Some(at) => {
438                            Some((at, Some(*index.get(form).ok_or_else(|| unknown(slot))?)))
439                        }
440                        None => None,
441                    }
442                } else {
443                    None
444                };
445                let target = match (recall, context) {
446                    (None, None) => *index.get(slot).ok_or_else(|| unknown(slot))?,
447                    _ => 0,
448                };
449                let mut mods = Vec::new();
450                let mut draw = Draw::Fresh;
451                let mut hide = false;
452                for field in fields {
453                    let field = field.trim();
454                    let misplaced = || WordsError::Misplaced {
455                        pool: pool.to_string(),
456                        entry,
457                        slot: slot.to_string(),
458                        modifier: field.to_string(),
459                    };
460                    match field {
461                        "once" | "sticky" | "hide" => {
462                            let wanted = match field {
463                                "once" => Draw::Once,
464                                "sticky" => Draw::Sticky,
465                                _ => draw,
466                            };
467                            if recall.is_some()
468                                || context.is_some()
469                                || (field != "hide" && draw != Draw::Fresh)
470                            {
471                                return Err(misplaced());
472                            }
473                            draw = wanted;
474                            hide |= field == "hide";
475                        }
476                        _ => mods.push(parse_modifier(field).ok_or_else(|| {
477                            WordsError::UnknownModifier {
478                                pool: pool.to_string(),
479                                entry,
480                                slot: slot.to_string(),
481                                modifier: field.to_string(),
482                            }
483                        })?),
484                    }
485                }
486                if !lit.is_empty() {
487                    parts.push(Part::Text(std::mem::take(&mut lit)));
488                }
489                match recall {
490                    None if context.is_some() => parts.push(Part::Context {
491                        name: context.unwrap_or_default().to_string(),
492                        mods,
493                    }),
494                    Some((at, form)) => parts.push(Part::Recall {
495                        name: bound[at].0.clone(),
496                        bind: at,
497                        bound: bound[at].1,
498                        form,
499                        mods,
500                    }),
501                    None => {
502                        let bind = bind_name.map(|n| {
503                            bound.push((n.to_string(), target));
504                            bound.len() - 1
505                        });
506                        parts.push(Part::Slot {
507                            pool: target,
508                            mods,
509                            bind,
510                            draw,
511                            hide,
512                        });
513                    }
514                }
515                i = close + 1;
516            }
517            _ => {
518                let end = text[i..].find(['{', '}']).map_or(b.len(), |k| i + k);
519                lit.push_str(&text[i..end]);
520                i = end;
521            }
522        }
523    }
524    if !lit.is_empty() {
525        parts.push(Part::Text(lit));
526    }
527    let literal = parts.iter().all(|p| matches!(p, Part::Text(_)));
528    Ok(Compiled {
529        parts,
530        literal,
531        binds: bound.len(),
532        source: entry,
533    })
534}
535
536fn literal_text(entry: &Compiled) -> String {
537    let mut out = String::new();
538    for part in &entry.parts {
539        if let Part::Text(t) = part {
540            out.push_str(t);
541        }
542    }
543    out
544}
545
546fn cap_first(text: &str, out: &mut String) {
547    let mut chars = text.chars();
548    if let Some(first) = chars.next() {
549        out.extend(first.to_uppercase());
550        out.push_str(chars.as_str());
551    }
552}
553
554fn title_case(text: &str, out: &mut String) {
555    let mut start = true;
556    for c in text.chars() {
557        if start {
558            out.extend(c.to_uppercase());
559        } else {
560            out.push(c);
561        }
562        start = c.is_whitespace() || c == '-';
563    }
564}
565
566fn takes_an(text: &str) -> bool {
567    let lower = text.trim_start().to_lowercase();
568    let Some(first) = lower.chars().next() else {
569        return false;
570    };
571    if ["hour", "honest", "honor", "honour", "heir"]
572        .iter()
573        .any(|w| lower.starts_with(w))
574    {
575        return true;
576    }
577    if ["uni", "use", "usu", "uti", "eu", "one", "once"]
578        .iter()
579        .any(|w| lower.starts_with(w))
580    {
581        return false;
582    }
583    matches!(first, 'a' | 'e' | 'i' | 'o' | 'u')
584}
585
586fn apply(mods: &[Modifier], piece: String, out: &mut String) {
587    let mut current = piece;
588    for m in mods {
589        let mut next = String::with_capacity(current.len() + 3);
590        match m {
591            Modifier::Cap => cap_first(&current, &mut next),
592            Modifier::Title => title_case(&current, &mut next),
593            Modifier::Upper => next = current.to_uppercase(),
594            Modifier::Lower => next = current.to_lowercase(),
595            Modifier::Article => {
596                if !current.trim().is_empty() {
597                    next.push_str(if takes_an(&current) { "an " } else { "a " });
598                }
599                next.push_str(&current);
600            }
601        }
602        current = next;
603    }
604    out.push_str(&current);
605}
606
607impl Words {
608    pub fn new<S: AsRef<str>>(
609        specs: &[PoolSpec],
610        avoid_results: impl IntoIterator<Item = S>,
611    ) -> Result<Self, WordsError> {
612        let mut index = BTreeMap::new();
613        for (i, spec) in specs.iter().enumerate() {
614            if index.insert(spec.name.clone(), i).is_some() {
615                return Err(WordsError::DuplicatePool {
616                    pool: spec.name.clone(),
617                });
618            }
619        }
620        let mut pools = Vec::with_capacity(specs.len());
621        for spec in specs {
622            if spec.entries.is_empty() {
623                return Err(WordsError::EmptyPool {
624                    pool: spec.name.clone(),
625                });
626            }
627            let mut avoid: Vec<String> = spec.avoid.iter().map(|a| a.to_lowercase()).collect();
628            avoid.sort();
629            avoid.dedup();
630            let mut entries = Vec::new();
631            let mut cumulative = Vec::new();
632            let mut total = 0u64;
633            for (n, entry) in spec.entries.iter().enumerate() {
634                let compiled = compile(&entry.text, &spec.name, n, &index)?;
635                if entry.weight == 0 {
636                    continue;
637                }
638                if compiled.literal
639                    && avoid
640                        .binary_search(&literal_text(&compiled).to_lowercase())
641                        .is_ok()
642                {
643                    continue;
644                }
645                total += entry.weight as u64;
646                cumulative.push(total);
647                entries.push(compiled);
648            }
649            if entries.is_empty() {
650                return Err(WordsError::NothingToProduce {
651                    pool: spec.name.clone(),
652                });
653            }
654            pools.push(Pool {
655                name: spec.name.clone(),
656                hash: name_hash(&spec.name),
657                entries,
658                cumulative,
659                avoid,
660                capacity: 0,
661            });
662        }
663        let mut words = Self {
664            pools,
665            index,
666            avoid_results: avoid_results
667                .into_iter()
668                .map(|a| a.as_ref().to_lowercase())
669                .collect(),
670        };
671        words.check_recursion()?;
672        words.check_forms()?;
673        words.fill_capacity();
674        Ok(words)
675    }
676
677    fn check_recursion(&self) -> Result<(), WordsError> {
678        let mut state = vec![0u8; self.pools.len()];
679        let mut stack = Vec::new();
680        for start in 0..self.pools.len() {
681            self.visit(start, &mut state, &mut stack)?;
682        }
683        Ok(())
684    }
685
686    fn visit(&self, at: usize, state: &mut [u8], stack: &mut Vec<usize>) -> Result<(), WordsError> {
687        match state[at] {
688            2 => return Ok(()),
689            1 => {
690                let from = stack.iter().position(|&p| p == at).unwrap_or(0);
691                let mut cycle: Vec<String> = stack[from..]
692                    .iter()
693                    .map(|&p| self.pools[p].name.clone())
694                    .collect();
695                cycle.push(self.pools[at].name.clone());
696                return Err(WordsError::Recursion { cycle });
697            }
698            _ => {}
699        }
700        state[at] = 1;
701        stack.push(at);
702        for entry in &self.pools[at].entries {
703            for part in &entry.parts {
704                if let Part::Slot { pool, .. } = part {
705                    self.visit(*pool, state, stack)?;
706                }
707            }
708        }
709        stack.pop();
710        state[at] = 2;
711        Ok(())
712    }
713
714    fn fill_capacity(&mut self) {
715        let mut memo: Vec<Option<u64>> = vec![None; self.pools.len()];
716        for i in 0..self.pools.len() {
717            self.capacity_of(i, &mut memo);
718        }
719        for (pool, cap) in self.pools.iter_mut().zip(memo) {
720            pool.capacity = cap.unwrap_or(u64::MAX);
721        }
722    }
723
724    fn capacity_of(&self, at: usize, memo: &mut [Option<u64>]) -> u64 {
725        if let Some(c) = memo[at] {
726            return c;
727        }
728        let mut sum = 0u64;
729        for entry in &self.pools[at].entries {
730            let mut product = 1u64;
731            for part in &entry.parts {
732                if let Part::Slot { pool, .. } = part {
733                    product = product.saturating_mul(self.capacity_of(*pool, memo));
734                }
735            }
736            sum = sum.saturating_add(product);
737        }
738        memo[at] = Some(sum);
739        sum
740    }
741
742    pub fn names(&self) -> impl Iterator<Item = &str> {
743        self.index.keys().map(String::as_str)
744    }
745
746    #[must_use]
747    pub fn has_pool(&self, pool: &str) -> bool {
748        self.index.contains_key(pool)
749    }
750
751    #[must_use]
752    pub fn capacity(&self, pool: &str) -> Option<u64> {
753        self.index.get(pool).map(|&i| self.pools[i].capacity)
754    }
755
756    fn check_forms(&self) -> Result<(), WordsError> {
757        for pool in &self.pools {
758            for entry in &pool.entries {
759                for part in &entry.parts {
760                    let Part::Recall {
761                        name,
762                        bound,
763                        form: Some(form),
764                        ..
765                    } = part
766                    else {
767                        continue;
768                    };
769                    let direct = |e: &Compiled| {
770                        e.parts
771                            .iter()
772                            .any(|p| matches!(p, Part::Slot { pool, .. } if pool == form))
773                    };
774                    if !self.pools[*bound].entries.iter().all(direct) {
775                        return Err(WordsError::MissingForm {
776                            pool: pool.name.clone(),
777                            entry: entry.source,
778                            binding: name.clone(),
779                            form: self.pools[*form].name.clone(),
780                        });
781                    }
782                }
783            }
784        }
785        Ok(())
786    }
787
788    fn expand(
789        &self,
790        at: usize,
791        path: u64,
792        sticky: bool,
793        run: &mut Run,
794        out: &mut String,
795        mut forms: Option<&mut Vec<(usize, String)>>,
796    ) -> Result<(), WordsError> {
797        let pool = &self.pools[at];
798        let start = out.len();
799        let trace_mark = run.trace.seen.len();
800        let forms_mark = forms.as_ref().map_or(0, |f| f.len());
801        for attempt in 0..ATTEMPTS {
802            let attempt_path = step(path, 0, attempt);
803            let pick_path = if sticky {
804                step(STICKY_TAG, pool.hash, attempt)
805            } else {
806                attempt_path
807            };
808            let mut rng = run.base.fork(pick_path);
809            let entry = &pool.entries[pool.pick(&mut rng)];
810            let mut bound: Vec<Option<Bound>> = vec![None; entry.binds];
811            for (j, part) in entry.parts.iter().enumerate() {
812                match part {
813                    Part::Text(t) => out.push_str(t),
814                    Part::Slot {
815                        pool: child,
816                        mods,
817                        bind,
818                        draw,
819                        hide,
820                    } => {
821                        let hash = self.pools[*child].hash;
822                        let child_path = match draw {
823                            Draw::Once => step(ONCE_TAG, hash, 0),
824                            _ => step(attempt_path, j as u64 + 1, hash),
825                        };
826                        let from = out.len();
827                        let mut child_forms = Vec::new();
828                        self.expand(
829                            *child,
830                            child_path,
831                            *draw == Draw::Sticky,
832                            run,
833                            out,
834                            bind.map(|_| &mut child_forms),
835                        )?;
836                        if !mods.is_empty() {
837                            let piece = out.split_off(from);
838                            apply(mods, piece, out);
839                        }
840                        if let Some(f) = forms.as_deref_mut() {
841                            f.push((*child, out[from..].to_string()));
842                        }
843                        if let Some(b) = bind {
844                            bound[*b] = Some(Bound {
845                                text: out[from..].to_string(),
846                                forms: child_forms,
847                            });
848                        }
849                        if *hide {
850                            out.truncate(from);
851                        }
852                    }
853                    Part::Recall {
854                        bind, form, mods, ..
855                    } => {
856                        let Some(b) = &bound[*bind] else { continue };
857                        let text = match form {
858                            None => Some(&b.text),
859                            Some(f) => b.forms.iter().find(|(p, _)| p == f).map(|(_, t)| t),
860                        };
861                        if let Some(text) = text {
862                            apply(mods, text.clone(), out);
863                        }
864                    }
865                    Part::Context { name, mods } => {
866                        let Some(&(_, value)) = run.context.iter().find(|(n, _)| n == name) else {
867                            return Err(WordsError::MissingContext { name: name.clone() });
868                        };
869                        apply(mods, value.to_string(), out);
870                    }
871                }
872            }
873            if entry.literal || !pool.avoids(&out[start..]) {
874                run.trace.note(at, &out[start..]);
875                return Ok(());
876            }
877            out.truncate(start);
878            run.trace.seen.truncate(trace_mark);
879            if let Some(f) = forms.as_deref_mut() {
880                f.truncate(forms_mark);
881            }
882        }
883        Err(WordsError::Exhausted {
884            pool: pool.name.clone(),
885        })
886    }
887
888    fn lookup(&self, pool: &str) -> Result<usize, WordsError> {
889        self.index
890            .get(pool)
891            .copied()
892            .ok_or_else(|| WordsError::UnknownPool {
893                pool: pool.to_string(),
894            })
895    }
896
897    fn draw(&self, at: usize, request: u64, run: &mut Run) -> Result<String, WordsError> {
898        let root = step(self.pools[at].hash, request, 0);
899        let mut out = String::new();
900        let mark = run.trace.seen.len();
901        for attempt in 0..ATTEMPTS {
902            out.clear();
903            run.trace.seen.truncate(mark);
904            self.expand(at, step(root, attempt, 1), false, run, &mut out, None)?;
905            if self.avoid_results.is_empty() {
906                return Ok(out);
907            }
908            let lower = out.to_lowercase();
909            if !self.avoid_results.contains(&lower) {
910                return Ok(out);
911            }
912        }
913        Err(WordsError::Exhausted {
914            pool: self.pools[at].name.clone(),
915        })
916    }
917
918    pub fn generate(&self, pool: &str, rng: &Rng, request: u64) -> Result<String, WordsError> {
919        self.generate_with(pool, rng, request, &[])
920    }
921
922    pub fn generate_with(
923        &self,
924        pool: &str,
925        rng: &Rng,
926        request: u64,
927        context: &[(&str, &str)],
928    ) -> Result<String, WordsError> {
929        let at = self.lookup(pool)?;
930        let mut run = Run {
931            base: rng,
932            trace: Trace::none(),
933            context,
934        };
935        self.draw(at, request, &mut run)
936    }
937
938    pub fn cast(&self, pool: &str, rng: &Rng, count: usize) -> Result<Vec<String>, WordsError> {
939        let cast = self.cast_distinct(pool, rng, count, &[] as &[&str])?;
940        Ok(cast.members.into_iter().map(|m| m.text).collect())
941    }
942
943    fn watcher(&self, distinct: &[usize]) -> Trace {
944        let mut watch = vec![false; self.pools.len()];
945        for &d in distinct {
946            watch[d] = true;
947        }
948        Trace {
949            watch,
950            any: !distinct.is_empty(),
951            seen: Vec::new(),
952        }
953    }
954
955    fn search(
956        &self,
957        at: usize,
958        distinct: &[usize],
959        base: &Rng,
960        tries: u64,
961        first_request: u64,
962        used: &Used,
963    ) -> Result<Option<(Member, u64)>, WordsError> {
964        let mut run = Run {
965            base,
966            trace: self.watcher(distinct),
967            context: &[],
968        };
969        let mut request = first_request;
970        for _ in 0..tries {
971            run.trace.seen.clear();
972            let text = self.draw(at, request, &mut run)?;
973            request += 1;
974            if used.text.contains(&text) {
975                continue;
976            }
977            if run
978                .trace
979                .seen
980                .iter()
981                .any(|(p, t)| used.parts.contains(&(*p, t.to_lowercase())))
982            {
983                continue;
984            }
985            let parts = run
986                .trace
987                .seen
988                .iter()
989                .map(|(p, t)| (self.pools[*p].name.clone(), t.clone()))
990                .collect();
991            return Ok(Some((Member { text, parts }, request)));
992        }
993        Ok(None)
994    }
995
996    fn remember(&self, used: &mut Used, member: &Member) -> Result<(), WordsError> {
997        used.text.insert(member.text.clone());
998        for (name, text) in &member.parts {
999            used.parts.insert((self.lookup(name)?, text.to_lowercase()));
1000        }
1001        Ok(())
1002    }
1003
1004    pub fn cast_distinct<S: AsRef<str>>(
1005        &self,
1006        pool: &str,
1007        rng: &Rng,
1008        count: usize,
1009        distinct: &[S],
1010    ) -> Result<Cast, WordsError> {
1011        let at = self.lookup(pool)?;
1012        let watched = distinct
1013            .iter()
1014            .map(|d| self.lookup(d.as_ref()))
1015            .collect::<Result<Vec<_>, _>>()?;
1016        let short = |got: usize| WordsError::NotEnoughDistinct {
1017            pool: pool.to_string(),
1018            wanted: count,
1019            got,
1020        };
1021        if (count as u64) > self.pools[at].capacity
1022            || watched
1023                .iter()
1024                .any(|&w| (count as u64) > self.pools[w].capacity)
1025        {
1026            return Err(short(0));
1027        }
1028        let tries = (CAST_BASE_TRIES + CAST_TRIES_PER_NAME * count as u64).min(CAST_TRIES_MAX);
1029        let base = rng.fork(CAST_TAG);
1030        let mut used = Used {
1031            text: HashSet::with_capacity(count),
1032            parts: HashSet::new(),
1033        };
1034        let mut members = Vec::with_capacity(count);
1035        let mut request = 0u64;
1036        while members.len() < count {
1037            let Some((member, next)) = self.search(at, &watched, &base, tries, request, &used)?
1038            else {
1039                return Err(short(members.len()));
1040            };
1041            request = next;
1042            self.remember(&mut used, &member)?;
1043            members.push(member);
1044        }
1045        Ok(Cast {
1046            pool: pool.to_string(),
1047            distinct: distinct.iter().map(|d| d.as_ref().to_string()).collect(),
1048            members,
1049        })
1050    }
1051
1052    pub fn redraw<'a>(
1053        &self,
1054        cast: &'a mut Cast,
1055        rng: &Rng,
1056        index: usize,
1057        counter: u64,
1058    ) -> Result<&'a str, WordsError> {
1059        if index >= cast.members.len() {
1060            return Err(WordsError::NoSuchMember {
1061                index,
1062                len: cast.members.len(),
1063            });
1064        }
1065        let at = self.lookup(&cast.pool)?;
1066        let watched = cast
1067            .distinct
1068            .iter()
1069            .map(|d| self.lookup(d))
1070            .collect::<Result<Vec<_>, _>>()?;
1071        let mut used = Used {
1072            text: HashSet::new(),
1073            parts: HashSet::new(),
1074        };
1075        for member in &cast.members {
1076            self.remember(&mut used, member)?;
1077        }
1078        let tries =
1079            (CAST_BASE_TRIES + CAST_TRIES_PER_NAME * cast.members.len() as u64).min(CAST_TRIES_MAX);
1080        let base = rng.fork(step(REDRAW_TAG, counter, 0));
1081        let Some((member, _)) = self.search(at, &watched, &base, tries, 0, &used)? else {
1082            return Err(WordsError::NotEnoughDistinct {
1083                pool: cast.pool.clone(),
1084                wanted: cast.members.len(),
1085                got: cast.members.len() - 1,
1086            });
1087        };
1088        cast.members[index] = member;
1089        Ok(&cast.members[index].text)
1090    }
1091}
1092
1093#[derive(Clone, Debug, PartialEq, Eq)]
1094pub struct Member {
1095    pub text: String,
1096    pub parts: Vec<(String, String)>,
1097}
1098
1099#[derive(Clone, Debug, PartialEq, Eq)]
1100pub struct Cast {
1101    pub pool: String,
1102    pub distinct: Vec<String>,
1103    pub members: Vec<Member>,
1104}
1105
1106impl Cast {
1107    pub fn texts(&self) -> impl Iterator<Item = &str> {
1108        self.members.iter().map(|m| m.text.as_str())
1109    }
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114    use super::*;
1115
1116    fn sample() -> Words {
1117        Words::new(
1118            &[
1119                PoolSpec::new("first", ["Ada", "Bram", "Cleo", "Dov", "Edda"]),
1120                PoolSpec::new("last", ["Stone", "Vale", "Marsh", "Holt"]),
1121                PoolSpec::weighted("title", [("the Elder", 1), ("the Bold", 3)]),
1122                PoolSpec::new("name", ["{first} {last}", "{first} {last} {title}"]),
1123            ],
1124            [] as [&str; 0],
1125        )
1126        .unwrap()
1127    }
1128
1129    #[test]
1130    fn stream_is_frozen() {
1131        let words = sample();
1132        let rng = Rng::new(7);
1133        let got: Vec<String> = (0..6)
1134            .map(|i| words.generate("name", &rng, i).unwrap())
1135            .collect();
1136        assert_eq!(
1137            got,
1138            [
1139                "Bram Stone the Bold",
1140                "Edda Vale the Bold",
1141                "Cleo Vale",
1142                "Cleo Stone",
1143                "Ada Marsh the Bold",
1144                "Cleo Holt the Elder",
1145            ]
1146        );
1147    }
1148
1149    #[test]
1150    fn same_seed_same_names_and_calls_do_not_advance_rng() {
1151        let words = sample();
1152        let rng = Rng::new(99);
1153        let a = words.cast("name", &rng, 12).unwrap();
1154        let b = words.cast("name", &rng, 12).unwrap();
1155        assert_eq!(a, b);
1156        assert_eq!(rng, Rng::new(99));
1157    }
1158
1159    #[test]
1160    fn adding_a_pool_does_not_shift_others() {
1161        let rng = Rng::new(3);
1162        let before = sample();
1163        let mut specs = vec![
1164            PoolSpec::new("first", ["Ada", "Bram", "Cleo", "Dov", "Edda"]),
1165            PoolSpec::new("last", ["Stone", "Vale", "Marsh", "Holt"]),
1166            PoolSpec::weighted("title", [("the Elder", 1), ("the Bold", 3)]),
1167            PoolSpec::new("name", ["{first} {last}", "{first} {last} {title}"]),
1168        ];
1169        specs.insert(0, PoolSpec::new("planet", ["Mars", "Venus"]));
1170        specs.push(PoolSpec::new("zone", ["north", "south"]));
1171        let after = Words::new(&specs, [] as [&str; 0]).unwrap();
1172        for i in 0..50 {
1173            assert_eq!(
1174                before.generate("name", &rng, i).unwrap(),
1175                after.generate("name", &rng, i).unwrap()
1176            );
1177        }
1178    }
1179
1180    #[test]
1181    fn same_slot_twice_draws_independently() {
1182        let words = Words::new(
1183            &[
1184                PoolSpec::new("n", (0..50).map(|i| i.to_string())),
1185                PoolSpec::new("pair", ["{n}-{n}"]),
1186            ],
1187            [] as [&str; 0],
1188        )
1189        .unwrap();
1190        let rng = Rng::new(1);
1191        let differing = (0..40)
1192            .filter(|&i| {
1193                let s = words.generate("pair", &rng, i).unwrap();
1194                let (a, b) = s.split_once('-').unwrap();
1195                a != b
1196            })
1197            .count();
1198        assert!(differing > 30);
1199    }
1200
1201    #[test]
1202    fn cast_has_no_repeats() {
1203        let words = sample();
1204        let names = words.cast("first", &Rng::new(5), 5).unwrap();
1205        let mut sorted = names.clone();
1206        sorted.sort();
1207        sorted.dedup();
1208        assert_eq!(sorted.len(), 5);
1209        let many = words.cast("name", &Rng::new(5), 50).unwrap();
1210        let set: HashSet<_> = many.iter().collect();
1211        assert_eq!(set.len(), 50);
1212    }
1213
1214    #[test]
1215    fn cast_errors_when_pool_cannot_give_enough() {
1216        let words = sample();
1217        assert!(matches!(
1218            words.cast("first", &Rng::new(5), 6),
1219            Err(WordsError::NotEnoughDistinct { wanted: 6, .. })
1220        ));
1221        let dup = Words::new(&[PoolSpec::new("p", ["x", "x", "y"])], [] as [&str; 0]).unwrap();
1222        assert_eq!(
1223            dup.cast("p", &Rng::new(1), 3),
1224            Err(WordsError::NotEnoughDistinct {
1225                pool: "p".into(),
1226                wanted: 3,
1227                got: 2
1228            })
1229        );
1230    }
1231
1232    #[test]
1233    fn avoid_entries_and_results() {
1234        let words = Words::new(
1235            &[
1236                PoolSpec::new("a", ["Ann", "Bob", "Cy"]).avoiding(["bob"]),
1237                PoolSpec::new("t", ["{a}", "{a} Jr"]).avoiding(["Cy Jr"]),
1238            ],
1239            ["Ann Jr"],
1240        )
1241        .unwrap();
1242        let rng = Rng::new(11);
1243        for i in 0..400 {
1244            let a = words.generate("a", &rng, i).unwrap();
1245            assert!(a == "Ann" || a == "Cy");
1246            let t = words.generate("t", &rng, i).unwrap();
1247            assert!(t != "Cy Jr" && t != "Ann Jr" && !t.contains("Bob"), "{t}");
1248        }
1249    }
1250
1251    #[test]
1252    fn pool_with_everything_avoided_is_refused() {
1253        let err = Words::new(
1254            &[PoolSpec::new("a", ["x", "y"]).avoiding(["X", "y"])],
1255            [] as [&str; 0],
1256        )
1257        .unwrap_err();
1258        assert_eq!(err, WordsError::NothingToProduce { pool: "a".into() });
1259        let err = Words::new(&[PoolSpec::weighted("a", [("x", 0)])], [] as [&str; 0]).unwrap_err();
1260        assert_eq!(err, WordsError::NothingToProduce { pool: "a".into() });
1261    }
1262
1263    #[test]
1264    fn avoided_template_results_exhaust_clearly() {
1265        let words = Words::new(
1266            &[
1267                PoolSpec::new("a", ["x"]),
1268                PoolSpec::new("t", ["{a}"]).avoiding(["x"]),
1269            ],
1270            [] as [&str; 0],
1271        )
1272        .unwrap();
1273        assert_eq!(
1274            words.generate("t", &Rng::new(1), 0),
1275            Err(WordsError::Exhausted { pool: "t".into() })
1276        );
1277    }
1278
1279    #[test]
1280    fn load_errors_name_pool_and_slot() {
1281        let err = |specs: &[PoolSpec]| Words::new(specs, [] as [&str; 0]).unwrap_err();
1282        assert_eq!(
1283            err(&[PoolSpec::new("a", ["{nope}"])]),
1284            WordsError::UnknownSlot {
1285                pool: "a".into(),
1286                entry: 0,
1287                slot: "nope".into()
1288            }
1289        );
1290        assert_eq!(
1291            err(&[
1292                PoolSpec::new("a", ["x", "{b}"]),
1293                PoolSpec::new("b", ["{c}"]),
1294                PoolSpec::new("c", ["{a}"])
1295            ]),
1296            WordsError::Recursion {
1297                cycle: vec!["a".into(), "b".into(), "c".into(), "a".into()]
1298            }
1299        );
1300        assert_eq!(
1301            err(&[PoolSpec::new("a", ["x{a}"])]),
1302            WordsError::Recursion {
1303                cycle: vec!["a".into(), "a".into()]
1304            }
1305        );
1306        assert_eq!(
1307            err(&[PoolSpec::new("a", ["x"]), PoolSpec::new("a", ["y"])]),
1308            WordsError::DuplicatePool { pool: "a".into() }
1309        );
1310        assert_eq!(
1311            err(&[PoolSpec::new("a", [] as [&str; 0])]),
1312            WordsError::EmptyPool { pool: "a".into() }
1313        );
1314        assert_eq!(
1315            err(&[PoolSpec::new("a", ["x"]), PoolSpec::new("b", ["{a|shout}"])]),
1316            WordsError::UnknownModifier {
1317                pool: "b".into(),
1318                entry: 0,
1319                slot: "a".into(),
1320                modifier: "shout".into()
1321            }
1322        );
1323        let parse = |text: &str| match err(&[PoolSpec::new("a", [text])]) {
1324            WordsError::Parse { reason, at, .. } => (reason, at),
1325            other => panic!("{other}"),
1326        };
1327        assert_eq!(parse("ab {a"), (ParseReason::UnclosedBrace, 3));
1328        assert_eq!(parse("ab {a {a}"), (ParseReason::UnclosedBrace, 3));
1329        assert_eq!(parse("ab }"), (ParseReason::StrayCloseBrace, 3));
1330        assert_eq!(parse("{}"), (ParseReason::EmptySlot, 0));
1331        assert_eq!(parse("{a b}"), (ParseReason::BadSlotName, 0));
1332        let message = err(&[PoolSpec::new("a", ["{nope}"])]).to_string();
1333        assert!(message.contains("`a`") && message.contains("`nope`"));
1334    }
1335
1336    #[test]
1337    fn braces_escape_and_modifiers_apply() {
1338        let words = Words::new(
1339            &[
1340                PoolSpec::new("w", ["owl"]),
1341                PoolSpec::new("u", ["unicorn"]),
1342                PoolSpec::new("h", ["hour"]),
1343                PoolSpec::new("p", ["red fox-cub"]),
1344                PoolSpec::new(
1345                    "t",
1346                    [
1347                        "{{{w|a}}} {{}}",
1348                        "{w|a|cap}",
1349                        "{u|a}",
1350                        "{h|a}",
1351                        "{p|title}",
1352                        "{p|upper}{ w | lower }",
1353                    ],
1354                ),
1355            ],
1356            [] as [&str; 0],
1357        )
1358        .unwrap();
1359        let rng = Rng::new(2);
1360        let mut seen = HashSet::new();
1361        for i in 0..300 {
1362            seen.insert(words.generate("t", &rng, i).unwrap());
1363        }
1364        let expected: HashSet<String> = [
1365            "{an owl} {}",
1366            "An owl",
1367            "a unicorn",
1368            "an hour",
1369            "Red Fox-Cub",
1370            "RED FOX-CUBowl",
1371        ]
1372        .iter()
1373        .map(|s| s.to_string())
1374        .collect();
1375        assert_eq!(seen, expected);
1376    }
1377
1378    #[test]
1379    fn weights_are_respected() {
1380        let words = Words::new(
1381            &[PoolSpec::weighted(
1382                "p",
1383                [("rare", 1), ("common", 9), ("never", 0)],
1384            )],
1385            [] as [&str; 0],
1386        )
1387        .unwrap();
1388        let rng = Rng::new(4);
1389        let mut rare = 0;
1390        for i in 0..4000 {
1391            match words.generate("p", &rng, i).unwrap().as_str() {
1392                "rare" => rare += 1,
1393                "common" => {}
1394                other => panic!("{other}"),
1395            }
1396        }
1397        assert!((300..500).contains(&rare), "{rare}");
1398    }
1399
1400    #[test]
1401    fn unknown_pool_is_an_error() {
1402        let words = sample();
1403        assert_eq!(
1404            words.generate("nope", &Rng::new(1), 0),
1405            Err(WordsError::UnknownPool {
1406                pool: "nope".into()
1407            })
1408        );
1409        assert_eq!(words.capacity("name"), Some(5 * 4 + 5 * 4 * 2));
1410    }
1411
1412    fn people() -> Words {
1413        Words::new(
1414            &[
1415                PoolSpec::new("first", ["Ada", "Bram", "Cleo", "Dov", "Edda", "Finn"]),
1416                PoolSpec::new("last", ["Stone", "Vale", "Marsh", "Holt", "Reed", "Cole"]),
1417                PoolSpec::new("person", ["{first} {last}"]).avoiding(["Ada Stone"]),
1418                PoolSpec::new("rank", ["Scout", "Lead", "Chief"]),
1419                PoolSpec::new("guild_rank", ["Apprentice", "Journeyman", "Master"]),
1420                PoolSpec::new("army_rank", ["Private", "Sergeant", "Major"]),
1421                PoolSpec::new("ladder", ["{guild_rank}", "{army_rank}"]),
1422                PoolSpec::new("theme", ["guild", "army", "choir", "order", "crew"]),
1423                PoolSpec::new(
1424                    "memo",
1425                    [
1426                        "{m=person|hide}{m.first} reports to {m}; ask {m.last}",
1427                        "{p=person} / {p.first} / {p}",
1428                    ],
1429                ),
1430                PoolSpec::new("who", ["{person}"]),
1431                PoolSpec::new("badge", ["{theme|once} {ladder|sticky}"]),
1432            ],
1433            [] as [&str; 0],
1434        )
1435        .unwrap()
1436    }
1437
1438    #[test]
1439    fn bound_draw_feeds_every_form_consistently() {
1440        let words = people();
1441        let rng = Rng::new(21);
1442        for i in 0..200 {
1443            let memo = words.generate("memo", &rng, i).unwrap();
1444            if let Some((a, b)) = memo.split_once(" / ") {
1445                let (first, full) = b.split_once(" / ").unwrap();
1446                assert_eq!(a, full);
1447                assert!(full.starts_with(first));
1448            } else {
1449                let (head, tail) = memo.split_once(" reports to ").unwrap();
1450                let (full, ask) = tail.split_once("; ask ").unwrap();
1451                assert!(full.starts_with(head) && full.ends_with(ask), "{memo}");
1452            }
1453            assert!(!memo.contains("Ada Stone"));
1454        }
1455    }
1456
1457    #[test]
1458    fn once_is_fixed_for_the_run_and_sticky_picks_a_branch_once() {
1459        let words = people();
1460        let rng = Rng::new(8);
1461        let badges: Vec<String> = (0..60)
1462            .map(|i| words.generate("badge", &rng, i).unwrap())
1463            .collect();
1464        let theme = badges[0].split(' ').next().unwrap();
1465        assert!(badges.iter().all(|b| b.starts_with(theme)));
1466        let guild = ["Apprentice", "Journeyman", "Master"];
1467        let first_is_guild = guild.iter().any(|g| badges[0].ends_with(g));
1468        let ranks: HashSet<&str> = badges
1469            .iter()
1470            .map(|b| b.split(' ').nth(1).unwrap())
1471            .collect();
1472        assert!(ranks.len() > 1);
1473        assert!(ranks.iter().all(|r| guild.contains(r) == first_is_guild));
1474        let other = Rng::new(9);
1475        let themes: HashSet<String> = (0..20)
1476            .map(|s| {
1477                let w = words.generate("badge", &Rng::new(100 + s), 0).unwrap();
1478                w.split(' ').next().unwrap().to_string()
1479            })
1480            .collect();
1481        assert!(themes.len() > 1);
1482        assert_eq!(
1483            words.generate("badge", &other, 5).unwrap(),
1484            words.generate("badge", &other, 5).unwrap()
1485        );
1486    }
1487
1488    #[test]
1489    fn composite_avoid_reaches_a_bound_draw() {
1490        let words = people();
1491        let rng = Rng::new(2);
1492        for i in 0..2000 {
1493            assert_ne!(words.generate("who", &rng, i).unwrap(), "Ada Stone");
1494            assert!(
1495                !words
1496                    .generate("memo", &rng, i)
1497                    .unwrap()
1498                    .contains("Ada Stone")
1499            );
1500        }
1501    }
1502
1503    #[test]
1504    fn cast_keeps_every_component_unique() {
1505        let words = people();
1506        let cast = words
1507            .cast_distinct("person", &Rng::new(6), 6, &["first", "last"])
1508            .unwrap();
1509        let firsts: HashSet<&str> = cast.texts().map(|t| t.split(' ').next().unwrap()).collect();
1510        let lasts: HashSet<&str> = cast.texts().map(|t| t.split(' ').nth(1).unwrap()).collect();
1511        assert_eq!((firsts.len(), lasts.len()), (6, 6));
1512        assert!(matches!(
1513            words.cast_distinct("person", &Rng::new(6), 7, &["first"]),
1514            Err(WordsError::NotEnoughDistinct { wanted: 7, .. })
1515        ));
1516    }
1517
1518    #[test]
1519    fn redraw_is_keyed_by_counter_and_keeps_the_cast_distinct() {
1520        let words = people();
1521        let rng = Rng::new(6);
1522        let cast = words
1523            .cast_distinct("person", &rng, 3, &["first", "last"])
1524            .unwrap();
1525        let mut a = cast.clone();
1526        let mut b = cast.clone();
1527        for noise in 0..37 {
1528            words.generate("person", &rng, noise).unwrap();
1529        }
1530        let x = words.redraw(&mut a, &rng, 2, 11).unwrap().to_string();
1531        let y = words
1532            .redraw(&mut b, &Rng::new(6), 2, 11)
1533            .unwrap()
1534            .to_string();
1535        assert_eq!(x, y);
1536        assert_eq!(a, b);
1537        let old = &cast.members[2];
1538        assert_ne!(a.members[2].text, old.text);
1539        let firsts: HashSet<&str> = a.texts().map(|t| t.split(' ').next().unwrap()).collect();
1540        let lasts: HashSet<&str> = a.texts().map(|t| t.split(' ').nth(1).unwrap()).collect();
1541        assert_eq!((firsts.len(), lasts.len()), (3, 3));
1542        for i in [0, 1] {
1543            assert_eq!(a.members[i], cast.members[i]);
1544        }
1545        let mut c = cast.clone();
1546        let varied: HashSet<String> = (0..12)
1547            .map(|k| {
1548                let mut d = cast.clone();
1549                words.redraw(&mut d, &rng, 2, k).unwrap().to_string()
1550            })
1551            .collect();
1552        assert!(varied.len() > 1);
1553        assert_eq!(
1554            words.redraw(&mut c, &rng, 9, 0),
1555            Err(WordsError::NoSuchMember { index: 9, len: 3 })
1556        );
1557    }
1558
1559    #[test]
1560    fn parts_keep_their_case_and_context_fills_at_draw_time() {
1561        let words = people();
1562        let cast = words
1563            .cast_distinct("person", &Rng::new(6), 4, &["first", "last"])
1564            .unwrap();
1565        for member in &cast.members {
1566            let joined: Vec<&str> = member.parts.iter().map(|(_, t)| t.as_str()).collect();
1567            assert_eq!(joined.join(" "), member.text);
1568        }
1569        let mail = Words::new(
1570            &[
1571                PoolSpec::new("first", ["Ada", "Bram"]),
1572                PoolSpec::new(
1573                    "line",
1574                    ["{first} from {$company|upper} wrote to {$contact}."],
1575                ),
1576            ],
1577            [] as [&str; 0],
1578        )
1579        .unwrap();
1580        let rng = Rng::new(3);
1581        let plain = mail.generate_with("line", &rng, 4, &[("company", "Acme"), ("contact", "Dov")]);
1582        let first = plain
1583            .as_deref()
1584            .unwrap()
1585            .split(' ')
1586            .next()
1587            .unwrap()
1588            .to_string();
1589        assert_eq!(plain.unwrap(), format!("{first} from ACME wrote to Dov."));
1590        assert_eq!(
1591            mail.generate("line", &rng, 4),
1592            Err(WordsError::MissingContext {
1593                name: "company".into()
1594            })
1595        );
1596        assert!(Words::new(&[PoolSpec::new("x", ["{$a|once}"])], [] as [&str; 0]).is_err());
1597        assert!(Words::new(&[PoolSpec::new("x", ["{b=$a}"])], [] as [&str; 0]).is_err());
1598    }
1599
1600    #[test]
1601    fn binding_errors_are_clear() {
1602        let err = |specs: &[PoolSpec]| Words::new(specs, [] as [&str; 0]).unwrap_err();
1603        assert_eq!(
1604            err(&[
1605                PoolSpec::new("first", ["x"]),
1606                PoolSpec::new("p", ["{first}", "y"]),
1607                PoolSpec::new("t", ["{b=p} {b.first}"]),
1608            ]),
1609            WordsError::MissingForm {
1610                pool: "t".into(),
1611                entry: 0,
1612                binding: "b".into(),
1613                form: "first".into()
1614            }
1615        );
1616        assert!(matches!(
1617            err(&[
1618                PoolSpec::new("p", ["x"]),
1619                PoolSpec::new("t", ["{b=p} {b|once}"]),
1620            ]),
1621            WordsError::Misplaced { .. }
1622        ));
1623        assert!(matches!(
1624            err(&[
1625                PoolSpec::new("p", ["x"]),
1626                PoolSpec::new("t", ["{p|once|sticky}"])
1627            ]),
1628            WordsError::Misplaced { .. }
1629        ));
1630    }
1631
1632    #[test]
1633    #[ignore = "bench; run with --release"]
1634    fn bench_ten_thousand_names() {
1635        let mut specs = Vec::new();
1636        for p in 0..19 {
1637            let entries: Vec<String> = (0..100)
1638                .map(|e| {
1639                    if p > 0 && e % 10 == 0 {
1640                        format!("w{p}x{e} {{p{}}}", p - 1)
1641                    } else {
1642                        format!("w{p}x{e}")
1643                    }
1644                })
1645                .collect();
1646            specs.push(PoolSpec::new(&format!("p{p}"), entries));
1647        }
1648        let top: Vec<String> = (0..100)
1649            .map(|e| {
1650                format!(
1651                    "{{p{}|cap}} {{p{}}} {{p{}|a}}",
1652                    e % 19,
1653                    (e + 5) % 19,
1654                    (e + 11) % 19
1655                )
1656            })
1657            .collect();
1658        specs.push(PoolSpec::new("top", top));
1659        let words = Words::new(&specs, [] as [&str; 0]).unwrap();
1660        let rng = Rng::new(1);
1661        let start = std::time::Instant::now();
1662        let mut len = 0;
1663        for i in 0..10_000 {
1664            len += words.generate("top", &rng, i).unwrap().len();
1665        }
1666        let single = start.elapsed();
1667        let start = std::time::Instant::now();
1668        let cast = words.cast("top", &rng, 10_000).unwrap();
1669        let cast_time = start.elapsed();
1670        println!(
1671            "words: 10000 draws {:?} ({:?} per draw, {len} bytes); cast of {} distinct {:?}",
1672            single,
1673            single / 10_000,
1674            cast.len(),
1675            cast_time
1676        );
1677    }
1678}