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(¤t, &mut next),
592 Modifier::Title => title_case(¤t, &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(¤t) { "an " } else { "a " });
598 }
599 next.push_str(¤t);
600 }
601 }
602 current = next;
603 }
604 out.push_str(¤t);
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}