ปูต้นฉบับกำลังใช้วงเวียนวัดริบบิ้นสีเขียวที่ถูกตัดเป็นช่วงบนโต๊ะสว่าง

Rust 1.98: substr_range คืนช่วง byte ของ slice โดยไม่ค้นซ้ำ

พี่โชว์Engineering

งาน parser มักได้ &str ชิ้นเล็กจากข้อความต้นฉบับ แล้วภายหลังต้องรายงานตำแหน่ง byte กลับไปยัง editor หรือ diagnostic ครับ ก่อน Rust 1.98 คนจำนวนมากคำนวณจาก pointer เองหรือค้นข้อความย่อยซ้ำ ซึ่งทั้งอ่านยากและผิดได้เมื่อข้อความเหมือนกันหลายตำแหน่ง

WHY: ข้อความเท่ากันไม่ได้แปลว่ามาจากตำแหน่งเดียวกัน

สมมติ input มีคำว่า cache สองครั้ง การใช้ find กับค่าของ slice จะได้ตำแหน่งแรก ไม่จำเป็นต้องเป็น slice ที่ parser ส่งมา str::substr_range แก้โจทย์คนละแบบ: มันตรวจว่าสตริงที่ส่งเป็น subslice ของ receiver เดียวกัน แล้วคืนช่วง byte ที่ชี้กลับตำแหน่งจริง

แนวคิดนี้เหมือนเก็บเศษริบบิ้นที่ตัดจากม้วนเดิม เราไม่ได้ดูแค่ว่าสีเหมือนกัน แต่ตรวจว่าขอบของชิ้นนั้นอยู่ตรงไหนบนม้วน หากเป็นสตริงคนละ allocation แม้เนื้อหาเหมือนกันก็ไม่ถือว่าเป็นชิ้นจากม้วนเดียวกัน

HOW: เก็บ source และ slice ให้มีอายุสัมพันธ์กัน

โค้ดตัวอย่างนี้เป็นเพียง fragment สำหรับตรวจรูปแบบ API ของ Rust 1.98 และยังไม่ได้รันบน toolchain 1.96 ของงานนี้

fn locate_token(source: &str, token: &str) -> Option<std::ops::Range<usize>> {
    source.substr_range(token)
}

fn main() {
    let source = "mint cache, warm cache";
    let token = &source[17..22];
    assert_eq!(locate_token(source, token), Some(17..22));

    let copied = token.to_owned();
    assert_eq!(locate_token(source, &copied), None);
}

ตัวอย่างนี้ทำ static review เท่านั้น เพราะเครื่องตรวจของงานนี้เป็น Rust 1.96 ขณะที่ API เสถียรใน 1.98 ประเด็นสำคัญคือ Range เป็น byte offsets ไม่ใช่จำนวนตัวอักษรที่ผู้ใช้อ่าน ดังนั้นระบบที่แสดง column ต้องแปลงตาม convention ของ editor เช่น UTF-8 bytes, Unicode scalar values หรือ UTF-16 code units

เลือกวิธีตามข้อมูลที่ถืออยู่

ถ้ามี subslice จาก source เดียวกันอยู่แล้ว substr_range เป็น default ที่ชัดที่สุด ถ้ามีเพียงข้อความที่ copy มา ต้องใช้ find หรือ parser index ที่เก็บไว้ตั้งแต่แรก ถ้าต้องรายงานตำแหน่งจำนวนมาก การให้ lexer ผลิต span พร้อม token มักประหยัดกว่าค้นหรือแปลงทุกครั้ง

อย่าสร้าง slice ด้วย byte index ที่ไม่ตรงขอบ UTF-8 เพราะ Rust จะ panic และอย่าเก็บ range แล้วแก้ source ก่อนนำกลับมาใช้ ตำแหน่งนั้นมีความหมายกับ snapshot เดิมเท่านั้น สำหรับ incremental editor ควรผูก span กับ revision หรือ document version

Recap

  • substr_range ตรวจความสัมพันธ์ของ subslice ไม่ใช่แค่ความเท่ากันของข้อความ
  • ผลลัพธ์เป็น byte range และต้องแปลงก่อนแสดง column บางรูปแบบ
  • ข้อความที่ copy ไป allocation ใหม่จะไม่ผ่าน แม้เนื้อหาเหมือนเดิม
  • งาน parser ขนาดใหญ่ยังควรสร้าง span ตั้งแต่ tokenization

ลองสำรวจจุดที่ใช้ pointer arithmetic หรือ find เพื่อกู้ตำแหน่ง แล้วแยกให้ชัดว่ามี subslice จริงหรือมีเพียงค่าข้อความครับ