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