use std::env;
use std::fs;
use std::path::PathBuf;
const DATA: &str = "data/CaseFolding.txt";
const MAX_RUN_LEN: u32 = 127;
const PAGE_BITS: u32 = 6;
const PAGE_MASK: u32 = (1u32 << PAGE_BITS) - 1;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={DATA}");
let folds = parse_folds(&fs::read_to_string(DATA).expect("read CaseFolding.txt"));
let runs = build_runs(&folds);
let runs = split_runs_at_page_boundary(&runs);
let runs = split_runs_at_byte_delta(&runs);
let out = emit_tables(&folds, &runs);
let out_path: PathBuf = env::var_os("OUT_DIR")
.expect("OUT_DIR is set during build")
.into();
fs::write(out_path.join("table.rs"), out).expect("write table.rs");
}
#[derive(Clone, Copy)]
struct Fold {
cp: u32,
fold: u32,
}
#[derive(Clone, Copy)]
struct Run {
start: u32,
stride: u8,
length: u8,
delta: i32,
}
fn parse_folds(text: &str) -> Vec<Fold> {
let mut out = Vec::new();
for raw in text.lines() {
let line = raw.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let mut parts = line.split(';').map(|s| s.trim());
let cp_str = parts.next().expect("code point field");
let cp = u32::from_str_radix(cp_str, 16).expect("code point is hex");
let status = parts.next().expect("status field");
let mapping = parts.next().expect("mapping field");
if status != "C" && status != "S" {
continue;
}
let targets: Vec<u32> = mapping
.split_whitespace()
.map(|s| u32::from_str_radix(s, 16).expect("mapping is hex"))
.collect();
assert_eq!(targets.len(), 1, "C/S mappings are always 1:1");
out.push(Fold {
cp,
fold: targets[0],
});
}
out.sort_by_key(|f| f.cp);
out
}
fn build_runs(folds: &[Fold]) -> Vec<Run> {
let mut runs = Vec::new();
let mut i = 0;
while i < folds.len() {
let cp0 = folds[i].cp;
let delta0 = folds[i].fold as i64 - folds[i].cp as i64;
let extend = |stride: u32| -> u32 {
let mut n: u32 = 1;
loop {
if n >= MAX_RUN_LEN {
break;
}
let j = i + n as usize;
if j >= folds.len() {
break;
}
if folds[j].cp != cp0 + n * stride {
break;
}
if (folds[j].fold as i64 - folds[j].cp as i64) != delta0 {
break;
}
n += 1;
}
n
};
let len1 = extend(1);
let len2 = extend(2);
let (stride, length) = if len2 > len1 { (2, len2) } else { (1, len1) };
runs.push(Run {
start: cp0,
stride: stride as u8,
length: length as u8,
delta: delta0 as i32,
});
i += length as usize;
}
runs
}
fn split_runs_at_page_boundary(runs: &[Run]) -> Vec<Run> {
let mut out = Vec::new();
for r in runs {
let stride = r.stride as u32;
let length = r.length as u32;
let mut i = 0u32;
while i < length {
let sub_start = r.start + i * stride;
let sub_page = sub_start >> PAGE_BITS;
let mut j = i + 1;
while j < length && (r.start + j * stride) >> PAGE_BITS == sub_page {
j += 1;
}
out.push(Run {
start: sub_start,
stride: r.stride,
length: (j - i) as u8,
delta: r.delta,
});
i = j;
}
}
out
}
fn utf8_le(cp: u32) -> u32 {
if cp < 0x80 {
cp
} else if cp < 0x800 {
(0xC0 | (cp >> 6)) | ((0x80 | (cp & 0x3F)) << 8)
} else if cp < 0x10000 {
(0xE0 | (cp >> 12)) | ((0x80 | ((cp >> 6) & 0x3F)) << 8) | ((0x80 | (cp & 0x3F)) << 16)
} else {
(0xF0 | (cp >> 18))
| ((0x80 | ((cp >> 12) & 0x3F)) << 8)
| ((0x80 | ((cp >> 6) & 0x3F)) << 16)
| ((0x80 | (cp & 0x3F)) << 24)
}
}
fn byte_delta(cp: u32, delta: i32) -> u32 {
let folded = (cp as i64 + delta as i64) as u32;
utf8_le(folded).wrapping_sub(utf8_le(cp))
}
fn split_runs_at_byte_delta(runs: &[Run]) -> Vec<Run> {
let mut out = Vec::new();
for r in runs {
let stride = r.stride as u32;
let length = r.length as u32;
let mut i = 0u32;
while i < length {
let sub_start = r.start + i * stride;
let bd = byte_delta(sub_start, r.delta);
let mut j = i + 1;
while j < length && byte_delta(r.start + j * stride, r.delta) == bd {
j += 1;
}
out.push(Run {
start: sub_start,
stride: r.stride,
length: (j - i) as u8,
delta: r.delta,
});
i = j;
}
}
out
}
fn emit_tables(folds: &[Fold], runs: &[Run]) -> String {
let n = runs.len() as u32;
let ends: Vec<u32> = runs
.iter()
.map(|r| r.start + (r.length as u32 - 1) * (r.stride as u32))
.collect();
let last_covered = *ends.last().expect("at least one run is required");
let num_pages = (last_covered >> PAGE_BITS) as usize + 1;
let num_bitmap_words = num_pages.div_ceil(64);
let mut page_bitmap = vec![0u64; num_bitmap_words];
let mut page_offset: Vec<u8> = vec![0];
let mut prev_page: Option<u32> = None;
let mut interval_count: u32 = 0;
for &end in &ends {
let page = end >> PAGE_BITS;
page_bitmap[(page as usize) / 64] |= 1u64 << (page % 64);
if Some(page) != prev_page {
if prev_page.is_some() {
page_offset.push(interval_count as u8);
}
prev_page = Some(page);
}
interval_count += 1;
}
page_offset.push(interval_count as u8);
let num_populated_pages = page_offset.len() - 1;
assert!(
interval_count <= 255,
"PAGE_OFFSET entries must fit in u8 (got {interval_count} intervals)",
);
let mut popcnt_samples = vec![0u8; num_bitmap_words + 1];
let mut cumul: u32 = 0;
for (i, &w) in page_bitmap.iter().enumerate() {
popcnt_samples[i] = cumul as u8;
cumul += w.count_ones();
}
popcnt_samples[num_bitmap_words] = cumul as u8;
assert_eq!(cumul as usize, num_populated_pages);
assert!(cumul <= 255, "POPCNT_SAMPLES must fit in u8");
let mut run_end_low = Vec::<u8>::with_capacity(runs.len());
let mut run_start_stride = Vec::<u8>::with_capacity(runs.len());
let mut max_abs_delta: i32 = 0;
for r in runs.iter() {
assert!(r.length >= 1 && r.length <= 127);
assert!(r.stride == 1 || r.stride == 2);
let start_low = (r.start & PAGE_MASK) as u8;
let end_low = ((r.start + (r.length as u32 - 1) * (r.stride as u32)) & PAGE_MASK) as u8;
let stride_bit = r.stride - 1;
max_abs_delta = max_abs_delta.max(r.delta.abs());
run_end_low.push(end_low);
run_start_stride.push(start_low | (stride_bit << 6));
}
let num_runs = run_end_low.len();
run_end_low.resize(num_runs + 8, 0xFF);
let byte_deltas: Vec<u32> = runs.iter().map(|r| byte_delta(r.start, r.delta)).collect();
let max_abs_byte_delta = byte_deltas
.iter()
.map(|&b| (b as i32).unsigned_abs())
.max()
.unwrap_or(0);
let index_deltas: Vec<u8> = runs.iter().map(|r| (r.delta & 0x7F) as u8).collect();
let index_bytes = page_bitmap.len() * 8 + popcnt_samples.len() + page_offset.len();
let total = index_bytes
+ run_end_low.len()
+ run_start_stride.len()
+ byte_deltas.len() * 4
+ index_deltas.len();
if env::var_os("CASEFOLD_BUILD_INFO").is_some() {
println!(
"cargo:warning=casefold table: {} fold entries, {} runs, {} populated pages, {} bytes total ({:.2} bits/entry), max |delta| = {}, max |byte_delta| = {}",
folds.len(),
n,
num_populated_pages,
total,
total as f64 * 8.0 / folds.len() as f64,
max_abs_delta,
max_abs_byte_delta,
);
}
let mut s = String::new();
s.push_str("// AUTO-GENERATED by build.rs from data/CaseFolding.txt. Do not edit.\n\n");
s.push_str("#[cfg(test)]\n");
s.push_str(&format!(
"pub(crate) const NUM_FOLD_ENTRIES: u32 = {};\n\n",
folds.len()
));
emit_u64_array(&mut s, "PAGE_BITMAP", &page_bitmap);
emit_u8_array(&mut s, "POPCNT_SAMPLES", &popcnt_samples);
emit_u8_array(&mut s, "PAGE_OFFSET", &page_offset);
emit_u8_array(&mut s, "RUN_END_LOW", &run_end_low);
emit_u8_array(&mut s, "RUN_START_STRIDE", &run_start_stride);
emit_u32_array(&mut s, "BYTE_DELTA", &byte_deltas);
emit_u8_array(&mut s, "INDEX_DELTA", &index_deltas);
s
}
fn emit_u64_array(s: &mut String, name: &str, data: &[u64]) {
s.push_str(&format!(
"pub(crate) static {name}: [u64; {}] = [\n",
data.len()
));
for chunk in data.chunks(4) {
s.push_str(" ");
for v in chunk {
s.push_str(&format!("0x{:016x}, ", v));
}
s.push('\n');
}
s.push_str("];\n\n");
}
fn emit_u8_array(s: &mut String, name: &str, data: &[u8]) {
s.push_str(&format!(
"pub(crate) static {name}: [u8; {}] = [\n",
data.len()
));
for chunk in data.chunks(16) {
s.push_str(" ");
for v in chunk {
s.push_str(&format!("0x{:02x}, ", v));
}
s.push('\n');
}
s.push_str("];\n\n");
}
fn emit_u32_array(s: &mut String, name: &str, data: &[u32]) {
s.push_str(&format!(
"pub(crate) static {name}: [u32; {}] = [\n",
data.len()
));
for chunk in data.chunks(8) {
s.push_str(" ");
for v in chunk {
s.push_str(&format!("0x{:08x}, ", v));
}
s.push('\n');
}
s.push_str("];\n\n");
}