โครงสร้าง Lexical Syntax ที่ชวนประหลาด
(justine.lol)- ระหว่างสร้าง ตัวไฮไลต์ไวยากรณ์ ตัวใหม่สำหรับ llamafile โดยต้องรองรับ 42 ภาษา พบว่าแม้แต่การทำ lexical analysis แบบพื้นฐานก็ยังซ่อนข้อยกเว้นเฉพาะภาษาและไวยากรณ์เก่าไว้แน่นขนัด
- การพัฒนาใช้ C++ และ GNU gperf เพื่อเร่งการค้นหาคีย์เวิร์ด และจัดการสตริง คอมเมนต์ และคีย์เวิร์ดเป็นหลักด้วย finite state machine ที่อิง
forloop และ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
- สามารถสร้างไฮไลต์เตอร์พื้นฐานได้ด้วยแค่
forloop และ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ให้เก็บคอมเมนต์ไว้ก็ตาม
- ซอร์สโค้ด v6 ยุคแรก ๆ ในไฟล์
ไวยากรณ์คอมเมนต์: 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 กำหนดให้
u2028LINE SEPARATOR และu2029PARAGRAPH 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 ทำให้แทบใช้จริงในโอเพนซอร์สไม่ได้
- คอมเมนต์แบบ UNIX ดั้งเดิมใช้เพียง
- 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
- สามารถมี character literal แบบ
- ตัวอย่าง 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 มี sigil อย่าง scalar
- 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 ความคิดเห็น
ความคิดเห็นจาก Hacker News
สิ่งที่ผมชอบที่สุดในบรรดา trigraph ของ C คือรูปแบบอย่าง
do_action() ??!??! handle_error()มันดูเหมือน ไวยากรณ์จัดการข้อผิดพลาด แบบพิเศษ แต่พอรู้แล้วว่า
??!??!ถูกแปลงเป็น||ซึ่งเป็นตรรกะ OR และด้วยกฎการประเมินแบบลัดวงจร จึงเป็นโครงสร้างที่เรียกhandle_error()เมื่อdo_action()คืนค่าที่ไม่ใช่ 0 ก็รู้สึกน่าพอใจดี??!จะถูกแปลงเป็น|ดังนั้น??!??!จึงกลายเป็น||หรือ “or”||ด้วยเหตุผลด้านความงามหรือเปล่า?อ่านสนุก แต่กลับรู้สึกเห็นด้วยกับมุมมองฝั่ง Lisp มากขึ้น
เท่าที่เข้าใจ คือมองว่าไวยากรณ์ไม่ใช่ส่วนสำคัญมากนักของภาษา และมักกลายเป็นอุปสรรคมากกว่าจะช่วย จึงควรทำให้ เรียบง่ายและสม่ำเสมอ ที่สุด เพื่อจะได้ไปโฟกัสอย่างอื่น
อย่างไรก็ตาม สำหรับผม การเรียนรู้การแก้ไขเชิงโครงสร้างใน Lisp จนถึงตอนนี้ยังใกล้เคียงกับอุปสรรคมากกว่าความช่วยเหลือ และก็หวังว่าสักวันจะได้ผลตอบแทนจากมัน
ถ้าทำให้เรียบง่ายแบบ Lisp ก็แค่ผลักการถกเถียงเรื่องไวยากรณ์ไปเป็น ปัญหาเชิง semantics เท่านั้น เปลี่ยนแค่ระดับชั้นไป
ผมคิดว่าไวยากรณ์ที่ซับซ้อนอ่านและเขียนง่ายกว่าไวยากรณ์เรียบง่ายที่มี semantics ซับซ้อนมาก ข้อผิดพลาดทางไวยากรณ์จะได้ feedback อย่างรวดเร็ว แต่ข้อผิดพลาดทางความหมายอาจซ่อนอยู่จนถึงตอนรันก็ได้
ตัวอย่างเช่น 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...สตริงอย่าง
"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 ที่ถูกต้องตามไวยากรณ์อย่าง
puts "This is #{<ซึ่งผสม string interpolation กับ heredoc นั้นแก้ต่างให้ยากอีกอย่าง ใน Ruby ช่องว่างก็เป็นอักขระอ้างอิง ได้ด้วย ในบริบทที่ไม่มี operand ฝั่งซ้าย
%จะเริ่ม quoted string และอักขระถัดไปบอกชนิดของ quote ดังนั้น% helloจึงกลายเป็นสตริงที่มีhelloอยู่%(this is a string)หรือ%{this is a string}นั้นดี แต่การใช้ช่องว่างผมไม่เคยเห็นในโลกจริงเลย และirbก็จัดการได้ไม่ถูกต้อง จึงน่าจะลบออกได้ถ้าเขียน
val s"${a} + ${b}" = "1 + 2"ค่าaจะเป็น1และbจะเป็น2สมัยก่อน ใน 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 อยู่ในคอมเมนต์ก็พอ
นักพัฒนาที่ไม่รู้คุณสมบัตินี้มีโอกาสสูงที่จะมองว่าเป็นส่วนที่ถูกคอมเมนต์ไว้แล้วและปล่อยผ่านไป
มีข้อความว่า “Ruby เป็นภาษาที่หลบเลี่ยงทุกความพยายามในการทำความเข้าใจไวยากรณ์” แต่ถ้ามองจากฝั่ง TeX ที่มี lexer ซึ่งสามารถโปรแกรมใหม่ได้ตามใจ ก็ยังน่ารักอยู่
แม้จะบอกว่า “โปรแกรมเมอร์ C ทุกคนรู้ว่าใส่คอมเมนต์หลายบรรทัดซ้อนในคอมเมนต์หลายบรรทัดไม่ได้” แต่สำหรับโปรแกรมเมอร์ Standard ML นี่อาจเป็นข้อจำกัดที่น่าแปลกใจ
คอมเมนต์ซ้อน อย่าง
(* (* Nested (**) *) comment *)นั้นถูกต้อง และตามด้วยval _ = print "hello, world\n"ก็รันได้ตามปกติเมื่อคิดว่า C เคยถูกมองว่าเป็นภาษาที่แสดงออกได้ดีตอนที่มันถือกำเนิดขึ้น ก็น่าสนใจที่คอมเมนต์ซ้อนไม่ได้ถูกใส่ไว้ในภาษา
ML ไม่มีคอมเมนต์บรรทัดเดียว ดังนั้นนั่นก็นับเป็นข้อจำกัดที่น่าแปลกใจในลักษณะเดียวกัน และผมไม่เคยได้ยินใครเรียก C ว่า “แสดงออกได้ดี” มาก่อน แต่ถ้าเทียบกับ assembly ในปี 1972 ก็คงอาจเป็นเช่นนั้น
อีกอย่าง ผมไม่รู้ว่าไวยากรณ์คอมเมนต์เกี่ยวข้องอะไรกับความสามารถในการแสดงออกของภาษา และตามนิยามแล้วผมมองว่าไม่เกี่ยวเลย
ตามธรรมเนียมแล้ว การวิเคราะห์คำศัพท์ถูกมองว่าเป็นขอบเขตที่ใช้เพียง finite state automaton แบบเดียวกับ regex ดังนั้นฟีเจอร์แบบนี้จึงดูอยู่นอกขอบเขตของการ lex
#if 0สามารถครอบแบบ
#if 0 ... #if 0 ... #endif ... #endifได้อยากเห็นการนำ 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...
ไวยากรณ์ของสถานะและ transition เข้าใจง่ายกว่าเครื่องมือมาตรฐานมาก
ข้อเสียคือชุดกฎจะยืดยาว และจัดโครงสร้างให้สวยงามในอุดมคติได้ยากขึ้นเล็กน้อย แต่ในแง่ที่ทำให้อนุมานกฎการสร้างทั้งหมดจากโค้ดได้ง่าย ผมกลับมองว่าเป็นข้อดี
ดูเหมือนผู้เขียนยังไม่เคยลองทำ syntax highlighting สำหรับ TeX
ซึ่งอาจเป็นผลดีต่อสุขภาพจิต เพราะโดยทั่วไปแล้ว TeX ไม่สามารถไฮไลต์ไวยากรณ์ให้สมบูรณ์ได้หากไม่ตีความมัน
แค่ parse อย่างเดียวยังไม่พอ เพราะสามารถนิยามหน้าที่ของอักขระแต่ละตัวใหม่ได้ ถึงขั้นทำอะไรอย่าง “ต่อไปนี้ K คือ
{และ C คือ}” ได้ในความเป็นจริงก็มีบทความบน arXiv บางฉบับที่ใช้ฟีเจอร์ต้องสาปนี้อยู่
.texที่พบในฮาร์ดไดรฟ์ของผม มันไฮไลต์ได้ค่อนข้างดีโดยไม่พังเป้าหมายคือครอบคลุม 99.9% ของการใช้งานในโลกจริง ถ้าทำได้ สิ่งที่ LLM น่าจะพ่นออกมาก็น่าจะครอบคลุมได้เป็นส่วนใหญ่เช่นกัน
ไวยากรณ์พิสดารก็มักไม่เป็นปัญหาใหญ่นัก ตราบใดที่สตริงหรือคอมเมนต์ไม่ลากยาวไปจนจบไฟล์และกลืนซอร์สโค้ดที่เหลือทั้งหมด
\makeatletterไม่ได้หมายถึง “ทำอะไรสักอย่างให้เป็นอักขระ at” แต่หมายถึงตอน parse ให้ ถือว่าอักขระ@เป็นตัวอักษรผมคิดว่าการเขียนเอนจินไฮไลต์ไวยากรณ์ดี ๆ แบบ Vim นั้นไม่ง่าย
การไฮไลต์ไวยากรณ์ต้องมี การประมวลผลตามบริบท เพื่อใช้กฎที่ต่างกันกับเนื้อหาที่ซ้อนกันในรูปแบบเฉพาะ
ตัวไฮไลต์ไวยากรณ์ของ Vim ให้ประกาศรายการได้สองชนิดคือ
matchและregionโดยmatchเป็นกฎเชิงคำศัพท์แบบง่าย ส่วนregionมีนิพจน์สำหรับแมตช์จุดเริ่มต้น จุดสิ้นสุด และส่วนกลางแยกกันรายการสามารถประกาศให้ทำงานเฉพาะเมื่ออยู่ภายใน region บางอย่างได้ และยังประกาศความสัมพันธ์การรวมซ้อนระหว่างกันได้ด้วย
บนโครงสร้างความหมายพื้นฐานนี้ ยังมีฟีเจอร์ต่าง ๆ สำหรับกรณีพิเศษอีกหลายอย่างถูกเสริมเข้ามา ต่อให้เป็น Justine ก็คงยากที่จะสร้างมันขึ้นมาได้ทันทีในห้องสัมภาษณ์ น่าจะต้องเป็นโจทย์การบ้านสักคืนหนึ่งมากกว่า
มีสคริปต์
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ถ้าไม่ทำแบบนั้น ก็น่าจะเพราะปัญหาด้านประสิทธิภาพ เช่น แทนที่จะไฮไลต์เฉพาะส่วนที่เห็นบนหน้าจอ ก็ต้องพาร์สทั้งไฟล์ ทำให้ต้นทุนสูงขึ้น