Skip to main content

oxilean_std/string/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use oxilean_kernel::Node;
6use oxilean_kernel::{Declaration, Environment, Expr, Level, Name};
7
8use super::types::{BytePos, LineCol, Span, StringBuilder, SubstringFinder2};
9
10/// Indent every line of `text` by `n` spaces.
11///
12/// Blank lines (consisting only of whitespace) are preserved but not indented.
13pub fn indent(text: &str, n: usize) -> String {
14    let pad = " ".repeat(n);
15    text.lines()
16        .map(|line| {
17            if line.trim().is_empty() {
18                String::new()
19            } else {
20                format!("{pad}{line}")
21            }
22        })
23        .collect::<Vec<_>>()
24        .join("\n")
25}
26/// Normalise all runs of whitespace in `text` to a single space.
27///
28/// Leading and trailing whitespace is also stripped.
29pub fn normalize_whitespace(text: &str) -> String {
30    text.split_whitespace().collect::<Vec<_>>().join(" ")
31}
32/// Split `text` into a list of whitespace-delimited words.
33pub fn words(text: &str) -> Vec<&str> {
34    text.split_whitespace().collect()
35}
36/// Join `parts` with `sep` between them.
37pub fn join(parts: &[impl AsRef<str>], sep: &str) -> String {
38    parts
39        .iter()
40        .map(|s| s.as_ref())
41        .collect::<Vec<_>>()
42        .join(sep)
43}
44/// Count non-overlapping occurrences of `needle` in `haystack`.
45pub fn count_occurrences(haystack: &str, needle: &str) -> usize {
46    if needle.is_empty() {
47        return 0;
48    }
49    let mut count = 0;
50    let mut pos = 0;
51    while let Some(idx) = haystack[pos..].find(needle) {
52        count += 1;
53        pos += idx + needle.len();
54    }
55    count
56}
57/// Truncate `s` to at most `max_len` characters, appending `suffix` if cut.
58///
59/// If `s.len() <= max_len`, returns `s` unchanged.
60/// Otherwise returns `s\[..max_len - suffix.len()\] + suffix`.
61pub fn truncate(s: &str, max_len: usize, suffix: &str) -> String {
62    let chars: Vec<char> = s.chars().collect();
63    if chars.len() <= max_len {
64        s.to_string()
65    } else {
66        let suffix_chars: Vec<char> = suffix.chars().collect();
67        let take = max_len.saturating_sub(suffix_chars.len());
68        let mut result: String = chars[..take].iter().collect();
69        result.push_str(suffix);
70        result
71    }
72}
73/// Pad `s` on the right with spaces to reach at least `width` chars.
74pub fn pad_right(s: &str, width: usize) -> String {
75    let len = s.chars().count();
76    if len >= width {
77        s.to_string()
78    } else {
79        format!("{s}{}", " ".repeat(width - len))
80    }
81}
82/// Pad `s` on the left with spaces to reach at least `width` chars.
83pub fn pad_left(s: &str, width: usize) -> String {
84    let len = s.chars().count();
85    if len >= width {
86        s.to_string()
87    } else {
88        format!("{}{s}", " ".repeat(width - len))
89    }
90}
91/// Center `s` within `width` characters, padding with spaces.
92///
93/// If padding is odd, the extra space goes on the right.
94pub fn center(s: &str, width: usize) -> String {
95    let len = s.chars().count();
96    if len >= width {
97        return s.to_string();
98    }
99    let total_pad = width - len;
100    let left_pad = total_pad / 2;
101    let right_pad = total_pad - left_pad;
102    format!("{}{s}{}", " ".repeat(left_pad), " ".repeat(right_pad))
103}
104/// Convert a `camelCase` or `PascalCase` string to `snake_case`.
105#[allow(clippy::while_let_on_iterator)]
106pub fn camel_to_snake(s: &str) -> String {
107    let mut result = String::with_capacity(s.len() + 4);
108    let mut chars = s.chars().peekable();
109    while let Some(c) = chars.next() {
110        if c.is_uppercase() {
111            if !result.is_empty() {
112                result.push('_');
113            }
114            result.push(
115                c.to_lowercase()
116                    .next()
117                    .expect("to_lowercase always yields at least one char"),
118            );
119        } else {
120            result.push(c);
121        }
122    }
123    result
124}
125/// Convert a `snake_case` string to `camelCase`.
126pub fn snake_to_camel(s: &str) -> String {
127    let mut result = String::with_capacity(s.len());
128    let mut capitalise_next = false;
129    for c in s.chars() {
130        if c == '_' {
131            capitalise_next = true;
132        } else if capitalise_next {
133            result.push(
134                c.to_uppercase()
135                    .next()
136                    .expect("to_uppercase always yields at least one char"),
137            );
138            capitalise_next = false;
139        } else {
140            result.push(c);
141        }
142    }
143    result
144}
145/// Convert a `snake_case` string to `PascalCase`.
146pub fn snake_to_pascal(s: &str) -> String {
147    let camel = snake_to_camel(s);
148    let mut chars = camel.chars();
149    match chars.next() {
150        None => String::new(),
151        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
152    }
153}
154/// Check whether `s` is a valid OxiLean/Lean 4 identifier.
155///
156/// A valid identifier starts with a letter or `_`, followed by letters,
157/// digits, `_`, or `'`.
158pub fn is_valid_ident(s: &str) -> bool {
159    if s.is_empty() {
160        return false;
161    }
162    let mut chars = s.chars();
163    let first = chars
164        .next()
165        .expect("s is non-empty: checked by early return");
166    if !first.is_alphabetic() && first != '_' {
167        return false;
168    }
169    chars.all(|c| c.is_alphanumeric() || c == '_' || c == '\'')
170}
171/// Escape a string for display in error messages or pretty-printed output.
172///
173/// Non-printable characters are replaced with `\uXXXX` escapes.
174pub fn escape_for_display(s: &str) -> String {
175    let mut result = String::with_capacity(s.len());
176    for c in s.chars() {
177        match c {
178            '\n' => result.push_str("\\n"),
179            '\t' => result.push_str("\\t"),
180            '\r' => result.push_str("\\r"),
181            '\\' => result.push_str("\\\\"),
182            c if c.is_control() => {
183                result.push_str(&format!("\\u{:04X}", c as u32));
184            }
185            c => result.push(c),
186        }
187    }
188    result
189}
190/// Repeat `s` exactly `n` times.
191pub fn repeat_str(s: &str, n: usize) -> String {
192    s.repeat(n)
193}
194/// Check whether `s` starts with `prefix`, returning the remainder.
195pub fn strip_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
196    s.strip_prefix(prefix)
197}
198/// Check whether `s` ends with `suffix`, returning the body.
199pub fn strip_suffix<'a>(s: &'a str, suffix: &str) -> Option<&'a str> {
200    s.strip_suffix(suffix)
201}
202/// Trim a balanced pair of delimiters from the start and end of `s`.
203///
204/// Returns the inner string if `s` starts with `open` and ends with `close`.
205pub fn strip_delimiters<'a>(s: &'a str, open: &str, close: &str) -> Option<&'a str> {
206    let inner = s.strip_prefix(open)?.strip_suffix(close)?;
207    Some(inner)
208}
209/// Replace the first occurrence of `from` in `s` with `to`.
210pub fn replace_first(s: &str, from: &str, to: &str) -> String {
211    if let Some(pos) = s.find(from) {
212        format!("{}{}{}", &s[..pos], to, &s[pos + from.len()..])
213    } else {
214        s.to_string()
215    }
216}
217/// Split `s` at the first occurrence of `sep`.
218///
219/// Returns `(before, after)` or `(s, "")` if `sep` is not found.
220pub fn split_once_or_whole<'a>(s: &'a str, sep: &str) -> (&'a str, &'a str) {
221    s.split_once(sep).unwrap_or((s, ""))
222}
223/// Wrap `text` at `width` characters, breaking on whitespace.
224///
225/// Returns a vector of lines, each at most `width` characters wide.
226pub fn word_wrap(text: &str, width: usize) -> Vec<String> {
227    let mut lines = Vec::new();
228    let mut current_line = String::new();
229    for word in text.split_whitespace() {
230        if current_line.is_empty() {
231            current_line.push_str(word);
232        } else if current_line.len() + 1 + word.len() <= width {
233            current_line.push(' ');
234            current_line.push_str(word);
235        } else {
236            lines.push(current_line.clone());
237            current_line = word.to_string();
238        }
239    }
240    if !current_line.is_empty() {
241        lines.push(current_line);
242    }
243    lines
244}
245/// Convert a dotted name string (`"Foo.bar.Baz"`) to a path of components.
246pub fn dotted_name_to_components(name: &str) -> Vec<&str> {
247    name.split('.').collect()
248}
249/// Convert a list of components back to a dotted name string.
250pub fn components_to_dotted_name(components: &[impl AsRef<str>]) -> String {
251    join(components, ".")
252}
253/// Return the last component of a dotted name.
254///
255/// E.g. `"Foo.bar.Baz"` → `"Baz"`.
256pub fn dotted_name_last(name: &str) -> &str {
257    name.rsplit('.').next().unwrap_or(name)
258}
259/// Return the namespace prefix of a dotted name (everything before the last `.`).
260///
261/// E.g. `"Foo.bar.Baz"` → `"Foo.bar"`.
262pub fn dotted_name_namespace(name: &str) -> &str {
263    match name.rfind('.') {
264        Some(pos) => &name[..pos],
265        None => "",
266    }
267}
268/// Mangle an OxiLean name to a C/LLVM-safe identifier.
269///
270/// Dots are replaced by `__`, and non-alphanumeric characters by `_`.
271pub fn mangle_name(name: &str) -> String {
272    name.chars()
273        .map(|c| {
274            if c == '.' {
275                '_'
276            } else if c.is_alphanumeric() || c == '_' {
277                c
278            } else {
279                '_'
280            }
281        })
282        .collect()
283}
284/// Check whether a name is in a given namespace.
285///
286/// `"Nat.succ"` is in namespace `"Nat"`.
287pub fn in_namespace(name: &str, ns: &str) -> bool {
288    if ns.is_empty() {
289        return true;
290    }
291    name.starts_with(&format!("{ns}."))
292}
293impl From<StringBuilder> for String {
294    fn from(sb: StringBuilder) -> String {
295        sb.finish()
296    }
297}
298/// Convert a `BytePos` to a `LineCol` within a source string.
299///
300/// Returns `LineCol { line: 1, col: 1 }` for position 0.
301pub fn byte_pos_to_line_col(src: &str, pos: BytePos) -> LineCol {
302    let offset = pos.to_usize().min(src.len());
303    let prefix = &src[..offset];
304    let line = prefix.chars().filter(|&c| c == '\n').count() as u32 + 1;
305    let col = prefix.chars().rev().take_while(|&c| c != '\n').count() as u32 + 1;
306    LineCol::new(line, col)
307}
308/// Build a table mapping line numbers (0-indexed) to their byte start positions.
309pub fn build_line_starts(src: &str) -> Vec<BytePos> {
310    let mut starts = vec![BytePos(0)];
311    for (i, c) in src.char_indices() {
312        if c == '\n' {
313            starts.push(BytePos((i + 1) as u32));
314        }
315    }
316    starts
317}
318/// Format a list of items with a separator.
319pub fn format_list<T: std::fmt::Display>(items: &[T], sep: &str) -> String {
320    items
321        .iter()
322        .map(|x| x.to_string())
323        .collect::<Vec<_>>()
324        .join(sep)
325}
326/// Format a list of items in parentheses, comma-separated.
327pub fn format_paren_list<T: std::fmt::Display>(items: &[T]) -> String {
328    if items.is_empty() {
329        "()".to_string()
330    } else {
331        format!("({})", format_list(items, ", "))
332    }
333}
334/// Format an optional value, using `default` if absent.
335pub fn format_opt<T: std::fmt::Display>(opt: &Option<T>, default: &str) -> String {
336    match opt {
337        Some(v) => v.to_string(),
338        None => default.to_string(),
339    }
340}
341/// Add `n` spaces of indent to the first line and `continuation_indent` to the rest.
342pub fn indent_continuation(text: &str, first: usize, cont: usize) -> String {
343    let mut lines = text.lines();
344    let first_line = match lines.next() {
345        None => return String::new(),
346        Some(l) => format!("{}{l}", " ".repeat(first)),
347    };
348    let rest: Vec<String> = lines.map(|l| format!("{}{l}", " ".repeat(cont))).collect();
349    if rest.is_empty() {
350        first_line
351    } else {
352        format!("{}\n{}", first_line, rest.join("\n"))
353    }
354}
355/// Create a horizontal rule of `n` dashes.
356pub fn hr(n: usize) -> String {
357    "─".repeat(n)
358}
359/// Box-draw a title line.
360pub fn box_title(title: &str, width: usize) -> String {
361    let pad = width.saturating_sub(title.chars().count() + 4);
362    format!("┌─ {title} {}", "─".repeat(pad))
363}
364/// Format a Lean 4 `#check` command string.
365pub fn lean4_check(expr: &str) -> String {
366    format!("#check {expr}")
367}
368/// Format a Lean 4 theorem declaration stub.
369pub fn lean4_theorem_stub(name: &str, ty: &str) -> String {
370    format!("theorem {name} : {ty} := by\n  sorry")
371}
372/// Format a Lean 4 `def` stub.
373pub fn lean4_def_stub(name: &str, ty: &str, body: &str) -> String {
374    format!("def {name} : {ty} := {body}")
375}
376/// Surround `s` with backticks for Lean 4 identifier quoting.
377pub fn lean4_quote_ident(s: &str) -> String {
378    format!("`{s}`")
379}
380#[cfg(test)]
381mod tests {
382    use super::*;
383    #[test]
384    fn test_indent_basic() {
385        let result = indent("hello\nworld", 2);
386        assert_eq!(result, "  hello\n  world");
387    }
388    #[test]
389    fn test_indent_blank_line() {
390        let result = indent("hello\n\nworld", 2);
391        assert_eq!(result, "  hello\n\n  world");
392    }
393    #[test]
394    fn test_normalize_whitespace() {
395        assert_eq!(normalize_whitespace("  hello   world  "), "hello world");
396    }
397    #[test]
398    fn test_count_occurrences() {
399        assert_eq!(count_occurrences("aababab", "ab"), 3);
400        assert_eq!(count_occurrences("hello", ""), 0);
401        assert_eq!(count_occurrences("hello", "xyz"), 0);
402    }
403    #[test]
404    fn test_truncate() {
405        assert_eq!(truncate("hello world", 8, "..."), "hello...");
406        assert_eq!(truncate("hi", 10, "..."), "hi");
407    }
408    #[test]
409    fn test_pad_right() {
410        assert_eq!(pad_right("hi", 5), "hi   ");
411        assert_eq!(pad_right("hello", 3), "hello");
412    }
413    #[test]
414    fn test_pad_left() {
415        assert_eq!(pad_left("hi", 5), "   hi");
416    }
417    #[test]
418    fn test_camel_to_snake() {
419        assert_eq!(camel_to_snake("CamelCase"), "camel_case");
420        assert_eq!(camel_to_snake("helloWorld"), "hello_world");
421    }
422    #[test]
423    fn test_snake_to_camel() {
424        assert_eq!(snake_to_camel("hello_world"), "helloWorld");
425        assert_eq!(snake_to_camel("foo_bar_baz"), "fooBarBaz");
426    }
427    #[test]
428    fn test_snake_to_pascal() {
429        assert_eq!(snake_to_pascal("hello_world"), "HelloWorld");
430    }
431    #[test]
432    fn test_is_valid_ident() {
433        assert!(is_valid_ident("foo"));
434        assert!(is_valid_ident("_bar"));
435        assert!(is_valid_ident("foo'"));
436        assert!(!is_valid_ident(""));
437        assert!(!is_valid_ident("1foo"));
438        assert!(!is_valid_ident("foo-bar"));
439    }
440    #[test]
441    fn test_escape_for_display() {
442        assert_eq!(escape_for_display("hello\nworld"), "hello\\nworld");
443        assert_eq!(escape_for_display("a\\b"), "a\\\\b");
444    }
445    #[test]
446    fn test_word_wrap() {
447        let wrapped = word_wrap("hello world foo bar", 10);
448        for line in &wrapped {
449            assert!(line.len() <= 10, "line too long: {line:?}");
450        }
451    }
452    #[test]
453    fn test_string_builder() {
454        let mut sb = StringBuilder::new();
455        sb.push_str("hello");
456        sb.push(' ');
457        sb.push_str("world");
458        assert_eq!(sb.finish(), "hello world");
459    }
460    #[test]
461    fn test_string_builder_newline() {
462        let mut sb = StringBuilder::new();
463        sb.push_str("line1");
464        sb.indent();
465        sb.newline();
466        sb.push_str("line2");
467        let s = sb.finish();
468        assert!(s.contains('\n'));
469        assert!(s.contains("  line2"));
470    }
471    #[test]
472    fn test_byte_pos_span() {
473        let s = Span::from_offsets(0, 5);
474        assert_eq!(s.len(), 5);
475        assert!(s.contains(BytePos(0)));
476        assert!(s.contains(BytePos(4)));
477        assert!(!s.contains(BytePos(5)));
478    }
479    #[test]
480    fn test_span_merge() {
481        let a = Span::from_offsets(0, 5);
482        let b = Span::from_offsets(3, 10);
483        let m = a.merge(b);
484        assert_eq!(m.start.0, 0);
485        assert_eq!(m.end.0, 10);
486    }
487    #[test]
488    fn test_span_slice() {
489        let src = "hello world";
490        let sp = Span::from_offsets(6, 11);
491        assert_eq!(sp.slice(src), Some("world"));
492    }
493    #[test]
494    fn test_byte_pos_to_line_col() {
495        let src = "abc\ndef\nghi";
496        let lc = byte_pos_to_line_col(src, BytePos(4));
497        assert_eq!(lc.line, 2);
498        assert_eq!(lc.col, 1);
499    }
500    #[test]
501    fn test_dotted_name_last() {
502        assert_eq!(dotted_name_last("Foo.bar.Baz"), "Baz");
503        assert_eq!(dotted_name_last("NoDot"), "NoDot");
504    }
505    #[test]
506    fn test_dotted_name_namespace() {
507        assert_eq!(dotted_name_namespace("Foo.bar.Baz"), "Foo.bar");
508        assert_eq!(dotted_name_namespace("NoDot"), "");
509    }
510    #[test]
511    fn test_in_namespace() {
512        assert!(in_namespace("Nat.succ", "Nat"));
513        assert!(!in_namespace("Int.succ", "Nat"));
514        assert!(in_namespace("anything", ""));
515    }
516    #[test]
517    fn test_mangle_name() {
518        assert_eq!(mangle_name("Foo.bar"), "Foo_bar");
519    }
520    #[test]
521    fn test_center() {
522        let s = center("hi", 6);
523        assert_eq!(s.len(), 6);
524        assert!(s.contains("hi"));
525    }
526    #[test]
527    fn test_join() {
528        assert_eq!(join(&["a", "b", "c"], ", "), "a, b, c");
529    }
530    #[test]
531    fn test_words() {
532        assert_eq!(words("  hello   world  "), vec!["hello", "world"]);
533    }
534    #[test]
535    fn test_strip_delimiters() {
536        assert_eq!(strip_delimiters("(hello)", "(", ")"), Some("hello"));
537        assert_eq!(strip_delimiters("hello", "(", ")"), None);
538    }
539    #[test]
540    fn test_build_line_starts() {
541        let src = "ab\ncd\nef";
542        let starts = build_line_starts(src);
543        assert_eq!(starts.len(), 3);
544        assert_eq!(starts[1].0, 3);
545    }
546}
547/// Build the standard String environment declarations.
548///
549/// Registers `String`, `Char`, and basic string operations as axioms in the
550/// OxiLean kernel environment.
551pub fn build_string_env(env: &mut oxilean_kernel::Environment) -> Result<(), String> {
552    use oxilean_kernel::{BinderInfo as Bi, Declaration, Expr, Level, Name};
553    let mut add = |name: &str, ty: Expr| -> Result<(), String> {
554        match env.add(Declaration::Axiom {
555            name: Name::str(name),
556            univ_params: vec![],
557            ty,
558        }) {
559            Ok(()) | Err(_) => Ok(()),
560        }
561    };
562    let cst = |s: &str| -> Expr { Expr::Const(Name::str(s), vec![]) };
563    let app = |f: Expr, a: Expr| -> Expr { Expr::App(Node::new(f), Node::new(a)) };
564    let arr = |a: Expr, b: Expr| -> Expr {
565        Expr::Pi(Bi::Default, Name::Anonymous, Node::new(a), Node::new(b))
566    };
567    let type1 = || -> Expr { Expr::Sort(Level::succ(Level::zero())) };
568    let nat_ty = || -> Expr { cst("Nat") };
569    let bool_ty = || -> Expr { cst("Bool") };
570    let string_ty = || -> Expr { cst("String") };
571    let char_ty = || -> Expr { cst("Char") };
572    let option_of = |ty: Expr| -> Expr { app(cst("Option"), ty) };
573    add("String", type1())?;
574    add("Char", type1())?;
575    add("String.length", arr(string_ty(), nat_ty()))?;
576    add(
577        "String.append",
578        arr(string_ty(), arr(string_ty(), string_ty())),
579    )?;
580    add("String.mk", arr(app(cst("List"), char_ty()), string_ty()))?;
581    add("String.data", arr(string_ty(), app(cst("List"), char_ty())))?;
582    add("String.isEmpty", arr(string_ty(), bool_ty()))?;
583    add("String.get", arr(string_ty(), arr(nat_ty(), char_ty())))?;
584    add(
585        "String.contains",
586        arr(string_ty(), arr(char_ty(), bool_ty())),
587    )?;
588    add(
589        "String.startsWith",
590        arr(string_ty(), arr(string_ty(), bool_ty())),
591    )?;
592    add(
593        "String.endsWith",
594        arr(string_ty(), arr(string_ty(), bool_ty())),
595    )?;
596    add(
597        "String.intercalate",
598        arr(string_ty(), arr(app(cst("List"), string_ty()), string_ty())),
599    )?;
600    add(
601        "String.splitOn",
602        arr(string_ty(), arr(string_ty(), app(cst("List"), string_ty()))),
603    )?;
604    add("String.trim", arr(string_ty(), string_ty()))?;
605    add("String.toNat?", arr(string_ty(), option_of(nat_ty())))?;
606    add(
607        "String.toList",
608        arr(string_ty(), app(cst("List"), char_ty())),
609    )?;
610    add("Char.val", arr(char_ty(), nat_ty()))?;
611    add("Char.ofNat", arr(nat_ty(), char_ty()))?;
612    add("Char.isAlpha", arr(char_ty(), bool_ty()))?;
613    add("Char.isDigit", arr(char_ty(), bool_ty()))?;
614    add("Char.isAlphanum", arr(char_ty(), bool_ty()))?;
615    add("Char.isWhitespace", arr(char_ty(), bool_ty()))?;
616    add("Char.isLower", arr(char_ty(), bool_ty()))?;
617    add("Char.isUpper", arr(char_ty(), bool_ty()))?;
618    add("Char.toLower", arr(char_ty(), char_ty()))?;
619    add("Char.toUpper", arr(char_ty(), char_ty()))?;
620    Ok(())
621}
622/// Remove duplicate consecutive characters from a string.
623///
624/// E.g., `"aabbbc"` → `"abc"`.
625#[allow(dead_code)]
626pub fn deduplicate_consecutive(s: &str) -> String {
627    let mut result = String::with_capacity(s.len());
628    let mut prev: Option<char> = None;
629    for c in s.chars() {
630        if Some(c) != prev {
631            result.push(c);
632            prev = Some(c);
633        }
634    }
635    result
636}
637/// Check whether a string is a palindrome.
638#[allow(dead_code)]
639pub fn is_palindrome(s: &str) -> bool {
640    let chars: Vec<char> = s.chars().collect();
641    let rev: Vec<char> = chars.iter().rev().cloned().collect();
642    chars == rev
643}
644/// Count the number of Unicode code points in a string.
645#[allow(dead_code)]
646pub fn char_count(s: &str) -> usize {
647    s.chars().count()
648}
649/// Return the nth Unicode code point (0-indexed), or `None` if out of range.
650#[allow(dead_code)]
651pub fn nth_char(s: &str, n: usize) -> Option<char> {
652    s.chars().nth(n)
653}
654/// Reverse a string by Unicode code points.
655#[allow(dead_code)]
656pub fn reverse_str(s: &str) -> String {
657    s.chars().rev().collect()
658}
659/// Split a string into fixed-size chunks of `n` characters.
660///
661/// The last chunk may be shorter than `n`.
662#[allow(dead_code)]
663pub fn chunk_chars(s: &str, n: usize) -> Vec<String> {
664    if n == 0 {
665        return vec![];
666    }
667    let chars: Vec<char> = s.chars().collect();
668    chars.chunks(n).map(|c| c.iter().collect()).collect()
669}
670/// Check whether `s` contains only ASCII whitespace.
671#[allow(dead_code)]
672pub fn is_blank(s: &str) -> bool {
673    s.chars().all(|c| c.is_ascii_whitespace())
674}
675/// Remove comments (starting with `--`) from a line.
676///
677/// Returns the portion before the first `--`.
678#[allow(dead_code)]
679pub fn strip_line_comment(line: &str) -> &str {
680    if let Some(pos) = line.find("--") {
681        &line[..pos]
682    } else {
683        line
684    }
685}
686/// Apply a substitution map to a string, replacing all occurrences of each key with its value.
687///
688/// Keys are substituted in order; no key is substituted twice.
689#[allow(dead_code)]
690pub fn apply_substitutions(s: &str, subs: &[(&str, &str)]) -> String {
691    let mut result = s.to_string();
692    for (from, to) in subs {
693        result = result.replace(from, to);
694    }
695    result
696}
697/// Split a string by a list of delimiters.
698///
699/// Each character in `delimiters` is treated as a split point.
700#[allow(dead_code)]
701pub fn split_by_chars<'a>(s: &'a str, delimiters: &[char]) -> Vec<&'a str> {
702    s.split(|c: char| delimiters.contains(&c))
703        .filter(|s| !s.is_empty())
704        .collect()
705}
706/// Insert a separator between characters in a string.
707///
708/// E.g., `interleave("abc", '-')` → `"a-b-c"`.
709#[allow(dead_code)]
710pub fn interleave_char(s: &str, sep: char) -> String {
711    let chars: Vec<char> = s.chars().collect();
712    if chars.is_empty() {
713        return String::new();
714    }
715    let mut result = String::with_capacity(s.len() * 2);
716    for (i, c) in chars.iter().enumerate() {
717        result.push(*c);
718        if i + 1 < chars.len() {
719            result.push(sep);
720        }
721    }
722    result
723}
724/// Slugify a string: lowercase, replace spaces and punctuation with `-`.
725#[allow(dead_code)]
726pub fn slugify(s: &str) -> String {
727    s.chars()
728        .map(|c| {
729            if c.is_alphanumeric() {
730                c.to_ascii_lowercase()
731            } else {
732                '-'
733            }
734        })
735        .collect::<String>()
736        .split('-')
737        .filter(|s| !s.is_empty())
738        .collect::<Vec<_>>()
739        .join("-")
740}
741/// Title-case: capitalize the first letter of each word.
742#[allow(dead_code)]
743pub fn title_case(s: &str) -> String {
744    s.split_whitespace()
745        .map(|word| {
746            let mut chars = word.chars();
747            match chars.next() {
748                None => String::new(),
749                Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
750            }
751        })
752        .collect::<Vec<_>>()
753        .join(" ")
754}
755/// Convert a string to upper case (ASCII only for simplicity).
756#[allow(dead_code)]
757pub fn to_upper(s: &str) -> String {
758    s.to_uppercase()
759}
760/// Convert a string to lower case.
761#[allow(dead_code)]
762pub fn to_lower(s: &str) -> String {
763    s.to_lowercase()
764}
765/// Capitalize the first character of a string.
766#[allow(dead_code)]
767pub fn capitalize(s: &str) -> String {
768    let mut chars = s.chars();
769    match chars.next() {
770        None => String::new(),
771        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
772    }
773}
774/// Lowercase the first character of a string.
775#[allow(dead_code)]
776pub fn decapitalize(s: &str) -> String {
777    let mut chars = s.chars();
778    match chars.next() {
779        None => String::new(),
780        Some(c) => c.to_lowercase().collect::<String>() + chars.as_str(),
781    }
782}
783/// Check whether a string matches a glob pattern (supports `*` and `?`).
784///
785/// `*` matches any sequence of characters; `?` matches a single character.
786#[allow(dead_code)]
787pub fn glob_match(pattern: &str, s: &str) -> bool {
788    let p: Vec<char> = pattern.chars().collect();
789    let t: Vec<char> = s.chars().collect();
790    glob_helper(&p, &t)
791}
792pub fn glob_helper(p: &[char], t: &[char]) -> bool {
793    match (p.first(), t.first()) {
794        (None, None) => true,
795        (Some(&'*'), _) => glob_helper(&p[1..], t) || (!t.is_empty() && glob_helper(p, &t[1..])),
796        (Some(&'?'), Some(_)) => glob_helper(&p[1..], &t[1..]),
797        (Some(a), Some(b)) if a == b => glob_helper(&p[1..], &t[1..]),
798        _ => false,
799    }
800}
801/// Compute the Levenshtein edit distance between two strings.
802#[allow(dead_code)]
803pub fn edit_distance(a: &str, b: &str) -> usize {
804    let a: Vec<char> = a.chars().collect();
805    let b: Vec<char> = b.chars().collect();
806    let m = a.len();
807    let n = b.len();
808    let mut dp = vec![vec![0usize; n + 1]; m + 1];
809    for (i, row) in dp.iter_mut().enumerate().take(m + 1) {
810        row[0] = i;
811    }
812    for (j, val) in dp[0].iter_mut().enumerate().take(n + 1) {
813        *val = j;
814    }
815    for i in 1..=m {
816        for j in 1..=n {
817            dp[i][j] = if a[i - 1] == b[j - 1] {
818                dp[i - 1][j - 1]
819            } else {
820                1 + dp[i - 1][j].min(dp[i][j - 1]).min(dp[i - 1][j - 1])
821            };
822        }
823    }
824    dp[m][n]
825}
826/// Find the closest match from `candidates` to `query` by Levenshtein distance.
827#[allow(dead_code)]
828pub fn closest_match<'a>(query: &str, candidates: &[&'a str]) -> Option<&'a str> {
829    candidates
830        .iter()
831        .min_by_key(|&&c| edit_distance(query, c))
832        .copied()
833}
834/// A simple string formatter that supports `{name}` template substitution.
835///
836/// Substitutes `{key}` with the provided value.
837#[allow(dead_code)]
838pub fn template_format(template: &str, vars: &[(&str, &str)]) -> String {
839    let mut result = template.to_string();
840    for (key, val) in vars {
841        result = result.replace(&format!("{{{}}}", key), val);
842    }
843    result
844}
845/// Count the number of words in a string.
846#[allow(dead_code)]
847pub fn word_count(s: &str) -> usize {
848    s.split_whitespace().count()
849}
850/// Count sentences (splits on `.`, `!`, `?`).
851#[allow(dead_code)]
852pub fn sentence_count(s: &str) -> usize {
853    s.chars().filter(|&c| matches!(c, '.' | '!' | '?')).count()
854}
855/// Return true if `s` contains only ASCII digits.
856#[allow(dead_code)]
857pub fn is_all_digits(s: &str) -> bool {
858    !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
859}
860/// Return true if `s` contains only ASCII letters.
861#[allow(dead_code)]
862pub fn is_all_alpha(s: &str) -> bool {
863    !s.is_empty() && s.chars().all(|c| c.is_ascii_alphabetic())
864}
865#[cfg(test)]
866mod extra_string_tests {
867    use super::*;
868    #[test]
869    fn test_deduplicate_consecutive() {
870        assert_eq!(deduplicate_consecutive("aabbcc"), "abc");
871        assert_eq!(deduplicate_consecutive("abab"), "abab");
872    }
873    #[test]
874    fn test_is_palindrome() {
875        assert!(is_palindrome("racecar"));
876        assert!(!is_palindrome("hello"));
877        assert!(is_palindrome("a"));
878        assert!(is_palindrome(""));
879    }
880    #[test]
881    fn test_char_count() {
882        assert_eq!(char_count("hello"), 5);
883        assert_eq!(char_count(""), 0);
884    }
885    #[test]
886    fn test_nth_char() {
887        assert_eq!(nth_char("hello", 1), Some('e'));
888        assert_eq!(nth_char("hello", 10), None);
889    }
890    #[test]
891    fn test_reverse_str() {
892        assert_eq!(reverse_str("abc"), "cba");
893        assert_eq!(reverse_str(""), "");
894    }
895    #[test]
896    fn test_chunk_chars() {
897        let chunks = chunk_chars("abcdefg", 3);
898        assert_eq!(chunks, vec!["abc", "def", "g"]);
899    }
900    #[test]
901    fn test_is_blank() {
902        assert!(is_blank("   \t"));
903        assert!(!is_blank("  a "));
904    }
905    #[test]
906    fn test_strip_line_comment() {
907        assert_eq!(strip_line_comment("code -- comment"), "code ");
908        assert_eq!(strip_line_comment("no comment"), "no comment");
909    }
910    #[test]
911    fn test_apply_substitutions() {
912        let subs = &[("foo", "bar"), ("baz", "qux")];
913        assert_eq!(apply_substitutions("foo baz", subs), "bar qux");
914    }
915    #[test]
916    fn test_split_by_chars() {
917        let parts = split_by_chars("a,b;c", &[',', ';']);
918        assert_eq!(parts, vec!["a", "b", "c"]);
919    }
920    #[test]
921    fn test_interleave_char() {
922        assert_eq!(interleave_char("abc", '-'), "a-b-c");
923        assert_eq!(interleave_char("", '-'), "");
924    }
925    #[test]
926    fn test_slugify() {
927        assert_eq!(slugify("Hello World!"), "hello-world");
928    }
929    #[test]
930    fn test_title_case() {
931        assert_eq!(title_case("hello world"), "Hello World");
932    }
933    #[test]
934    fn test_capitalize() {
935        assert_eq!(capitalize("hello"), "Hello");
936        assert_eq!(capitalize(""), "");
937    }
938    #[test]
939    fn test_decapitalize() {
940        assert_eq!(decapitalize("Hello"), "hello");
941    }
942    #[test]
943    fn test_glob_match() {
944        assert!(glob_match("*.rs", "main.rs"));
945        assert!(glob_match("foo?bar", "fooXbar"));
946        assert!(!glob_match("*.rs", "main.txt"));
947        assert!(glob_match("*", "anything"));
948    }
949    #[test]
950    fn test_edit_distance() {
951        assert_eq!(edit_distance("kitten", "sitting"), 3);
952        assert_eq!(edit_distance("", "abc"), 3);
953        assert_eq!(edit_distance("abc", "abc"), 0);
954    }
955    #[test]
956    fn test_closest_match() {
957        let candidates = &["intro", "apply", "exact"];
958        let closest = closest_match("intro2", candidates);
959        assert_eq!(closest, Some("intro"));
960    }
961    #[test]
962    fn test_template_format() {
963        let t = "Hello, {name}! You are {age} years old.";
964        let result = template_format(t, &[("name", "Alice"), ("age", "30")]);
965        assert_eq!(result, "Hello, Alice! You are 30 years old.");
966    }
967    #[test]
968    fn test_word_count() {
969        assert_eq!(word_count("hello world foo"), 3);
970        assert_eq!(word_count(""), 0);
971    }
972    #[test]
973    fn test_is_all_digits() {
974        assert!(is_all_digits("12345"));
975        assert!(!is_all_digits("123a5"));
976        assert!(!is_all_digits(""));
977    }
978    #[test]
979    fn test_is_all_alpha() {
980        assert!(is_all_alpha("abc"));
981        assert!(!is_all_alpha("abc1"));
982    }
983}
984pub(super) fn str_ext2_levenshtein(a: &str, b: &str) -> usize {
985    let a: Vec<char> = a.chars().collect();
986    let b: Vec<char> = b.chars().collect();
987    let m = a.len();
988    let n = b.len();
989    let mut dp = vec![vec![0usize; n + 1]; m + 1];
990    for i in 0..=m {
991        dp[i][0] = i;
992    }
993    for j in 0..=n {
994        dp[0][j] = j;
995    }
996    for i in 1..=m {
997        for j in 1..=n {
998            dp[i][j] = if a[i - 1] == b[j - 1] {
999                dp[i - 1][j - 1]
1000            } else {
1001                1 + dp[i - 1][j].min(dp[i][j - 1]).min(dp[i - 1][j - 1])
1002            };
1003        }
1004    }
1005    dp[m][n]
1006}
1007pub fn str_ext2_kmp_failure(pattern: &[char]) -> Vec<usize> {
1008    let m = pattern.len();
1009    let mut fail = vec![0usize; m];
1010    let mut k = 0usize;
1011    for i in 1..m {
1012        while k > 0 && pattern[k] != pattern[i] {
1013            k = fail[k - 1];
1014        }
1015        if pattern[k] == pattern[i] {
1016            k += 1;
1017        }
1018        fail[i] = k;
1019    }
1020    fail
1021}
1022pub(super) fn str_ext2_kmp_search(text: &str, pattern: &str) -> Vec<usize> {
1023    let t: Vec<char> = text.chars().collect();
1024    let p: Vec<char> = pattern.chars().collect();
1025    if p.is_empty() {
1026        return vec![];
1027    }
1028    let fail = str_ext2_kmp_failure(&p);
1029    let mut positions = Vec::new();
1030    let mut q = 0usize;
1031    for (i, &tc) in t.iter().enumerate() {
1032        while q > 0 && p[q] != tc {
1033            q = fail[q - 1];
1034        }
1035        if p[q] == tc {
1036            q += 1;
1037        }
1038        if q == p.len() {
1039            positions.push(i + 1 - p.len());
1040            q = fail[q - 1];
1041        }
1042    }
1043    positions
1044}
1045pub(super) fn str_ext2_rabin_karp(
1046    text: &str,
1047    pattern: &str,
1048    base: u64,
1049    modulus: u64,
1050) -> Vec<usize> {
1051    let t: Vec<char> = text.chars().collect();
1052    let p: Vec<char> = pattern.chars().collect();
1053    let n = t.len();
1054    let m = p.len();
1055    if m == 0 || m > n {
1056        return vec![];
1057    }
1058    let mut pat_hash = 0u64;
1059    let mut win_hash = 0u64;
1060    let mut high_pow = 1u64;
1061    for i in 0..m {
1062        pat_hash = (pat_hash.wrapping_mul(base).wrapping_add(p[i] as u64)) % modulus;
1063        win_hash = (win_hash.wrapping_mul(base).wrapping_add(t[i] as u64)) % modulus;
1064        if i + 1 < m {
1065            high_pow = high_pow.wrapping_mul(base) % modulus;
1066        }
1067    }
1068    let mut positions = Vec::new();
1069    if win_hash == pat_hash && t[..m] == p[..] {
1070        positions.push(0);
1071    }
1072    for i in 1..=(n - m) {
1073        win_hash = (win_hash
1074            + modulus.wrapping_sub((t[i - 1] as u64).wrapping_mul(high_pow) % modulus))
1075        .wrapping_mul(base)
1076        .wrapping_add(t[i + m - 1] as u64)
1077            % modulus;
1078        if win_hash == pat_hash && t[i..i + m] == p[..] {
1079            positions.push(i);
1080        }
1081    }
1082    positions
1083}
1084pub fn str_ext2_suffix_array(s: &str) -> Vec<usize> {
1085    let n = s.len();
1086    let mut sa: Vec<usize> = (0..n).collect();
1087    sa.sort_by(|&a, &b| s[a..].cmp(&s[b..]));
1088    sa
1089}
1090pub fn str_ext2_lcp_array(s: &str, sa: &[usize]) -> Vec<usize> {
1091    let n = s.len();
1092    if n == 0 {
1093        return vec![];
1094    }
1095    let chars: Vec<char> = s.chars().collect();
1096    let mut rank = vec![0usize; n];
1097    for (i, &sai) in sa.iter().enumerate() {
1098        rank[sai] = i;
1099    }
1100    let mut lcp = vec![0usize; n];
1101    let mut h = 0usize;
1102    for i in 0..n {
1103        if rank[i] > 0 {
1104            let j = sa[rank[i] - 1];
1105            while i + h < n && j + h < n && chars[i + h] == chars[j + h] {
1106                h += 1;
1107            }
1108            lcp[rank[i]] = h;
1109            if h > 0 {
1110                h -= 1;
1111            }
1112        }
1113    }
1114    lcp
1115}
1116pub fn str_ext2_aho_corasick_naive(text: &str, patterns: &[&str]) -> Vec<(usize, usize)> {
1117    let mut results = Vec::new();
1118    for (pat_idx, &pattern) in patterns.iter().enumerate() {
1119        if pattern.is_empty() {
1120            continue;
1121        }
1122        let mut start = 0;
1123        while let Some(pos) = text[start..].find(pattern) {
1124            results.push((start + pos, pat_idx));
1125            start += pos + 1;
1126        }
1127    }
1128    results.sort();
1129    results
1130}
1131pub fn str_ext2_as_list_char_ty() -> Expr {
1132    use oxilean_kernel::BinderInfo;
1133    let _type1 = Expr::Sort(Level::succ(Level::zero()));
1134    Expr::Pi(
1135        BinderInfo::Default,
1136        Name::str("s"),
1137        Node::new(Expr::Const(Name::str("String"), vec![])),
1138        Node::new(Expr::App(
1139            Node::new(Expr::Const(Name::str("List"), vec![])),
1140            Node::new(Expr::Const(Name::str("Char"), vec![])),
1141        )),
1142    )
1143}
1144pub fn str_ext2_concat_assoc_ty() -> Expr {
1145    use oxilean_kernel::BinderInfo;
1146    let s = || Expr::Const(Name::str("String"), vec![]);
1147    let arr = |a: Expr, b: Expr| {
1148        Expr::Pi(
1149            BinderInfo::Default,
1150            Name::str("_"),
1151            Node::new(a),
1152            Node::new(b),
1153        )
1154    };
1155    arr(s(), arr(s(), arr(s(), s())))
1156}
1157pub fn str_ext2_concat_left_id_ty() -> Expr {
1158    use oxilean_kernel::BinderInfo;
1159    let s = Expr::Const(Name::str("String"), vec![]);
1160    Expr::Pi(
1161        BinderInfo::Default,
1162        Name::str("s"),
1163        Node::new(s.clone()),
1164        Node::new(s),
1165    )
1166}
1167pub fn str_ext2_concat_right_id_ty() -> Expr {
1168    use oxilean_kernel::BinderInfo;
1169    let s = Expr::Const(Name::str("String"), vec![]);
1170    Expr::Pi(
1171        BinderInfo::Default,
1172        Name::str("s"),
1173        Node::new(s.clone()),
1174        Node::new(s),
1175    )
1176}
1177pub fn str_ext2_length_append_ty() -> Expr {
1178    use oxilean_kernel::BinderInfo;
1179    let s = || Expr::Const(Name::str("String"), vec![]);
1180    let n = || Expr::Const(Name::str("Nat"), vec![]);
1181    let arr = |a: Expr, b: Expr| {
1182        Expr::Pi(
1183            BinderInfo::Default,
1184            Name::str("_"),
1185            Node::new(a),
1186            Node::new(b),
1187        )
1188    };
1189    arr(s(), arr(s(), n()))
1190}
1191pub fn str_ext2_length_empty_ty() -> Expr {
1192    Expr::Const(Name::str("Nat"), vec![])
1193}
1194pub fn str_ext2_dec_eq_ty() -> Expr {
1195    use oxilean_kernel::BinderInfo;
1196    let s = || Expr::Const(Name::str("String"), vec![]);
1197    let b = Expr::Const(Name::str("Bool"), vec![]);
1198    Expr::Pi(
1199        BinderInfo::Default,
1200        Name::str("_"),
1201        Node::new(s()),
1202        Node::new(Expr::Pi(
1203            BinderInfo::Default,
1204            Name::str("_"),
1205            Node::new(s()),
1206            Node::new(b),
1207        )),
1208    )
1209}
1210pub fn str_ext2_lex_lt_irrefl_ty() -> Expr {
1211    use oxilean_kernel::BinderInfo;
1212    let s = Expr::Const(Name::str("String"), vec![]);
1213    let b = Expr::Const(Name::str("Bool"), vec![]);
1214    Expr::Pi(
1215        BinderInfo::Default,
1216        Name::str("s"),
1217        Node::new(s),
1218        Node::new(b),
1219    )
1220}
1221pub fn str_ext2_lex_lt_trans_ty() -> Expr {
1222    use oxilean_kernel::BinderInfo;
1223    let s = || Expr::Const(Name::str("String"), vec![]);
1224    let b = Expr::Const(Name::str("Bool"), vec![]);
1225    let arr = |a: Expr, b: Expr| {
1226        Expr::Pi(
1227            BinderInfo::Default,
1228            Name::str("_"),
1229            Node::new(a),
1230            Node::new(b),
1231        )
1232    };
1233    arr(s(), arr(s(), arr(s(), b)))
1234}
1235pub fn str_ext2_lex_total_ty() -> Expr {
1236    use oxilean_kernel::BinderInfo;
1237    let s = || Expr::Const(Name::str("String"), vec![]);
1238    let b = Expr::Const(Name::str("Bool"), vec![]);
1239    let arr = |a: Expr, b: Expr| {
1240        Expr::Pi(
1241            BinderInfo::Default,
1242            Name::str("_"),
1243            Node::new(a),
1244            Node::new(b),
1245        )
1246    };
1247    arr(s(), arr(s(), b))
1248}
1249pub fn str_ext2_substring_ty() -> Expr {
1250    use oxilean_kernel::BinderInfo;
1251    let s = Expr::Const(Name::str("String"), vec![]);
1252    let n = || Expr::Const(Name::str("Nat"), vec![]);
1253    let arr = |a: Expr, b: Expr| {
1254        Expr::Pi(
1255            BinderInfo::Default,
1256            Name::str("_"),
1257            Node::new(a),
1258            Node::new(b),
1259        )
1260    };
1261    arr(s.clone(), arr(n(), arr(n(), s)))
1262}
1263pub fn str_ext2_slice_len_ty() -> Expr {
1264    use oxilean_kernel::BinderInfo;
1265    let n = || Expr::Const(Name::str("Nat"), vec![]);
1266    let s = Expr::Const(Name::str("String"), vec![]);
1267    let arr = |a: Expr, b: Expr| {
1268        Expr::Pi(
1269            BinderInfo::Default,
1270            Name::str("_"),
1271            Node::new(a),
1272            Node::new(b),
1273        )
1274    };
1275    arr(s, arr(n(), arr(n(), n())))
1276}
1277pub fn str_ext2_prefix_refl_ty() -> Expr {
1278    use oxilean_kernel::BinderInfo;
1279    let s = Expr::Const(Name::str("String"), vec![]);
1280    let b = Expr::Const(Name::str("Bool"), vec![]);
1281    Expr::Pi(
1282        BinderInfo::Default,
1283        Name::str("s"),
1284        Node::new(s),
1285        Node::new(b),
1286    )
1287}
1288pub fn str_ext2_prefix_trans_ty() -> Expr {
1289    use oxilean_kernel::BinderInfo;
1290    let s = || Expr::Const(Name::str("String"), vec![]);
1291    let b = Expr::Const(Name::str("Bool"), vec![]);
1292    let arr = |a: Expr, b: Expr| {
1293        Expr::Pi(
1294            BinderInfo::Default,
1295            Name::str("_"),
1296            Node::new(a),
1297            Node::new(b),
1298        )
1299    };
1300    arr(s(), arr(s(), arr(s(), b)))
1301}
1302pub fn str_ext2_suffix_refl_ty() -> Expr {
1303    use oxilean_kernel::BinderInfo;
1304    let s = Expr::Const(Name::str("String"), vec![]);
1305    let b = Expr::Const(Name::str("Bool"), vec![]);
1306    Expr::Pi(
1307        BinderInfo::Default,
1308        Name::str("s"),
1309        Node::new(s),
1310        Node::new(b),
1311    )
1312}
1313pub fn str_ext2_split_join_ty() -> Expr {
1314    use oxilean_kernel::BinderInfo;
1315    let s = || Expr::Const(Name::str("String"), vec![]);
1316    let arr = |a: Expr, b: Expr| {
1317        Expr::Pi(
1318            BinderInfo::Default,
1319            Name::str("_"),
1320            Node::new(a),
1321            Node::new(b),
1322        )
1323    };
1324    arr(s(), arr(s(), s()))
1325}
1326pub fn str_ext2_join_split_ty() -> Expr {
1327    use oxilean_kernel::BinderInfo;
1328    let s = || Expr::Const(Name::str("String"), vec![]);
1329    let lst = || {
1330        Expr::App(
1331            Node::new(Expr::Const(Name::str("List"), vec![])),
1332            Node::new(s()),
1333        )
1334    };
1335    let arr = |a: Expr, b: Expr| {
1336        Expr::Pi(
1337            BinderInfo::Default,
1338            Name::str("_"),
1339            Node::new(a),
1340            Node::new(b),
1341        )
1342    };
1343    arr(s(), arr(lst(), lst()))
1344}
1345pub fn str_ext2_trim_idempotent_ty() -> Expr {
1346    use oxilean_kernel::BinderInfo;
1347    let s = Expr::Const(Name::str("String"), vec![]);
1348    Expr::Pi(
1349        BinderInfo::Default,
1350        Name::str("s"),
1351        Node::new(s.clone()),
1352        Node::new(s),
1353    )
1354}
1355pub fn str_ext2_to_upper_idempotent_ty() -> Expr {
1356    use oxilean_kernel::BinderInfo;
1357    let s = Expr::Const(Name::str("String"), vec![]);
1358    Expr::Pi(
1359        BinderInfo::Default,
1360        Name::str("s"),
1361        Node::new(s.clone()),
1362        Node::new(s),
1363    )
1364}
1365pub fn str_ext2_to_lower_idempotent_ty() -> Expr {
1366    use oxilean_kernel::BinderInfo;
1367    let s = Expr::Const(Name::str("String"), vec![]);
1368    Expr::Pi(
1369        BinderInfo::Default,
1370        Name::str("s"),
1371        Node::new(s.clone()),
1372        Node::new(s),
1373    )
1374}
1375pub fn str_ext2_contains_refl_ty() -> Expr {
1376    use oxilean_kernel::BinderInfo;
1377    let s = Expr::Const(Name::str("String"), vec![]);
1378    let b = Expr::Const(Name::str("Bool"), vec![]);
1379    Expr::Pi(
1380        BinderInfo::Default,
1381        Name::str("s"),
1382        Node::new(s),
1383        Node::new(b),
1384    )
1385}
1386pub fn str_ext2_starts_with_refl_ty() -> Expr {
1387    use oxilean_kernel::BinderInfo;
1388    let s = Expr::Const(Name::str("String"), vec![]);
1389    let b = Expr::Const(Name::str("Bool"), vec![]);
1390    Expr::Pi(
1391        BinderInfo::Default,
1392        Name::str("s"),
1393        Node::new(s),
1394        Node::new(b),
1395    )
1396}
1397pub fn str_ext2_ends_with_refl_ty() -> Expr {
1398    use oxilean_kernel::BinderInfo;
1399    let s = Expr::Const(Name::str("String"), vec![]);
1400    let b = Expr::Const(Name::str("Bool"), vec![]);
1401    Expr::Pi(
1402        BinderInfo::Default,
1403        Name::str("s"),
1404        Node::new(s),
1405        Node::new(b),
1406    )
1407}
1408pub fn str_ext2_find_replace_ty() -> Expr {
1409    use oxilean_kernel::BinderInfo;
1410    let s = || Expr::Const(Name::str("String"), vec![]);
1411    let arr = |a: Expr, b: Expr| {
1412        Expr::Pi(
1413            BinderInfo::Default,
1414            Name::str("_"),
1415            Node::new(a),
1416            Node::new(b),
1417        )
1418    };
1419    arr(s(), arr(s(), arr(s(), s())))
1420}
1421pub fn str_ext2_replace_id_ty() -> Expr {
1422    use oxilean_kernel::BinderInfo;
1423    let s = || Expr::Const(Name::str("String"), vec![]);
1424    let arr = |a: Expr, b: Expr| {
1425        Expr::Pi(
1426            BinderInfo::Default,
1427            Name::str("_"),
1428            Node::new(a),
1429            Node::new(b),
1430        )
1431    };
1432    arr(s(), arr(s(), s()))
1433}
1434pub fn str_ext2_unicode_valid_ty() -> Expr {
1435    use oxilean_kernel::BinderInfo;
1436    let s = Expr::Const(Name::str("String"), vec![]);
1437    let b = Expr::Const(Name::str("Bool"), vec![]);
1438    Expr::Pi(
1439        BinderInfo::Default,
1440        Name::str("s"),
1441        Node::new(s),
1442        Node::new(b),
1443    )
1444}
1445pub fn str_ext2_utf8_roundtrip_ty() -> Expr {
1446    use oxilean_kernel::BinderInfo;
1447    let s = Expr::Const(Name::str("String"), vec![]);
1448    Expr::Pi(
1449        BinderInfo::Default,
1450        Name::str("s"),
1451        Node::new(s.clone()),
1452        Node::new(s),
1453    )
1454}
1455pub fn str_ext2_utf16_len_ty() -> Expr {
1456    use oxilean_kernel::BinderInfo;
1457    let s = Expr::Const(Name::str("String"), vec![]);
1458    let n = Expr::Const(Name::str("Nat"), vec![]);
1459    Expr::Pi(
1460        BinderInfo::Default,
1461        Name::str("s"),
1462        Node::new(s),
1463        Node::new(n),
1464    )
1465}
1466pub fn str_ext2_char_to_string_ty() -> Expr {
1467    use oxilean_kernel::BinderInfo;
1468    let c = Expr::Const(Name::str("Char"), vec![]);
1469    let s = Expr::Const(Name::str("String"), vec![]);
1470    Expr::Pi(
1471        BinderInfo::Default,
1472        Name::str("c"),
1473        Node::new(c),
1474        Node::new(s),
1475    )
1476}
1477pub fn str_ext2_string_to_nat_ty() -> Expr {
1478    use oxilean_kernel::BinderInfo;
1479    let s = Expr::Const(Name::str("String"), vec![]);
1480    let opt_n = Expr::App(
1481        Node::new(Expr::Const(Name::str("Option"), vec![])),
1482        Node::new(Expr::Const(Name::str("Nat"), vec![])),
1483    );
1484    Expr::Pi(
1485        BinderInfo::Default,
1486        Name::str("s"),
1487        Node::new(s),
1488        Node::new(opt_n),
1489    )
1490}
1491pub fn str_ext2_format_parse_ty() -> Expr {
1492    use oxilean_kernel::BinderInfo;
1493    let s = Expr::Const(Name::str("String"), vec![]);
1494    let opt_s = Expr::App(
1495        Node::new(Expr::Const(Name::str("Option"), vec![])),
1496        Node::new(s.clone()),
1497    );
1498    Expr::Pi(
1499        BinderInfo::Default,
1500        Name::str("s"),
1501        Node::new(s),
1502        Node::new(opt_s),
1503    )
1504}
1505pub fn str_ext2_hash_consistent_ty() -> Expr {
1506    use oxilean_kernel::BinderInfo;
1507    let s = || Expr::Const(Name::str("String"), vec![]);
1508    let n = Expr::Const(Name::str("Nat"), vec![]);
1509    let arr = |a: Expr, b: Expr| {
1510        Expr::Pi(
1511            BinderInfo::Default,
1512            Name::str("_"),
1513            Node::new(a),
1514            Node::new(b),
1515        )
1516    };
1517    arr(s(), arr(s(), n))
1518}
1519pub fn str_ext2_hash_det_ty() -> Expr {
1520    use oxilean_kernel::BinderInfo;
1521    let s = Expr::Const(Name::str("String"), vec![]);
1522    let n = Expr::Const(Name::str("Nat"), vec![]);
1523    Expr::Pi(
1524        BinderInfo::Default,
1525        Name::str("s"),
1526        Node::new(s),
1527        Node::new(n),
1528    )
1529}
1530pub fn str_ext2_kmp_correct_ty() -> Expr {
1531    use oxilean_kernel::BinderInfo;
1532    let s = || Expr::Const(Name::str("String"), vec![]);
1533    let lst = || {
1534        Expr::App(
1535            Node::new(Expr::Const(Name::str("List"), vec![])),
1536            Node::new(Expr::Const(Name::str("Nat"), vec![])),
1537        )
1538    };
1539    let arr = |a: Expr, b: Expr| {
1540        Expr::Pi(
1541            BinderInfo::Default,
1542            Name::str("_"),
1543            Node::new(a),
1544            Node::new(b),
1545        )
1546    };
1547    arr(s(), arr(s(), lst()))
1548}
1549pub fn str_ext2_rabin_karp_correct_ty() -> Expr {
1550    use oxilean_kernel::BinderInfo;
1551    let s = || Expr::Const(Name::str("String"), vec![]);
1552    let lst = || {
1553        Expr::App(
1554            Node::new(Expr::Const(Name::str("List"), vec![])),
1555            Node::new(Expr::Const(Name::str("Nat"), vec![])),
1556        )
1557    };
1558    let arr = |a: Expr, b: Expr| {
1559        Expr::Pi(
1560            BinderInfo::Default,
1561            Name::str("_"),
1562            Node::new(a),
1563            Node::new(b),
1564        )
1565    };
1566    arr(s(), arr(s(), lst()))
1567}
1568pub fn str_ext2_aho_corasick_ty() -> Expr {
1569    use oxilean_kernel::BinderInfo;
1570    let s = || Expr::Const(Name::str("String"), vec![]);
1571    let lst = || {
1572        Expr::App(
1573            Node::new(Expr::Const(Name::str("List"), vec![])),
1574            Node::new(Expr::Const(Name::str("Nat"), vec![])),
1575        )
1576    };
1577    let arr = |a: Expr, b: Expr| {
1578        Expr::Pi(
1579            BinderInfo::Default,
1580            Name::str("_"),
1581            Node::new(a),
1582            Node::new(b),
1583        )
1584    };
1585    arr(s(), lst())
1586}
1587pub fn str_ext2_suffix_array_sorted_ty() -> Expr {
1588    use oxilean_kernel::BinderInfo;
1589    let s = Expr::Const(Name::str("String"), vec![]);
1590    let lst = Expr::App(
1591        Node::new(Expr::Const(Name::str("List"), vec![])),
1592        Node::new(Expr::Const(Name::str("Nat"), vec![])),
1593    );
1594    Expr::Pi(
1595        BinderInfo::Default,
1596        Name::str("s"),
1597        Node::new(s),
1598        Node::new(lst),
1599    )
1600}
1601pub fn str_ext2_lcp_array_ty() -> Expr {
1602    use oxilean_kernel::BinderInfo;
1603    let s = Expr::Const(Name::str("String"), vec![]);
1604    let n = Expr::Const(Name::str("Nat"), vec![]);
1605    Expr::Pi(
1606        BinderInfo::Default,
1607        Name::str("s"),
1608        Node::new(s),
1609        Node::new(n),
1610    )
1611}
1612pub fn str_ext2_edit_dist_zero_ty() -> Expr {
1613    use oxilean_kernel::BinderInfo;
1614    let s = Expr::Const(Name::str("String"), vec![]);
1615    let n = Expr::Const(Name::str("Nat"), vec![]);
1616    Expr::Pi(
1617        BinderInfo::Default,
1618        Name::str("s"),
1619        Node::new(s),
1620        Node::new(n),
1621    )
1622}
1623pub fn str_ext2_edit_dist_sym_ty() -> Expr {
1624    use oxilean_kernel::BinderInfo;
1625    let s = || Expr::Const(Name::str("String"), vec![]);
1626    let n = Expr::Const(Name::str("Nat"), vec![]);
1627    let arr = |a: Expr, b: Expr| {
1628        Expr::Pi(
1629            BinderInfo::Default,
1630            Name::str("_"),
1631            Node::new(a),
1632            Node::new(b),
1633        )
1634    };
1635    arr(s(), arr(s(), n))
1636}
1637pub fn str_ext2_edit_dist_triangle_ty() -> Expr {
1638    use oxilean_kernel::BinderInfo;
1639    let s = || Expr::Const(Name::str("String"), vec![]);
1640    let b = Expr::Const(Name::str("Bool"), vec![]);
1641    let arr = |a: Expr, b: Expr| {
1642        Expr::Pi(
1643            BinderInfo::Default,
1644            Name::str("_"),
1645            Node::new(a),
1646            Node::new(b),
1647        )
1648    };
1649    arr(s(), arr(s(), arr(s(), b)))
1650}
1651/// Register all extended string axioms in the environment.
1652pub fn register_string_extended_axioms(env: &mut Environment) {
1653    let axioms: &[(&str, fn() -> Expr)] = &[
1654        ("String.Ext.AsListChar", str_ext2_as_list_char_ty),
1655        ("String.Ext.ConcatAssoc", str_ext2_concat_assoc_ty),
1656        ("String.Ext.ConcatLeftId", str_ext2_concat_left_id_ty),
1657        ("String.Ext.ConcatRightId", str_ext2_concat_right_id_ty),
1658        ("String.Ext.LengthAppend", str_ext2_length_append_ty),
1659        ("String.Ext.LengthEmpty", str_ext2_length_empty_ty),
1660        ("String.Ext.DecEq", str_ext2_dec_eq_ty),
1661        ("String.Ext.LexLtIrrefl", str_ext2_lex_lt_irrefl_ty),
1662        ("String.Ext.LexLtTrans", str_ext2_lex_lt_trans_ty),
1663        ("String.Ext.LexTotal", str_ext2_lex_total_ty),
1664        ("String.Ext.Substring", str_ext2_substring_ty),
1665        ("String.Ext.SliceLen", str_ext2_slice_len_ty),
1666        ("String.Ext.PrefixRefl", str_ext2_prefix_refl_ty),
1667        ("String.Ext.PrefixTrans", str_ext2_prefix_trans_ty),
1668        ("String.Ext.SuffixRefl", str_ext2_suffix_refl_ty),
1669        ("String.Ext.SplitJoin", str_ext2_split_join_ty),
1670        ("String.Ext.JoinSplit", str_ext2_join_split_ty),
1671        ("String.Ext.TrimIdempotent", str_ext2_trim_idempotent_ty),
1672        (
1673            "String.Ext.ToUpperIdempotent",
1674            str_ext2_to_upper_idempotent_ty,
1675        ),
1676        (
1677            "String.Ext.ToLowerIdempotent",
1678            str_ext2_to_lower_idempotent_ty,
1679        ),
1680        ("String.Ext.ContainsRefl", str_ext2_contains_refl_ty),
1681        ("String.Ext.StartsWithRefl", str_ext2_starts_with_refl_ty),
1682        ("String.Ext.EndsWithRefl", str_ext2_ends_with_refl_ty),
1683        ("String.Ext.FindReplace", str_ext2_find_replace_ty),
1684        ("String.Ext.ReplaceId", str_ext2_replace_id_ty),
1685        ("String.Ext.UnicodeValid", str_ext2_unicode_valid_ty),
1686        ("String.Ext.Utf8Roundtrip", str_ext2_utf8_roundtrip_ty),
1687        ("String.Ext.Utf16Len", str_ext2_utf16_len_ty),
1688        ("String.Ext.CharToString", str_ext2_char_to_string_ty),
1689        ("String.Ext.StringToNat", str_ext2_string_to_nat_ty),
1690        ("String.Ext.FormatParse", str_ext2_format_parse_ty),
1691        ("String.Ext.HashConsistent", str_ext2_hash_consistent_ty),
1692        ("String.Ext.HashDet", str_ext2_hash_det_ty),
1693        ("String.Ext.KmpCorrect", str_ext2_kmp_correct_ty),
1694        (
1695            "String.Ext.RabinKarpCorrect",
1696            str_ext2_rabin_karp_correct_ty,
1697        ),
1698        ("String.Ext.AhoCorasick", str_ext2_aho_corasick_ty),
1699        (
1700            "String.Ext.SuffixArraySorted",
1701            str_ext2_suffix_array_sorted_ty,
1702        ),
1703        ("String.Ext.LcpArray", str_ext2_lcp_array_ty),
1704        ("String.Ext.EditDistZero", str_ext2_edit_dist_zero_ty),
1705        ("String.Ext.EditDistSym", str_ext2_edit_dist_sym_ty),
1706        (
1707            "String.Ext.EditDistTriangle",
1708            str_ext2_edit_dist_triangle_ty,
1709        ),
1710    ];
1711    for (name, ty_fn) in axioms {
1712        let _ = env.add(Declaration::Axiom {
1713            name: Name::str(*name),
1714            univ_params: vec![],
1715            ty: ty_fn(),
1716        });
1717    }
1718}
1719/// Check string concatenation associativity.
1720pub fn str_concat_assoc_check(a: &str, b: &str, c: &str) -> bool {
1721    let left = format!("{}{}{}", a, b, c);
1722    let right = format!("{}{}{}", a, b, c);
1723    left == right
1724}
1725/// Check left identity: "" ++ s = s.
1726pub fn str_left_id_check(s: &str) -> bool {
1727    format!("{}{}", "", s) == s
1728}
1729/// Check right identity: s ++ "" = s.
1730pub fn str_right_id_check(s: &str) -> bool {
1731    format!("{}{}", s, "") == s
1732}
1733/// Check length additivity: len(s ++ t) = len(s) + len(t).
1734pub fn str_length_additive(s: &str, t: &str) -> bool {
1735    let combined = format!("{}{}", s, t);
1736    combined.chars().count() == s.chars().count() + t.chars().count()
1737}
1738/// Lexicographic less-than for strings.
1739pub fn str_lex_lt(a: &str, b: &str) -> bool {
1740    a < b
1741}
1742/// Check lexicographic irreflexivity: ¬(s < s).
1743pub fn str_lex_lt_irrefl(s: &str) -> bool {
1744    !str_lex_lt(s, s)
1745}
1746/// Check lexicographic transitivity.
1747pub fn str_lex_lt_trans(a: &str, b: &str, c: &str) -> bool {
1748    if str_lex_lt(a, b) && str_lex_lt(b, c) {
1749        str_lex_lt(a, c)
1750    } else {
1751        true
1752    }
1753}
1754/// Extract a substring by char index and length.
1755pub fn str_substring(s: &str, start: usize, len: usize) -> String {
1756    s.chars().skip(start).take(len).collect()
1757}
1758/// Check that a string starts with prefix.
1759pub fn str_starts_with(s: &str, prefix: &str) -> bool {
1760    s.starts_with(prefix)
1761}
1762/// Check that a string ends with suffix.
1763pub fn str_ends_with(s: &str, suffix: &str) -> bool {
1764    s.ends_with(suffix)
1765}
1766/// Check reflexivity of prefix: s.starts_with(s).
1767pub fn str_prefix_refl(s: &str) -> bool {
1768    s.starts_with(s)
1769}
1770/// Check transitivity of prefix relation.
1771pub fn str_prefix_trans(a: &str, b: &str, c: &str) -> bool {
1772    if b.starts_with(a) && c.starts_with(b) {
1773        c.starts_with(a)
1774    } else {
1775        true
1776    }
1777}
1778/// Split and join roundtrip: join sep (split sep s) = s.
1779pub fn str_split_join_roundtrip(s: &str, sep: &str) -> bool {
1780    if sep.is_empty() {
1781        return true;
1782    }
1783    let parts: Vec<&str> = s.split(sep).collect();
1784    let rejoined = parts.join(sep);
1785    rejoined == s
1786}
1787/// Check trim idempotency: trim(trim(s)) = trim(s).
1788pub fn str_trim_idempotent(s: &str) -> bool {
1789    let trimmed = s.trim();
1790    trimmed.trim() == trimmed
1791}
1792/// Check toUpper idempotency.
1793pub fn str_to_upper_idempotent(s: &str) -> bool {
1794    let upper = s.to_uppercase();
1795    upper.to_uppercase() == upper
1796}
1797/// Check toLower idempotency.
1798pub fn str_to_lower_idempotent(s: &str) -> bool {
1799    let lower = s.to_lowercase();
1800    lower.to_lowercase() == lower
1801}
1802/// Check contains reflexivity: s.contains(s).
1803pub fn str_contains_refl(s: &str) -> bool {
1804    s.contains(s)
1805}
1806/// Find and replace all occurrences.
1807pub fn str_find_replace(s: &str, from: &str, to: &str) -> String {
1808    if from.is_empty() {
1809        return s.to_string();
1810    }
1811    s.replace(from, to)
1812}
1813/// Check replace identity: replace(p, p, s) = s.
1814pub fn str_replace_id(s: &str, p: &str) -> bool {
1815    str_find_replace(s, p, p) == s
1816}
1817/// Check UTF-8 roundtrip: decode(encode(s)) = s.
1818pub fn str_utf8_roundtrip(s: &str) -> bool {
1819    let encoded = s.as_bytes();
1820    String::from_utf8(encoded.to_vec()).as_deref() == Ok(s)
1821}
1822/// Get UTF-16 code unit count.
1823pub fn str_utf16_len(s: &str) -> usize {
1824    s.encode_utf16().count()
1825}
1826/// Convert char to string.
1827pub fn str_char_to_string(c: char) -> String {
1828    c.to_string()
1829}
1830/// Check hash consistency: equal strings have equal hashes.
1831pub fn str_hash_consistent(a: &str, b: &str) -> bool {
1832    if a == b {
1833        use super::functions::*;
1834        use std::collections::hash_map::DefaultHasher;
1835        use std::fmt;
1836        use std::hash::{Hash, Hasher};
1837        let mut h1 = DefaultHasher::new();
1838        let mut h2 = DefaultHasher::new();
1839        a.hash(&mut h1);
1840        b.hash(&mut h2);
1841        h1.finish() == h2.finish()
1842    } else {
1843        true
1844    }
1845}
1846/// KMP pattern search (public wrapper).
1847pub fn str_kmp_search(text: &str, pattern: &str) -> Vec<usize> {
1848    str_ext2_kmp_search(text, pattern)
1849}
1850/// Rabin-Karp pattern search (public wrapper).
1851pub fn str_rabin_karp_search(text: &str, pattern: &str) -> Vec<usize> {
1852    str_ext2_rabin_karp(text, pattern, 31, 1_000_000_007)
1853}
1854/// Naive multi-pattern search (Aho-Corasick approximation).
1855pub fn str_aho_corasick_search(text: &str, patterns: &[&str]) -> Vec<(usize, usize)> {
1856    str_ext2_aho_corasick_naive(text, patterns)
1857}
1858/// Build suffix array for a string.
1859pub fn str_suffix_array(s: &str) -> Vec<usize> {
1860    str_ext2_suffix_array(s)
1861}
1862/// Build LCP array from suffix array.
1863pub fn str_lcp_array(s: &str) -> Vec<usize> {
1864    let sa = str_suffix_array(s);
1865    str_ext2_lcp_array(s, &sa)
1866}
1867/// Compute Levenshtein edit distance (public wrapper).
1868pub fn str_levenshtein(a: &str, b: &str) -> usize {
1869    str_ext2_levenshtein(a, b)
1870}
1871/// Check edit distance identity: d(s, s) = 0.
1872pub fn str_edit_dist_zero(s: &str) -> bool {
1873    str_levenshtein(s, s) == 0
1874}
1875/// Check edit distance symmetry: d(a, b) = d(b, a).
1876pub fn str_edit_dist_sym(a: &str, b: &str) -> bool {
1877    str_levenshtein(a, b) == str_levenshtein(b, a)
1878}
1879/// Check triangle inequality for edit distance.
1880pub fn str_edit_dist_triangle(a: &str, b: &str, c: &str) -> bool {
1881    str_levenshtein(a, c) <= str_levenshtein(a, b) + str_levenshtein(b, c)
1882}
1883/// Check KMP and naive search give same results.
1884pub fn str_kmp_matches_naive(text: &str, pattern: &str) -> bool {
1885    let kmp = str_kmp_search(text, pattern);
1886    let naive = SubstringFinder2::new(text, pattern).find_all();
1887    kmp == naive
1888}
1889/// Check Rabin-Karp and KMP give same results.
1890pub fn str_rabin_karp_matches_kmp(text: &str, pattern: &str) -> bool {
1891    let rk = str_rabin_karp_search(text, pattern);
1892    let kmp = str_kmp_search(text, pattern);
1893    rk == kmp
1894}
1895/// Check suffix array is sorted.
1896pub fn str_suffix_array_sorted(s: &str) -> bool {
1897    let sa = str_suffix_array(s);
1898    for i in 1..sa.len() {
1899        if s[sa[i - 1]..] > s[sa[i]..] {
1900            return false;
1901        }
1902    }
1903    true
1904}
1905/// Check suffix array has correct length.
1906pub fn str_suffix_array_len(s: &str) -> bool {
1907    str_suffix_array(s).len() == s.len()
1908}
1909/// Unicode validity: all bytes form valid UTF-8.
1910pub fn str_unicode_valid(s: &str) -> bool {
1911    std::str::from_utf8(s.as_bytes()).is_ok()
1912}
1913/// Count ASCII alphanumeric chars.
1914pub fn str_count_alnum(s: &str) -> usize {
1915    s.chars().filter(|c| c.is_alphanumeric()).count()
1916}
1917/// Count whitespace characters.
1918pub fn str_count_whitespace(s: &str) -> usize {
1919    s.chars().filter(|c| c.is_whitespace()).count()
1920}
1921/// Check decidable equality: eq(s, s) = true.
1922pub fn str_dec_eq_refl(s: &str) -> bool {
1923    #[allow(clippy::eq_op)]
1924    let result = s == s;
1925    result
1926}
1927/// Check decidable equality symmetry.
1928pub fn str_dec_eq_sym(a: &str, b: &str) -> bool {
1929    #[allow(clippy::eq_op)]
1930    let result = (a == b) == (b == a);
1931    result
1932}