2 คะแนน โดย GN⁺ 2024-11-03 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ระหว่างสร้าง ตัวไฮไลต์ไวยากรณ์ ตัวใหม่สำหรับ llamafile โดยต้องรองรับ 42 ภาษา พบว่าแม้แต่การทำ lexical analysis แบบพื้นฐานก็ยังซ่อนข้อยกเว้นเฉพาะภาษาและไวยากรณ์เก่าไว้แน่นขนัด
  • การพัฒนาใช้ C++ และ GNU gperf เพื่อเร่งการค้นหาคีย์เวิร์ด และจัดการสตริง คอมเมนต์ และคีย์เวิร์ดเป็นหลักด้วย finite state machine ที่อิง for loop และ switch
  • trigraph ของ C, ตัวจบบรรทัด u2028 และ u2029 ของ JavaScript, heredoc ของ Shell·Perl·Ruby, และ string interpolation ของ Kotlin·Scala·TypeScript·Swift เป็น กรณีที่จัดการได้ยากด้วยการทำ lexing เพียงอย่างเดียว และโผล่ซ้ำอยู่เรื่อย ๆ
  • เมื่อดูจากจำนวนบรรทัดโค้ด FORTH ใช้เพียง 125 บรรทัด ขณะที่ Ruby พุ่งไปถึง 1042 บรรทัด โดย Ruby มีทั้งตัวดำเนินการ <<, heredoc และ backquote ที่ชนกัน ทำให้ การไฮไลต์โดยไม่ parse ยากเป็นพิเศษ
  • ตัวไฮไลต์ใหม่ของ llamafile ถูกสาธิตบน Windows 10 ด้วย Meta LLaMA 3.2 3B Instruct และยังรันได้บน macOS·Linux·FreeBSD·NetBSD จึงเป็นจุดต่างจาก ollama ที่ไม่มี syntax highlighting

ที่มาของการสร้างไฮไลต์เตอร์สำหรับ llamafile

  • เพื่อสร้าง ตัวไฮไลต์ไวยากรณ์ ตัวใหม่สำหรับ llamafile ผู้เขียนใช้เวลาหนึ่งเดือนเรียนรู้ภาษาโปรแกรม 42 ภาษา
  • ภาษาที่รองรับได้แก่ Ada, Assembly, BASIC, C, C#, C++, COBOL, CSS, D, FORTH, FORTRAN, Go, Haskell, HTML, Java, JavaScript, Julia, JSON, Kotlin, ld, LISP, Lua, m4, Make, Markdown, MATLAB, Pascal, Perl, PHP, Python, R, Ruby, Rust, Scala, Shell, SQL, Swift, Tcl, TeX, TXT, TypeScript, Zig
  • รายชื่อนี้ครอบคลุมภาษาส่วนใหญ่ใน TIOBE Index แต่ Scratch ถูกตัดออกเพราะใช้บล็อกแทนข้อความ จึงไม่ใช่เป้าหมายของการไฮไลต์

วิธีพัฒนา: gperf และ finite state machine

  • คอขวดที่ใหญ่ที่สุดของตัวไฮไลต์ไวยากรณ์พื้นฐานคือช่วงที่ต้อง ตัดสินว่าโทเค็นเป็นคีย์เวิร์ดหรือไม่ ซึ่งทำให้เกิดการเปรียบเทียบสตริงซ้ำ ๆ มากขึ้น
  • ใช้ C++ และ GNU gperf เพื่อสร้าง perfect hash table
    • ตัวอย่างอินพุตของ gperf คือการกำหนด Java constant อย่าง true, false, null เป็นคีย์เวิร์ด
    • ไฟล์ C ที่ gperf สร้างขึ้นสามารถสร้างฟังก์ชันแฮชที่พิจารณาเพียงอักขระตัวเดียวเพื่อให้ค้นหาได้แบบไม่มี collision
  • การไฮไลต์ C แม้จะกำหนดคีย์เวิร์ดราว 4,000 รายการ ก็ยังประมวลผลได้ที่ 35 MB/s ด้วย gperf
  • ส่วนที่เหลือส่วนใหญ่จัดการได้ด้วย finite state machine
    • สามารถสร้างไฮไลต์เตอร์พื้นฐานได้ด้วยแค่ for loop และ switch โดยไม่ต้องใช้ flex, bison, ragel
    • หากโฟกัสที่สตริง คอมเมนต์ และคีย์เวิร์ด โดยมากก็จัดการได้ในระดับ lexing
    • แต่ถ้าต้องการไฮไลต์องค์ประกอบอย่างชื่อฟังก์ชันใน C อาจจำเป็นต้อง parse จริง
  • highlight_ada.cpp ถูกใช้เป็นตัวอย่างการพัฒนา

เดโม llamafile และสภาพแวดล้อมการใช้งาน

  • ตัวไฮไลต์ใหม่และอินเทอร์เฟซแชตบอตช่วยเพิ่มความใช้งานได้ของ llamafile และ syntax highlighting ที่ ollama ไม่มี ก็กลายเป็นจุดต่างสำคัญ
  • เดโมรันบน Windows 10 ด้วยโมเดล Meta LLaMA 3.2 3B Instruct
  • llamafile ตัวนี้ยังรันได้บน macOS, Linux, FreeBSD, NetBSD
  • เมื่อคุณภาพของโมเดล open-weight อย่าง gemma 27b it ดีขึ้น ก็ทำให้แรงจูงใจในการใช้ Claude ลดลง

C: ข้อยกเว้นด้าน lexing ที่ต่างจากภาพลักษณ์ว่าเป็นภาษาง่าย

  • แม้ C จะถูกมองว่าเรียบง่าย แต่ในทาง lexical แล้วกลับมีองค์ประกอบที่แปลกไม่น้อย
  • trigraph สามารถแทนอักขระอย่าง #, [, \\, ^, {, |, }, ~ ได้ด้วย ??=, ??(, ??/, ??), ??', ??<, ??!, ??>, ??-
    • แม้ถูกถอดออกจากมาตรฐาน C23 แล้ว แต่คอมไพเลอร์ก็น่าจะยังรองรับต่อไปเพราะซอฟต์แวร์เก่า
    • ถ้าเป็นตัวไฮไลต์ไวยากรณ์ที่ดี ก็ควรจัดการไวยากรณ์นี้ได้ด้วย
  • universal character ของ C ทำให้เขียน identifier แบบ Unicode ในซอร์สโค้ด ASCII ได้ เช่น int \\uFEB2 = 1;
    • GCC จะรายงาน error หากไม่ใช่ Unicode plane บางชุดที่คณะกรรมการมาตรฐานอนุญาต
  • คอมเมนต์บรรทัดเดียวของ C สามารถลากข้ามหลายบรรทัดได้ถ้าใช้ backslash ต่อท้ายบรรทัด
    • ภาษาอย่าง Perl, Ruby, Shell แม้จะรองรับ backslash escape ในซอร์สโค้ด แต่ไม่ได้รองรับพฤติกรรมแบบ C นี้
    • ผู้เขียนระบุว่า Tcl และ GNU Make รองรับพฤติกรรมนี้
    • Emacs และ Pygments บางครั้งจัดการผิด ขณะที่ Vim ดูเหมือนจะจัดการ backslash ได้ถูกเสมอ
  • C ยังมี null preprocessor directive ด้วย
    • ซอร์สโค้ด v6 ยุคแรก ๆ ในไฟล์ .c มักขึ้นต้นด้วยบรรทัด # เพียงบรรทัดเดียว
    • ปัจจุบันก็ยังเป็นโค้ดที่ถูกต้อง และยังใช้เพื่อบังคับให้ preprocessor ลบคอมเมนต์บางแบบได้ แม้จะสั่ง cc -C -E ให้เก็บคอมเมนต์ไว้ก็ตาม

ไวยากรณ์คอมเมนต์: Haskell และ D

  • ใน C ไม่สามารถซ้อนคอมเมนต์หลายบรรทัดไว้ภายในคอมเมนต์หลายบรรทัดอีกชั้นได้
  • Haskell รองรับ nested comment ในรูปแบบ {- ... {- ... -} ... -}
  • D รับคอมเมนต์ //, /* ... */ ของ C มาตรงตัว และเพิ่มไวยากรณ์คอมเมนต์แบบ recursive เป็น /+ ... +/
  • เอกสาร lexical syntax ของ D มีความเป็นทางการและละเอียด จึงให้ข้อมูลที่จำเป็นต่อการพัฒนาได้ดี
    • เอกสาร D lexical syntax ครอบคลุมรายละเอียดอย่าง hex string และ heredoc string
    • D มีสตริงหลายรูปแบบ เช่น \"...\", backtick string, r\"...\", q\"...\", x\"...\"

Tcl และ JavaScript: เครื่องหมายคำพูดกับตัวจบบรรทัดที่มองไม่เห็น

  • ใน Tcl สามารถมีเครื่องหมายคำพูดอยู่ใน identifier ได้
    • puts a\"b จะพิมพ์ a\"b
    • ชื่อตัวแปรก็ใส่เครื่องหมายคำพูดได้ แต่เวลาอ้างอิงต้องใช้ ${a\"b} แทน $a\"b
  • JavaScript มี lexical grammar สำหรับ regular expression อยู่ในตัว
    • ภายในวงเล็บชุดอักขระอย่าง /[/]/g นั้น / ไม่จำเป็นต้อง escape
    • ถ้าใช้วิธีสแกนหา slash ปิดแบบตรงไปตรงมา ก็อาจพลาดในโค้ดที่ถูก minify
  • ECMAScript กำหนดให้ u2028 LINE SEPARATOR และ u2029 PARAGRAPH SEPARATOR เป็น line terminator
    • อักขระเหล่านี้ทำงานแทบไม่ต่างจาก \\n
    • เนื่องจากเป็นอักขระประเภท Trojan Source ผู้เขียนจึงตั้งค่า Emacs ให้แสดงเป็น และ
    • ซอฟต์แวร์จำนวนมากไม่รู้จักอักขระเหล่านี้และมักเรนเดอร์เป็นเครื่องหมายคำถาม
    • ผู้เขียนบอกว่า นอกจาก D แล้วไม่รู้จักภาษาอื่นที่จัดการแบบนี้
  • คุณสมบัตินี้ทำให้สามารถสร้าง polyglot C และ JavaScript ใน SectorLISP ได้
    • lisp.js รันได้ในเบราว์เซอร์ และยังคอมไพล์ด้วย GCC เพื่อรันแบบโลคัลได้
    • llamafile สามารถไฮไลต์โค้ดแบบนี้ได้ถูกต้อง แต่ผู้เขียนบอกว่ายังไม่พบไฮไลต์เตอร์อื่นที่ทำได้เหมือนกัน

Shell และเคสขอบของ heredoc

  • heredoc ของ Shell ใช้เขียนสตริงหลายบรรทัดในรูปแบบ cat <<EOF ... EOF
  • ไวยากรณ์ quoted heredoc อย่าง cat <<'END' จะปิดการแทนค่าตัวแปร
  • ถ้าตั้ง heredoc marker เป็นสตริงว่าง heredoc จะจบที่บรรทัดว่างถัดไป
    • โปรแกรมตัวอย่างจะพิมพ์ hello และ world คนละบรรทัด
  • ภาษาอย่าง Shell, Ruby, Perl ที่รองรับ heredoc สามารถมี heredoc หลายชุดในบรรทัดเดียวกันได้
  • Shell ก็เหมือน Tcl ตรงที่ # ไม่ได้เริ่มคอมเมนต์เสมอไป
    • ใน ${x#hi-} นั้น # ใช้เพื่อลบ prefix และตัวอย่างจะพิมพ์ there

String interpolation: ภาษาที่ต้องมี state stack

  • สตริงของ Kotlin แม้จะเริ่มด้วย \" ก็อาจต้องเปลี่ยน state เมื่อเจอ { เพราะมี string interpolation
  • TypeScript, Swift, Kotlin, Scala รองรับ string interpolation แบบที่ฝังโค้ดจริงไว้ในสตริงอย่างเข้มข้น
  • ถ้าจะไฮไลต์ Kotlin, Scala, TypeScript ต้องนับวงเล็บปีกกาและเก็บ parser state stack ไว้
    • TypeScript ค่อนข้างง่ายกว่า โดยเพิ่ม state อีกไม่กี่ตัวใน finite state machine ก็พอ
    • Kotlin และ Scala รองรับ interpolation ทั้งในไวยากรณ์ double quote และ triple quote ทำให้เฉพาะการ lex สตริงก็ต้องใช้ state แยกประมาณ 13 ตัว
    • Swift รองรับไวยากรณ์ interpolation แบบ \"\\(var)\" และ triple quote แต่การพัฒนาต้องใช้ 10 state

Swift, C#, FORTH: ความต่างของการครอบสตริง

  • Swift รองรับไวยากรณ์ครอบ \"...\", \"\"\"...\"\"\", /regex/ ด้วย # จำนวนเท่าใดก็ได้
    • จำนวน # ทั้งสองฝั่งต้องตรงกัน
    • วิธีนี้ช่วยแก้ปัญหาการใส่เครื่องหมายคำพูดหรือขอบเขต regex ไว้ในสตริง
  • C# คล้ายสตริงหลายบรรทัดแบบ triple quote ของ Python แต่สามารถใช้เครื่องหมายคำพูดจำนวนมากกว่า และต้องให้ต้นกับท้ายตรงกัน
    • จำนวนเครื่องหมายคำพูดทางซ้ายจะกลายเป็นเงื่อนไขการปิดทางขวา
    • ผู้เขียนมองว่า การอนุญาตจำนวนเครื่องหมายคำพูดแบบอิสระทำให้มีกฎความถูกต้องน้อยกว่า Python triple quote แบบดั้งเดิม และถอดรหัสด้วย finite state machine ได้ง่ายกว่า
  • FORTH แยกทุกอย่างเป็นโทเค็นด้วยขอบเขตของช่องว่าง
    • ไวยากรณ์เปิดสตริงอย่าง c\" ก็ถือเป็นโทเค็นหนึ่งตัว
    • c\" hello world\" มีความหมายเทียบเท่า \"hello world\" ในภาษาอื่น

FORTRAN และ COBOL: กฎ fixed column

  • llamafile ถูกเสนอให้เป็นกรณีใช้งานสำหรับช่วยดูแลระบบธนาคาร แม้โปรแกรมเมอร์ FORTRAN และ COBOL จะเกษียณไปแล้ว
  • ผู้เขียนระบุว่าสามารถให้ AI ที่ควบคุมได้ในสภาพแวดล้อม air-gapped อย่าง Gemma 27b ช่วยกับโค้ด COBOL และ FORTRAN ได้
  • กฎ fixed column ของ FORTRAN มีดังนี้
    • ถ้าใส่ *, c, C ในคอลัมน์ที่ 1 บรรทัดนั้นจะเป็นคอมเมนต์
    • ถ้าใส่อักขระที่ไม่ใช่ช่องว่างในคอลัมน์ที่ 6 จะเป็นการต่อบรรทัดที่ยาวเกิน 80 ตัวอักษร
    • ถ้ามีตัวเลขในคอลัมน์ 1~5 จะถือเป็น label
  • กฎของ COBOL มีดังนี้
    • ถ้าใส่ * ในคอลัมน์ที่ 7 บรรทัดนั้นจะเป็นคอมเมนต์
    • ถ้าใส่ - ในคอลัมน์ที่ 7 จะเป็นการต่อบรรทัดที่ยาวเกิน 80 ตัวอักษร
    • คอลัมน์ 1~6 ใช้เป็นเลขบรรทัด

Zig และ Lua: แนวทางต่างกันของสตริงหลายบรรทัด

  • Zig มีไวยากรณ์ multiline string ที่เริ่มด้วย backslash สองตัว
    • วิธีนี้ตัดความจำเป็นในการต้องเรียก textwrap.dedent() แบบที่มักเกิดกับสตริง triple quote ของ Python
    • ข้อเสียคือเครื่องหมายอัฒภาคดูไม่สวยนัก
    • ผู้เขียนเสนอว่าอาจเป็นไวยากรณ์สตริงที่น่าสนใจสำหรับภาษาที่ไม่ต้องใช้อัฒภาคอย่าง Go, Scala, Python
  • สตริงหลายบรรทัดของ Lua อิงกับ [[...]] และสามารถใส่ = ได้ตามจำนวนใดก็ได้ระหว่างวงเล็บเหลี่ยม
    • เช่น [==[ ... ]==] โดยจำนวน = ตอนเปิดและปิดต้องตรงกัน
    • วิธีเดียวกันนี้ใช้กับคอมเมนต์ได้ด้วย
    • ทั้ง --[[ ... ]] และ --[==[ ... ]==] ใช้ได้

Assembly: การผสมกันของ dialect และ preprocessor

  • Assembly เป็นหนึ่งในภาษาที่ไฮไลต์ได้ยาก เพราะแตกแขนงออกเป็นหลาย dialect
  • llamafile ตั้งเป้าจัดการไวยากรณ์หลายแบบอย่าง AT&T, nasm ให้ได้อย่างสมเหตุสมผล
  • ถ้ามองคีย์เวิร์ดเป็น identifier ตัวแรกของบรรทัดที่ไม่ได้ตามหลัง colon ก็สามารถทำให้ assembly ส่วนใหญ่ดูใช้ได้
  • ไวยากรณ์คอมเมนต์ก็ไม่ได้ง่าย
    • คอมเมนต์แบบ UNIX ดั้งเดิมใช้เพียง / ตัวเดียว
    • GNU as ยังรองรับอยู่ แต่เฉพาะเมื่ออยู่ตำแหน่งต้นบรรทัด
    • ผู้เขียนระบุว่า Clang ไม่รองรับ fixed comment ทำให้แทบใช้จริงในโอเพนซอร์สไม่ได้
  • assembler ของ UNIX ดั้งเดิมไม่ใช้เครื่องหมายคำพูดปิดสำหรับ character literal
    • 'x หมายถึงค่า 0x78 ของอักขระ x
    • GNU as ยังรองรับ แต่ LLVM ไม่รองรับ
    • เพราะยังมีไวยากรณ์นี้อยู่ในโค้ดเก่า ตัวไฮไลต์ที่ดีก็ควรรองรับด้วย
  • GNU assembler อนุญาต identifier แบบมีเครื่องหมายคำพูด ทำให้ใส่อักขระแทบทุกชนิดลงใน symbol ได้
  • Assembly มักใช้ร่วมกับ C preprocessor หรือ m4
    • จึงควรมองบรรทัดที่ขึ้นต้นด้วย dnl, m4_dnl, C ว่าเป็นคอมเมนต์ด้วย

Ada และ BASIC: กรณีที่ไวยากรณ์เล็ก ๆ ทำให้ lexing สั่นคลอน

  • Ada ค่อนข้างง่ายต่อการทำ lexing แต่การใช้ single quote มีลักษณะเฉพาะ
    • สามารถมี character literal แบบ 'x' ได้เหมือน C
    • และยังใช้ single quote เพื่ออ้างอิง attribute เช่น Foo'Size
    • Character'(')')'Image คือการประกาศอักขระแล้วแปลงเป็นรูปแบบสตริงผ่านฟังก์ชัน Image
  • ตัวอย่าง Commodore BASIC ทำลายสมมติฐานหลายอย่างของ syntax highlighting
    • สตริงสามารถละเครื่องหมายคำพูดปิดที่ท้ายบรรทัดได้
    • ชื่อตัวแปรมี sigil อย่าง $ ติดอยู่ได้
    • คีย์เวิร์ดอย่าง goto ถูก lex อย่างจริงจังแม้อยู่ภายใน identifier
  • Visual BASIC มีไวยากรณ์ date literal เช่น #1/1/2024#
  • Visual BASIC ยังมี preprocessor directive อย่าง #If DEBUG Then, #Else, #End If ทำให้ lexing ยุ่งยากขึ้น

Perl: ความซับซ้อนกึ่ง Shell กึ่งภาษาโปรแกรม

  • Perl อยู่กึ่งกลางระหว่าง Shell กับภาษาโปรแกรม จึงรับความซับซ้อนมาทั้งสองด้าน
  • มันทำให้ regular expression เป็นองค์ประกอบระดับ first-class ของภาษา และอิทธิพลนี้ก็ส่งต่อไปยังภาษาอื่นอย่าง Python
  • ไวยากรณ์แทนที่ของ Perl คล้าย sed เช่น s/hello/Perl/i
    • สามารถใช้เครื่องหมายวรรคตอนอะไรก็ได้แทน / เป็นตัวคั่น
    • รูปแบบอย่าง s!hello!Perl!i จึงสะดวกเมื่อมี slash อยู่ใน regex
    • ถ้าใช้ตัวคั่นแบบเป็นคู่ เช่น s{hello}{Perl}i ก็ต้องมีกฎเพิ่มอีกชุด
  • Perl มี magic prefix จำนวนมากที่ต้องไฮไลต์คล้ายสตริง
    • เช่น m, s, y, qr, qw, qq, qx ที่ใช้ร่วมกับตัวคั่นได้หลายแบบ
  • หากไม่อยากตีความ y/x/y/ ผิดเป็นการหาร ก็ต้องดูบริบท
    • ตัวแปรของ Perl มี sigil อย่าง scalar $, array @, hash % จึงช่วยแยกแยะได้โดยไม่ต้อง parse ทั้งภาษา
  • Perl มีธรรมเนียมแทรกเอกสาร POD สำหรับ man page ไว้ในซอร์สโค้ด
    • บรรทัดที่ขึ้นต้นด้วย =word จะเริ่มเอกสาร POD และ =cut จะเป็นจุดสิ้นสุด

Ruby: เป้าหมายด้าน lexing ที่ยากที่สุด

  • Ruby ดูคล้ายผลรวมของภาษาก่อนหน้า และถูกมองว่าเอกสารไวยากรณ์ยังไม่เป็นทางการและละเอียดพอ
  • เอกสาร syntax ของ Ruby manual ถูกมองว่าลงรายละเอียดค่อนข้างเบา
  • Ruby รองรับ backquote syntax และในขณะเดียวกันก็ใช้ backquote เป็นชื่อเมธอดได้ ทำให้ไฮไลต์เตอร์แยกได้ยากว่าสิ่งนั้นเป็นสตริงหรือไม่
  • Ruby มีทั้งตัวดำเนินการ << และ heredoc
    • ในโค้ดจริงมีรูปแบบอย่าง options[:includes] <<arg; true
    • โค้ดนี้อาจดูเหมือน heredoc และ Emacs ก็จัดการผิดเช่นกัน
  • โค้ดอย่าง puts \"This is #{<<HERE.strip} evil\" ที่ใส่ heredoc ไว้ใน string interpolation ก็ยังเป็นโค้ดที่ถูกต้อง
  • จากทั้ง 42 ภาษา ผู้เขียนบอกว่า Ruby น่าตกใจที่สุด และอาจเป็นภาษาที่ lex โดยไม่ parse ไม่ได้ หรือแม้ parse แล้วก็ยังไม่แน่ใจว่าจะตีความได้ถูกต้องเสมอ

ความซับซ้อนเมื่อดูจากจำนวนบรรทัดโค้ดพัฒนา

  • ถ้าวัดจากจำนวนบรรทัดโค้ดของตัวไฮไลต์แต่ละภาษา FORTH เรียบง่ายที่สุด ส่วน Ruby ซับซ้อนที่สุด
  • ตัวพัฒนาที่สั้นที่สุดคือ highlight_forth.cpp ที่ 125 บรรทัด
  • งานพัฒนาที่ค่อนข้างสั้นยังรวมถึง m4 132 บรรทัด, Ada 149 บรรทัด, LISP 160 บรรทัด, MATLAB 166 บรรทัด, COBOL 186 บรรทัด, BASIC 199 บรรทัด, FORTRAN 200 บรรทัด
  • งานขนาดกลางมี JavaScript 337 บรรทัด, TypeScript 371 บรรทัด, Kotlin 387 บรรทัด, Scala 387 บรรทัด, Assembly 447 บรรทัด, C 449 บรรทัด, Swift 455 บรรทัด, D 521 บรรทัด
  • งานที่ยาวที่สุดคือ Shell 570 บรรทัด, Perl 583 บรรทัด, และ highlight_ruby.cpp 1042 บรรทัด

1 ความคิดเห็น

 
GN⁺ 2024-11-03
ความคิดเห็นจาก Hacker News
  • สิ่งที่ผมชอบที่สุดในบรรดา trigraph ของ C คือรูปแบบอย่าง do_action() ??!??! handle_error()
    มันดูเหมือน ไวยากรณ์จัดการข้อผิดพลาด แบบพิเศษ แต่พอรู้แล้วว่า ??!??! ถูกแปลงเป็น || ซึ่งเป็นตรรกะ OR และด้วยกฎการประเมินแบบลัดวงจร จึงเป็นโครงสร้างที่เรียก handle_error() เมื่อ do_action() คืนค่าที่ไม่ใช่ 0 ก็รู้สึกน่าพอใจดี

    • https://en.wikipedia.org/wiki/Digraphs_and_trigraphs_(progra...
      ??! จะถูกแปลงเป็น | ดังนั้น ??!??! จึงกลายเป็น || หรือ “or”
    • เลือกใช้ trigraph C แบบเก่า แทน || ด้วยเหตุผลด้านความงามหรือเปล่า?
  • อ่านสนุก แต่กลับรู้สึกเห็นด้วยกับมุมมองฝั่ง Lisp มากขึ้น
    เท่าที่เข้าใจ คือมองว่าไวยากรณ์ไม่ใช่ส่วนสำคัญมากนักของภาษา และมักกลายเป็นอุปสรรคมากกว่าจะช่วย จึงควรทำให้ เรียบง่ายและสม่ำเสมอ ที่สุด เพื่อจะได้ไปโฟกัสอย่างอื่น
    อย่างไรก็ตาม สำหรับผม การเรียนรู้การแก้ไขเชิงโครงสร้างใน Lisp จนถึงตอนนี้ยังใกล้เคียงกับอุปสรรคมากกว่าความช่วยเหลือ และก็หวังว่าสักวันจะได้ผลตอบแทนจากมัน

    • ไวยากรณ์ที่เรียบง่ายอาจดีสำหรับคอมพิวเตอร์ แต่ไวยากรณ์ถูกออกแบบมาเพื่อให้มนุษย์อ่านและเขียนเป็นหลัก
      ถ้าทำให้เรียบง่ายแบบ Lisp ก็แค่ผลักการถกเถียงเรื่องไวยากรณ์ไปเป็น ปัญหาเชิง semantics เท่านั้น เปลี่ยนแค่ระดับชั้นไป
      ผมคิดว่าไวยากรณ์ที่ซับซ้อนอ่านและเขียนง่ายกว่าไวยากรณ์เรียบง่ายที่มี semantics ซับซ้อนมาก ข้อผิดพลาดทางไวยากรณ์จะได้ feedback อย่างรวดเร็ว แต่ข้อผิดพลาดทางความหมายอาจซ่อนอยู่จนถึงตอนรันก็ได้
    • ใน Lisp มี reader macro ที่สามารถตั้งโปรแกรม lexer ใหม่ได้ และขั้นตอนการแปลงโครงสร้างที่มองเห็นให้เป็น syntax tree ก็จัดการด้วย macro ได้เช่นกัน
      ตัวอย่างเช่น https://pyret.org/ ก็เป็นแบบหนึ่ง จริง ๆ แล้วคงพูดยากว่า Lisp นั้นเรียบง่ายหรือจำเป็นต้องสม่ำเสมอเสมอไป
    • แปลกใจเล็กน้อยที่การแก้ไขเชิงโครงสร้างเป็นอุปสรรค และถ้าจะให้คำแนะนำอย่างหนึ่ง ก็คือใช้ parinfer แล้วรู้แค่ key binding ของสามคำสั่ง slurp, barf, raise ก็พอ
      แค่สี่อย่างนี้ก็ทำให้ได้ประโยชน์ของ paredit ราว 95% โดยไม่ต้องรับความซับซ้อน และเคล็ดลับที่เหลือค่อยเรียนหลังจากคุ้นแล้วก็ได้
  • มีท่อนที่ว่า “ไม่รู้ว่าใครอยากทำ syntax highlighting ให้ C ที่ 35MB ต่อวินาที แต่ตอนนี้ทำได้แล้ว” ซึ่งก็เร็วจริง แต่ tcc สามารถ คอมไพล์ C เป็นไบนารีโค้ดที่ 29MB ต่อวินาที ได้แม้บนคอมพิวเตอร์ที่เก่ามาก: https://bellard.org/tcc/#speed
    น่าจะทำให้เร็วกว่านี้ได้อีก แต่คงไม่จำเป็นนัก

  • มีบางอย่างที่ผู้เขียนพลาดไป
    ไม่ใช่แค่ TypeScript, Swift, Kotlin, Scala ที่ไปไกลถึงขั้นใส่โค้ดจริงลงในการแทรกค่าในสตริง แต่ C#, Python, JavaScript, Ruby, Shell, Make ก็รองรับคล้ายกัน
    Tcl แยกโค้ดกับข้อมูลได้ยาก ดังนั้น { } จึงแทบจะเป็นตัวคั่นสตริงแบบแปลก ๆ และใน xyzzy {#hello world} ก่อนถึงเวลาประเมินค่า เราไม่อาจรู้ได้ว่า #hello world เป็นคอมเมนต์หรือเป็นสตริง
    PostgreSQL มี สตริงแบบ dollar-quoted ที่สะดวก ทำให้ 'Dianne''s horse', $$Dianne's horse$$, $SomeTag$Dianne's horse$SomeTag$ มีความหมายเหมือนกัน ดังใน https://www.postgresql.org/docs/current/sql-syntax-lexical.h...

    • Perl ก็ทำได้
      สตริงอย่าง "I have $foo $bar's: @{[$bar x $foo]}" สามารถพิมพ์ I have 5 x's: xxxxx ได้
      ไวยากรณ์ @{[...]} ใช้คุณสมบัติของ Perl ที่แทรกค่าได้ไม่เฉพาะ scalar แต่รวมถึง array interpolation ด้วย โดย [...] ด้านในสร้าง array reference และ @{...} ด้านนอก dereference มัน ตัวแปลภาษา Perl อนุญาตให้มีโค้ดใด ๆ ใน expression ด้านในนั้น
    • ผมคิดว่ามองว่าเป็น ไวยากรณ์พิเศษสำหรับต่อสตริง จะถูกกว่าการบอกว่า “มีโค้ดอยู่ในสตริง”
      ถ้าเป็นแบบนั้นก็ซ้อนกันได้ เช่น "foo { toUpper("bar { x + y } bar") } foo" และถ้า + คือการต่อสตริง ก็แทบเทียบเท่ากับ "foo " + toUpper("bar " + (x + y) + " bar") + " foo"
      ไม่รู้เหมือนกันว่ามีภาษาใดที่ทำงานแบบนี้จริงหรือไม่
    • Ruby ผลักเรื่องนี้ไปจนสุดทาง
      ผมชอบ Ruby แต่ Ruby ที่ถูกต้องตามไวยากรณ์อย่าง puts "This is #{< ซึ่งผสม string interpolation กับ heredoc นั้นแก้ต่างให้ยาก
      อีกอย่าง ใน Ruby ช่องว่างก็เป็นอักขระอ้างอิง ได้ด้วย ในบริบทที่ไม่มี operand ฝั่งซ้าย % จะเริ่ม quoted string และอักขระถัดไปบอกชนิดของ quote ดังนั้น % hello จึงกลายเป็นสตริงที่มี hello อยู่ %(this is a string) หรือ %{this is a string} นั้นดี แต่การใช้ช่องว่างผมไม่เคยเห็นในโลกจริงเลย และ irb ก็จัดการได้ไม่ถูกต้อง จึงน่าจะลบออกได้
    • string interpolation ของ Scala ยังใช้เป็น เป้าหมายของ pattern matching ได้ด้วย
      ถ้าเขียน val s"${a} + ${b}" = "1 + 2" ค่า a จะเป็น 1 และ b จะเป็น 2
    • จุดที่ยอดเยี่ยมของ interpolated string ใน C# คือมันถูก ประเมินแบบหน่วงเวลา
      สมัยก่อน ใน hot loop โค้ดอย่าง log.trace($"Entering iteration {i} for customer {c.ID} [{c.ShortName}]"); จะเรียก string.Concat ทุกครั้งแม้ก่อนที่ logger จะหลุดออกไป ทำให้ logger จำนวนมากต้องทำ interpolation ของตัวเอง
      ใน C# สามารถประกาศ overload ที่รับ DefaultInterpolatedStringHandler หรือ pattern ของ custom handler ได้ และ overload นี้จะถูกเลือกก่อน ทำให้ตรวจสอบได้ว่าจำเป็นต้องเขียน log จริงหรือไม่ แล้วจึงเลื่อนการสร้างสตริงออกไป
  • ยังมีความแปลกทางไวยากรณ์อีกอย่างที่ไม่ได้กล่าวถึงตรงนี้ ซึ่งทำให้ syntax highlighter ส่วนใหญ่พังได้
    ใน Java Unicode escape สามารถปรากฏได้ทุกที่ ไม่ใช่แค่ภายในสตริง
    ตัวอย่างเช่น class Foo\u007b} เป็นคลาสที่ถูกต้อง และ \u000A ภายในคอมเมนต์ // อาจถูกประมวลผลเป็นการขึ้นบรรทัดใหม่จริง ทำให้ตัวอย่าง assert ทำงานต่างจากที่คาดไว้

    • การที่ไฮไลต์ไวยากรณ์เรื่องนี้ให้ถูกต้องไม่ได้ ผมมองว่าเป็น ปัญหาด้านความปลอดภัย ด้วย
      เพราะสามารถใช้ Unicode escape เพื่อปิด block comment ได้ ดังนั้นถ้าจะซ่อนโค้ดอันตรายในคอมเมนต์ของไฟล์ซอร์ส Java ก็แค่สร้างข้ออ้างให้มีบล็อก Unicode escape อยู่ในคอมเมนต์ก็พอ
      นักพัฒนาที่ไม่รู้คุณสมบัตินี้มีโอกาสสูงที่จะมองว่าเป็นส่วนที่ถูกคอมเมนต์ไว้แล้วและปล่อยผ่านไป
    • ผมไม่เคยเห็นอะไรแบบนี้ใน Java เลย จริง ๆ แล้วมันมีประโยชน์ในการใช้งานจริงไหม?
  • มีข้อความว่า “Ruby เป็นภาษาที่หลบเลี่ยงทุกความพยายามในการทำความเข้าใจไวยากรณ์” แต่ถ้ามองจากฝั่ง TeX ที่มี lexer ซึ่งสามารถโปรแกรมใหม่ได้ตามใจ ก็ยังน่ารักอยู่

    • reader macro ของ Lisp ก็สามารถโปรแกรม lexer ได้เช่นกัน
  • แม้จะบอกว่า “โปรแกรมเมอร์ C ทุกคนรู้ว่าใส่คอมเมนต์หลายบรรทัดซ้อนในคอมเมนต์หลายบรรทัดไม่ได้” แต่สำหรับโปรแกรมเมอร์ Standard ML นี่อาจเป็นข้อจำกัดที่น่าแปลกใจ
    คอมเมนต์ซ้อน อย่าง (* (* Nested (**) *) comment *) นั้นถูกต้อง และตามด้วย val _ = print "hello, world\n" ก็รันได้ตามปกติ
    เมื่อคิดว่า C เคยถูกมองว่าเป็นภาษาที่แสดงออกได้ดีตอนที่มันถือกำเนิดขึ้น ก็น่าสนใจที่คอมเมนต์ซ้อนไม่ได้ถูกใส่ไว้ในภาษา

    • สิ่งที่น่าสนุกในคำพูดนั้นมีสามอย่าง
      ML ไม่มีคอมเมนต์บรรทัดเดียว ดังนั้นนั่นก็นับเป็นข้อจำกัดที่น่าแปลกใจในลักษณะเดียวกัน และผมไม่เคยได้ยินใครเรียก C ว่า “แสดงออกได้ดี” มาก่อน แต่ถ้าเทียบกับ assembly ในปี 1972 ก็คงอาจเป็นเช่นนั้น
      อีกอย่าง ผมไม่รู้ว่าไวยากรณ์คอมเมนต์เกี่ยวข้องอะไรกับความสามารถในการแสดงออกของภาษา และตามนิยามแล้วผมมองว่าไม่เกี่ยวเลย
    • การ lex คอมเมนต์ซ้อนต้องใช้ stack หรืออย่างน้อยตัวนับระดับการซ้อน
      ตามธรรมเนียมแล้ว การวิเคราะห์คำศัพท์ถูกมองว่าเป็นขอบเขตที่ใช้เพียง finite state automaton แบบเดียวกับ regex ดังนั้นฟีเจอร์แบบนี้จึงดูอยู่นอกขอบเขตของการ lex
    • ใน C เองก็มีวิธีหนึ่งในการซ้อนคอมเมนต์ได้ถ้าใช้ #if 0
      สามารถครอบแบบ #if 0 ... #if 0 ... #endif ... #endif ได้
    • Pascal ก็รองรับ ไวยากรณ์คอมเมนต์ซ้อน แบบในตัวอย่างมาโดยตลอด
    • เรื่องนี้ใช้ได้กับ ML ทั่วไปด้วย ไม่ใช่แค่ Standard ML
  • อยากเห็นการนำ implementation ของ syntax highlighting ใน joe มาอยู่ในรูปแบบที่ใช้ซ้ำได้
    รูปแบบของ https://joe-editor.sf.net/ ทรงพลังพอที่จะไฮไลต์ Python f-string ได้อย่างถูกต้อง
    เอกสารและตัวอย่างที่เกี่ยวข้องอยู่ที่ https://github.com/cmur2/joe-syntax/blob/joe-4.4/misc/HowItW..., https://gist.github.com/irdc/6188f11b1e699d615ce2520f03f1d0d...

    • ผมเคยสร้าง lexer และ parser หลายตัวโดยอิงจาก การ parse แบบ DFA ของ joe
      ไวยากรณ์ของสถานะและ transition เข้าใจง่ายกว่าเครื่องมือมาตรฐานมาก
      ข้อเสียคือชุดกฎจะยืดยาว และจัดโครงสร้างให้สวยงามในอุดมคติได้ยากขึ้นเล็กน้อย แต่ในแง่ที่ทำให้อนุมานกฎการสร้างทั้งหมดจากโค้ดได้ง่าย ผมกลับมองว่าเป็นข้อดี
    • น่าสนใจที่ Python f-string มี ไวยากรณ์เปลี่ยนไปใน 3.12 ดังนั้นการไฮไลต์ก็ควรแตกต่างกันตามเวอร์ชันด้วย
  • ดูเหมือนผู้เขียนยังไม่เคยลองทำ syntax highlighting สำหรับ TeX
    ซึ่งอาจเป็นผลดีต่อสุขภาพจิต เพราะโดยทั่วไปแล้ว TeX ไม่สามารถไฮไลต์ไวยากรณ์ให้สมบูรณ์ได้หากไม่ตีความมัน
    แค่ parse อย่างเดียวยังไม่พอ เพราะสามารถนิยามหน้าที่ของอักขระแต่ละตัวใหม่ได้ ถึงขั้นทำอะไรอย่าง “ต่อไปนี้ K คือ { และ C คือ }” ได้
    ในความเป็นจริงก็มีบทความบน arXiv บางฉบับที่ใช้ฟีเจอร์ต้องสาปนี้อยู่

    • ผมทำ https://github.com/Mozilla-Ocho/llamafile/blob/main/llamafil... ขึ้นมา และสำหรับไฟล์ .tex ที่พบในฮาร์ดไดรฟ์ของผม มันไฮไลต์ได้ค่อนข้างดีโดยไม่พัง
      เป้าหมายคือครอบคลุม 99.9% ของการใช้งานในโลกจริง ถ้าทำได้ สิ่งที่ LLM น่าจะพ่นออกมาก็น่าจะครอบคลุมได้เป็นส่วนใหญ่เช่นกัน
      ไวยากรณ์พิสดารก็มักไม่เป็นปัญหาใหญ่นัก ตราบใดที่สตริงหรือคอมเมนต์ไม่ลากยาวไปจนจบไฟล์และกลืนซอร์สโค้ดที่เหลือทั้งหมด
    • ผมแทบไม่อยากเชื่อเมื่อรู้ว่า \makeatletter ไม่ได้หมายถึง “ทำอะไรสักอย่างให้เป็นอักขระ at” แต่หมายถึงตอน parse ให้ ถือว่าอักขระ @ เป็นตัวอักษร
    • Common Lisp ก็สามารถนิยาม readtable ใหม่ได้ในลักษณะคล้ายกัน เพียงแต่น่าจะถูกใช้ในทางที่ผิดบน arXiv น้อยกว่า
  • ผมคิดว่าการเขียนเอนจินไฮไลต์ไวยากรณ์ดี ๆ แบบ Vim นั้นไม่ง่าย
    การไฮไลต์ไวยากรณ์ต้องมี การประมวลผลตามบริบท เพื่อใช้กฎที่ต่างกันกับเนื้อหาที่ซ้อนกันในรูปแบบเฉพาะ
    ตัวไฮไลต์ไวยากรณ์ของ Vim ให้ประกาศรายการได้สองชนิดคือ match และ region โดย match เป็นกฎเชิงคำศัพท์แบบง่าย ส่วน region มีนิพจน์สำหรับแมตช์จุดเริ่มต้น จุดสิ้นสุด และส่วนกลางแยกกัน
    รายการสามารถประกาศให้ทำงานเฉพาะเมื่ออยู่ภายใน region บางอย่างได้ และยังประกาศความสัมพันธ์การรวมซ้อนระหว่างกันได้ด้วย
    บนโครงสร้างความหมายพื้นฐานนี้ ยังมีฟีเจอร์ต่าง ๆ สำหรับกรณีพิเศษอีกหลายอย่างถูกเสริมเข้ามา ต่อให้เป็น Justine ก็คงยากที่จะสร้างมันขึ้นมาได้ทันทีในห้องสัมภาษณ์ น่าจะต้องเป็นโจทย์การบ้านสักคืนหนึ่งมากกว่า

    • ตัวอย่างที่รับมือยากคือ TXR Lisp ที่ฝังอยู่ในภาษา TXR
      มีสคริปต์ genman ที่รับผลลัพธ์จากการแปลง manpage เป็น HTML แล้วปรับแต่งให้เป็น HTML คู่มือ TXR อยู่ที่นี่: https://www.kylheku.com/cgit/txr/tree/genman.txr
      ส่วนที่เห็นเป็นสีขาวคือเทมเพลตแบบลิเทอรัล และโค้ด Lisp จะอยู่ภายใน directive อย่าง @(do ...) คีย์เวิร์ดของ TXR จะเป็นสีม่วง ส่วนคีย์เวิร์ดของ TXR Lisp จะเป็นสีเขียว และแม้เป็นคำเดียวกันก็เปลี่ยนไปตามบริบทได้
      quasi-string ก็สามารถมีไวยากรณ์ที่ซ้อนกันอยู่ข้างในได้ และภายในนั้นยังสามารถมีโค้ดฝังหรือ quasi-string ซ้อนเข้าไปอีกได้ ไฟล์นิยามไวยากรณ์ txr.vim และ tl.vim ทั้งคู่ถูกสร้างจาก https://www.kylheku.com/cgit/txr/tree/genvim.txr
    • แบบคิดง่าย ๆ ผมเคยนึกว่าถ้าจะทำตัวไฮไลต์ไวยากรณ์ให้ถูกต้อง ก็แค่ พาร์สเป็น AST แล้วเดินดูโทเคนพร้อมอัปเดตสีตามชนิดของโหนดก็พอ
      ถ้าไม่ทำแบบนั้น ก็น่าจะเพราะปัญหาด้านประสิทธิภาพ เช่น แทนที่จะไฮไลต์เฉพาะส่วนที่เห็นบนหน้าจอ ก็ต้องพาร์สทั้งไฟล์ ทำให้ต้นทุนสูงขึ้น
    • ผมคงไม่พนันเงินว่า Justine จะทำสิ่งนั้นไม่ได้ในการสัมภาษณ์หรอก ;)