2 điểm bởi GN⁺ 2024-11-03 | 1 bình luận | Chia sẻ qua WhatsApp
  • Khi xử lý 42 ngôn ngữ để tạo trình tô sáng cú pháp mới cho llamafile, tác giả nhận ra ngay cả việc lexing đơn giản cũng ẩn đầy các ngoại lệ theo từng ngôn ngữ và những ngữ pháp cũ kỹ
  • Việc triển khai dùng C++ và GNU gperf để tăng tốc tra cứu từ khóa, còn xử lý xoay quanh chuỗi, chú thích và từ khóa được giải quyết bằng máy trạng thái hữu hạn dựa trên vòng lặp forswitch
  • Trigraph của C, ký tự kết thúc dòng u2028·u2029 của JavaScript, heredoc của Shell·Perl·Ruby, và nội suy chuỗi của Kotlin·Scala·TypeScript·Swift liên tục xuất hiện như những trường hợp khó xử lý chỉ bằng lexing
  • Tính theo số dòng mã, FORTH chỉ có 125 dòng còn Ruby lên tới 1042 dòng; trong Ruby, toán tử <<, heredoc và backquote xung đột nhau nên tô sáng không cần parsing đặc biệt khó
  • Trình tô sáng mới của llamafile được trình diễn với Meta LLaMA 3.2 3B Instruct trên Windows 10 và cũng chạy được trên macOS·Linux·FreeBSD·NetBSD, tạo khác biệt với ollama vốn không có tô sáng cú pháp

Động lực tạo trình tô sáng cho llamafile

  • Tác giả đã học 42 ngôn ngữ lập trình trong một tháng để tạo trình tô sáng cú pháp mới cho llamafile
  • Các ngôn ngữ được hỗ trợ gồm 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
  • Danh sách này bao phủ phần lớn TIOBE Index, nhưng Scratch bị loại vì dùng khối thay vì văn bản nên không thuộc đối tượng tô sáng

Cách triển khai: gperf và máy trạng thái hữu hạn

  • Nút thắt lớn nhất trong một trình tô sáng cú pháp cơ bản là lúc cần xác định token có phải từ khóa hay không, vì số lần so sánh chuỗi lặp đi lặp lại sẽ tăng lên
  • Tác giả dùng C++ và GNU gperf để tạo bảng băm hoàn hảo
    • Ví dụ đầu vào gperf định nghĩa các hằng Java như true, false, null là từ khóa
    • Tệp C do gperf sinh ra có thể tạo hàm băm chỉ cần xét một ký tự duy nhất để tra cứu không va chạm
  • Phần tô sáng cho C vẫn xử lý được khoảng 4.000 từ khóa với tốc độ 35 MB/s nhờ gperf
  • Phần còn lại chủ yếu chỉ cần máy trạng thái hữu hạn
    • Có thể tạo trình tô sáng cơ bản chỉ với vòng lặp forswitch, không cần flex, bison, ragel
    • Nếu tập trung vào chuỗi, chú thích và từ khóa thì phần lớn chỉ cần xử lý ở mức lexing
    • Nhưng nếu muốn tô sáng cả những yếu tố như tên hàm trong C thì có thể cần parsing thực sự
  • highlight_ada.cpp được dùng làm ví dụ triển khai

Demo llamafile và môi trường sử dụng

  • Trình tô sáng mới cùng giao diện chatbot giúp tăng khả năng sử dụng của llamafile, và tô sáng cú pháp là điểm khác biệt so với ollama
  • Bản demo chạy trên Windows 10 với mô hình Meta LLaMA 3.2 3B Instruct
  • Tệp llamafile này cũng chạy được trên macOS, Linux, FreeBSD, NetBSD
  • Khi chất lượng của các mô hình trọng số mở như gemma 27b it tăng lên, động lực dùng Claude giảm đi

C: các ngoại lệ lexing trái với ấn tượng là ngôn ngữ đơn giản

  • Trái với ấn tượng là đơn giản, C lại có nhiều thành phần lexical khá dị biệt
  • trigraph cho phép thay thế các ký tự như #, [, \\, ^, {, |, }, ~ bằng ??=, ??(, ??/, ??), ??', ??<, ??!, ??>, ??-
    • Dù đã bị loại khỏi tiêu chuẩn C23, trình biên dịch có lẽ vẫn sẽ tiếp tục hỗ trợ vì phần mềm legacy
    • Một trình tô sáng tốt cũng cần xử lý ngữ pháp này
  • universal character của C cho phép biểu diễn định danh Unicode trong mã nguồn ASCII như int \\uFEB2 = 1;
    • GCC sẽ báo lỗi nếu ký tự đó không thuộc một số plane Unicode nhất định mà ủy ban tiêu chuẩn cho phép
  • Chú thích một dòng của C có thể kéo dài sang nhiều dòng nếu dùng dấu gạch chéo ngược ở cuối dòng
    • Các ngôn ngữ cho phép escape bằng backslash trong mã nguồn như Perl, Ruby, Shell lại không hỗ trợ hành vi kiểu C này
    • Tcl và GNU Make được cho là có hỗ trợ hành vi này
    • Emacs và Pygments đôi khi xử lý sai, còn Vim có vẻ luôn xử lý đúng phần backslash
  • C cũng có null preprocessor directive
    • Các tệp .c trong mã nguồn v6 đời đầu thường bắt đầu bằng một dòng chỉ có #
    • Đây vẫn là mã hợp lệ và có thể dùng để buộc preprocessor xóa một số chú thích nhất định, ngay cả khi yêu cầu giữ chú thích bằng cc -C -E

Cú pháp chú thích: Haskell và D

  • Trong C, không thể lồng chú thích nhiều dòng bên trong chú thích nhiều dòng khác
  • Haskell hỗ trợ chú thích lồng nhau theo dạng {- ... {- ... -} ... -}
  • D giữ nguyên các chú thích //, /* ... */ của C, đồng thời thêm cú pháp chú thích đệ quy riêng /+ ... +/
  • Tài liệu ngữ pháp lexical của D có tính hình thức và chi tiết, cung cấp tốt thông tin cần thiết để triển khai
    • Tài liệu lexical syntax của D đề cập các chi tiết như chuỗi hex và chuỗi heredoc
    • D có nhiều dạng chuỗi như "...", chuỗi backtick, r"...", q"...", x"..."

Tcl và JavaScript: dấu nháy và ký tự kết thúc dòng vô hình

  • Trong Tcl, dấu nháy có thể xuất hiện bên trong định danh
    • puts a"b sẽ in ra a"b
    • Có thể đặt dấu nháy trong tên biến, nhưng khi tham chiếu phải dùng ${a"b} thay vì $a"b
  • JavaScript có ngữ pháp lexical tích hợp cho biểu thức chính quy
    • Trong trường hợp như /[/]/g, ký tự / bên trong cặp ngoặc của tập ký tự không cần escape
    • Cách chỉ quét đơn giản để tìm dấu gạch chéo đóng có thể sai với mã đã minify
  • ECMAScript định nghĩa u2028 LINE SEPARATOR và u2029 PARAGRAPH SEPARATOR là ký tự kết thúc dòng
    • Trên thực tế chúng hoạt động như \\n
    • Vì là ký tự Trojan Source nên trong Emacs chúng được cấu hình hiển thị thành ,
    • Nhiều phần mềm không nhận ra các ký tự này và có thể render chúng thành dấu hỏi
    • Ngoài D, tác giả không biết ngôn ngữ nào khác xử lý theo cách này
  • Tận dụng đặc tính đó, tác giả đã tạo được polyglot C và JavaScript trong SectorLISP
    • lisp.js chạy được trong trình duyệt và cũng có thể biên dịch bằng GCC để chạy cục bộ
    • llamafile tô sáng đúng loại mã này, còn tác giả chưa tìm thấy trình tô sáng nào khác xử lý tương tự

Shell và các trường hợp rìa của heredoc

  • Heredoc của Shell cho phép viết chuỗi nhiều dòng theo dạng cat <<EOF ... EOF
  • Cú pháp quoted heredoc như cat <<'END' sẽ vô hiệu hóa thay thế biến
  • Nếu để marker heredoc là chuỗi rỗng thì heredoc sẽ kết thúc ở dòng trống tiếp theo
    • Chương trình ví dụ in helloworld trên hai dòng
  • Trong các ngôn ngữ hỗ trợ heredoc như Shell, Ruby, Perl, có thể đặt nhiều heredoc trên cùng một dòng
  • Shell cũng giống Tcl ở chỗ # không phải lúc nào cũng bắt đầu chú thích
    • Trong ${x#hi-}, # dùng để xóa tiền tố, và ví dụ sẽ in ra there

Nội suy chuỗi: các ngôn ngữ cần stack trạng thái

  • Chuỗi của Kotlin dù bắt đầu bằng " vẫn có thể chuyển sang trạng thái khác tại { vì nội suy chuỗi
  • TypeScript, Swift, Kotlin, Scala hỗ trợ mạnh kiểu nội suy chuỗi cho phép đặt mã thực sự vào trong chuỗi
  • Để tô sáng Kotlin, Scala, TypeScript, cần đếm ngoặc nhọn và duy trì stack trạng thái parser
    • TypeScript tương đối đơn giản, chỉ cần thêm vài trạng thái vào máy trạng thái hữu hạn
    • Kotlin và Scala hỗ trợ nội suy trong cả cú pháp dấu nháy kép lẫn ba dấu nháy, nên riêng việc lexing chuỗi đã cần khoảng 13 trạng thái độc lập
    • Swift hỗ trợ nội suy "\\(var)" và triple quote, nhưng phần triển khai vẫn cần 10 trạng thái

Swift, C#, FORTH: khác biệt trong cách bao chuỗi

  • Swift hỗ trợ cú pháp bao chuỗi "...", """...""", /regex/ bằng số lượng # tùy ý
    • Số lượng # ở hai bên phải khớp nhau
    • Cách này giải quyết vấn đề cần đặt dấu nháy hoặc ranh giới regex bên trong chuỗi
  • C# giống chuỗi nhiều dòng triple quote của Python, nhưng cho phép dùng số lượng dấu nháy lớn hơn miễn là khớp giữa đầu và cuối
    • Số dấu nháy ở bên trái quyết định điều kiện kết thúc ở bên phải
    • Theo tác giả, cho phép số dấu nháy tùy ý làm giảm các quy tắc hợp lệ so với triple quote Python cổ điển, nên có thể giải mã đơn giản hơn bằng máy trạng thái hữu hạn
  • FORTH token hóa mọi thứ theo ranh giới khoảng trắng
    • Cú pháp bắt đầu chuỗi c" cũng là một token
    • c" hello world" có ý nghĩa tương đương "hello world" trong các ngôn ngữ khác

FORTRAN và COBOL: quy tắc cột cố định

  • llamafile được nêu như một trường hợp sử dụng có thể hỗ trợ bảo trì hệ thống ngân hàng sau khi các lập trình viên FORTRAN và COBOL nghỉ hưu
  • Có thể yêu cầu AI có thể kiểm soát trong môi trường air-gap như Gemma 27b sinh mã COBOL và FORTRAN
  • Quy tắc cột cố định của FORTRAN như sau
    • Nếu cột 1 là *, c, C thì cả dòng là chú thích
    • Nếu cột 6 là ký tự không phải khoảng trắng thì có thể nối tiếp dòng vượt quá 80 ký tự
    • Nếu cột 1~5 là chữ số thì đó là nhãn
  • Quy tắc của COBOL như sau
    • Nếu cột 7 là * thì là chú thích
    • Nếu cột 7 là - thì có thể nối tiếp dòng vượt quá 80 ký tự
    • Cột 1~6 chứa số dòng

Zig và Lua: các cách giải khác nhau cho chuỗi nhiều dòng

  • Zig có cú pháp chuỗi nhiều dòng bắt đầu bằng hai dấu gạch chéo ngược
    • Cú pháp này loại bỏ nhu cầu phải gọi textwrap.dedent() như với chuỗi triple quote của Python
    • Nhược điểm là dấu chấm phẩy trông không đẹp mắt
    • Nó được nêu như một cú pháp chuỗi đáng để các ngôn ngữ không cần dấu chấm phẩy như Go, Scala, Python cân nhắc
  • Chuỗi nhiều dòng của Lua dựa trên [[...]], và có thể chèn số lượng = tùy ý giữa các dấu ngoặc vuông
    • Dạng như [==[ ... ]==] yêu cầu số lượng = ở đầu và cuối khớp nhau
    • Cách tương tự cũng dùng được cho chú thích
    • Đều có thể viết theo dạng --[[ ... ]]--[==[ ... ]==]

Assembly: kết hợp phương ngữ và preprocessor

  • Assembly là một trong những ngôn ngữ khó tô sáng vì bị chia thành nhiều phương ngữ
  • llamafile hướng tới xử lý hợp lý nhiều cú pháp như AT&T, nasm
  • Nếu coi từ khóa là định danh đầu tiên trên dòng và không đứng sau dấu hai chấm, thì phần lớn mã assembly sẽ trông ổn
  • Cú pháp chú thích cũng không hề đơn giản
    • Chú thích UNIX nguyên gốc chỉ cần một ký tự /
    • GNU as vẫn hỗ trợ kiểu chú thích này nhưng chỉ ở đầu dòng
    • Clang không hỗ trợ fixed comment nên theo tác giả khó dùng thực tế trong mã nguồn mở
  • Assembler UNIX nguyên gốc không dùng dấu nháy đóng cho literal ký tự
    • 'x biểu thị giá trị 0x78 của ký tự x
    • GNU as vẫn hỗ trợ nhưng LLVM thì không
    • Vì cú pháp này còn có trong mã cũ nên trình tô sáng tốt cũng nên hỗ trợ
  • GNU assembler cho phép định danh được trích dẫn, nên symbol có thể chứa gần như mọi ký tự
  • Assembly thường được dùng kèm C preprocessor hoặc m4
    • Các dòng bắt đầu bằng dnl, m4_dnl, C cũng cần được xem là chú thích

Ada và BASIC: những cú pháp nhỏ làm rung chuyển lexing

  • Ada khá đơn giản để lexing, nhưng cách dùng dấu nháy đơn lại khác thường
    • Nó có thể có literal ký tự kiểu C như 'x'
    • Nó cũng dùng dấu nháy đơn để tham chiếu thuộc tính như Foo'Size
    • Character'(')')'Image khai báo một ký tự rồi chuyển nó thành biểu diễn chuỗi thông qua hàm Image
  • Ví dụ Commodore BASIC phá vỡ nhiều giả định của tô sáng cú pháp
    • Chuỗi có thể bỏ dấu nháy đóng ở cuối dòng
    • Tên biến có thể mang sigil như $
    • Các từ khóa như goto vẫn được lexing tích cực ngay cả bên trong định danh
  • Visual BASIC có cú pháp literal ngày tháng như #1/1/2024#
  • Visual BASIC cũng có các chỉ thị preprocessor như #If DEBUG Then, #Else, #End If, khiến việc lexing phức tạp hơn

Perl: độ phức tạp nằm giữa Shell và ngôn ngữ lập trình

  • Perl nằm giữa Shell và ngôn ngữ lập trình, nên thừa hưởng độ phức tạp của cả hai phía
  • Nó biến biểu thức chính quy thành yếu tố hạng nhất của ngôn ngữ, và ảnh hưởng đó còn lan sang các ngôn ngữ khác như Python
  • Cú pháp thay thế của Perl giống sed, chẳng hạn s/hello/Perl/i
    • Có thể dùng bất kỳ ký tự dấu câu nào làm dấu phân cách thay cho /
    • Dạng như s!hello!Perl!i tiện khi regex chứa dấu gạch chéo
    • Nếu dùng ký tự có cặp như s{hello}{Perl}i thì cần thêm ký tự tương ứng
  • Perl có nhiều tiền tố ma thuật cần được tô sáng như chuỗi
    • m, s, y, qr, qw, qq, qx đều có thể đi cùng nhiều kiểu dấu phân cách
  • Để không nhầm y/x/y/ là phép chia, cần xét ngữ cảnh
    • Biến Perl có sigil như vô hướng $, mảng @, băm %, nên có thể giúp phân biệt mà chưa cần phân tích toàn bộ cú pháp
  • Perl có thông lệ chèn tài liệu POD cho man page ngay trong mã nguồn
    • =word ở đầu dòng sẽ bắt đầu tài liệu POD và =cut sẽ kết thúc nó

Ruby: đối tượng lexing khó nhất

  • Ruby giống như hợp của các ngôn ngữ trước đó, và theo tác giả thì cú pháp của nó chưa được tài liệu hóa đủ hình thức
  • Tài liệu syntax của Ruby bị đánh giá là khá nhẹ về chi tiết
  • Ruby hỗ trợ cú pháp backquote nhưng đồng thời cũng cho phép dùng backquote như tên phương thức, khiến trình tô sáng khó xác định đó có phải chuỗi hay không
  • Ruby vừa có toán tử << vừa có heredoc
    • Trong mã thực tế có dạng như options[:includes] <<arg; true
    • Đoạn này có thể trông giống heredoc và ngay cả Emacs cũng xử lý sai
  • Mã như puts "This is #{<<HERE.strip} evil" với heredoc nằm bên trong nội suy chuỗi cũng là hợp lệ
  • Trong 42 ngôn ngữ, Ruby bị xem là cú sốc lớn nhất; tác giả cho rằng Ruby có thể không thể lexing đúng nếu không parsing, và ngay cả khi parsing thì cũng khó chắc có diễn giải đúng hay không

Độ phức tạp nhìn qua số dòng mã triển khai

  • Nếu lấy số dòng mã triển khai của từng trình tô sáng theo ngôn ngữ làm thước đo, FORTH là đơn giản nhất còn Ruby là phức tạp nhất
  • Triển khai ngắn nhất là highlight_forth.cpp với 125 dòng
  • Các triển khai tương đối ngắn còn có m4 132 dòng, Ada 149 dòng, LISP 160 dòng, MATLAB 166 dòng, COBOL 186 dòng, BASIC 199 dòng, FORTRAN 200 dòng
  • Các triển khai cỡ trung gồm JavaScript 337 dòng, TypeScript 371 dòng, Kotlin 387 dòng, Scala 387 dòng, Assembly 447 dòng, C 449 dòng, Swift 455 dòng, D 521 dòng
  • Các triển khai dài nhất là Shell 570 dòng, Perl 583 dòng, highlight_ruby.cpp 1042 dòng

1 bình luận

 
GN⁺ 2024-11-03
Ý kiến trên Hacker News
  • Thứ tôi thích nhất trong các trigraph của C là dạng như do_action() ??!??! handle_error()
    Nhìn thì giống một cú pháp xử lý lỗi đặc biệt, nhưng khi biết rằng ??!??! được đổi thành toán tử OR logic ||, và theo quy tắc đánh giá ngắn mạch thì cấu trúc này sẽ chạy handle_error() khi do_action() trả về giá trị khác 0, nên thấy khá thỏa mãn

  • Đọc rất thú vị, nhưng ngược lại lại thấy đồng cảm hơn với góc nhìn phía Lisp
    Theo tôi hiểu, lập trường đó là cú pháp không phải phần quá quan trọng của ngôn ngữ, và vì dễ trở thành chướng ngại hơn là trợ giúp, nên nó nên đơn giản và đồng nhất nhất có thể để ta tập trung vào những thứ khác
    Tuy vậy, việc học chỉnh sửa có cấu trúc trong Lisp cho đến nay với tôi giống chướng ngại hơn là trợ giúp, và tôi nghĩ một ngày nào đó nó sẽ được đền đáp

    • Cú pháp đơn giản có thể ổn với máy tính, nhưng cú pháp chủ yếu được thiết kế để con người đọc và viết
      Nếu làm đơn giản như Lisp thì các cuộc tranh luận về cú pháp chỉ bị đẩy sang vấn đề ngữ nghĩa, tức chỉ là đổi tầng mà thôi
      Tôi cho rằng cú pháp phức tạp dễ đọc và viết hơn nhiều so với cú pháp đơn giản có ngữ nghĩa phức tạp. Lỗi cú pháp nhận phản hồi nhanh, còn lỗi ngữ nghĩa có thể ẩn đến tận lúc chạy
    • Lisp có reader macro cho phép lập trình lại lexer, và quá trình biến cấu trúc nhìn thấy thành cây cú pháp cũng có thể được xử lý bằng macro
      Ví dụ có cả những thứ như https://pyret.org/. Thực ra khó nói Lisp là đơn giản hay nhất thiết đồng nhất
    • Tôi hơi ngạc nhiên khi chỉnh sửa có cấu trúc lại là chướng ngại; nếu đưa ra một lời khuyên thì chỉ cần dùng parinfer và biết key binding của ba lệnh slurp, barf, raise
      Chỉ với bốn thứ này đã có thể hưởng khoảng 95% lợi ích của paredit mà không phải chịu sự phức tạp, các mẹo còn lại có thể học sau khi đã quen
  • Có đoạn “không biết ai muốn tô sáng cú pháp C ở tốc độ 35MB/giây, nhưng giờ thì làm được rồi”; đúng là nhanh, nhưng tcc biên dịch C thành mã nhị phân với tốc độ 29MB/giây ngay cả trên máy tính rất cũ: https://bellard.org/tcc/#speed
    Có lẽ còn có thể nhanh hơn nữa, nhưng chắc không cần thiết

  • Có vài thứ tác giả đã bỏ sót
    Không chỉ TypeScript, Swift, Kotlin, Scala mới đi đến mức cực đoan là đưa mã thật vào nội suy chuỗi; C#, Python, JavaScript, Ruby, Shell, Make cũng hỗ trợ tương tự
    Với Tcl, ranh giới giữa mã và dữ liệu khó phân biệt nên { } thực chất là một kiểu dấu phân tách chuỗi đặc biệt, và trong xyzzy {#hello world}, trước thời điểm đánh giá thì không thể biết #hello world là chú thích hay là chuỗi
    PostgreSQL có chuỗi trích dẫn bằng dấu đô la rất tiện, nên như trong https://www.postgresql.org/docs/current/sql-syntax-lexical.h..., 'Dianne''s horse', $$Dianne's horse$$, $SomeTag$Dianne's horse$SomeTag$ có cùng ý nghĩa

    • Perl cũng làm được
      Một chuỗi như "I have $foo $bar's: @{[$bar x $foo]}" có thể in ra I have 5 x's: xxxxx
      Cú pháp @{[...]} tận dụng đặc tính của Perl là có thể nội suy mảng chứ không chỉ scalar; phần [...] bên trong tạo một tham chiếu mảng, còn @{...} bên ngoài giải tham chiếu nó. Trình thông dịch Perl cho phép mã tùy ý trong biểu thức bên trong đó
    • Tôi nghĩ nên xem đây là cú pháp nối chuỗi đặc biệt hơn là “mã nằm trong chuỗi”
      Khi đó có thể lồng nhau như "foo { toUpper("bar { x + y } bar") } foo", và nếu + là phép nối chuỗi thì về cơ bản nó giống "foo " + toUpper("bar " + (x + y) + " bar") + " foo"
      Tôi không biết có ngôn ngữ nào thực sự hoạt động như vậy không
    • Ruby đẩy chuyện này đến cực hạn
      Tôi thích Ruby, nhưng khó mà bênh vực những đoạn Ruby hợp lệ như puts "This is #{< khi trộn nội suy chuỗi với heredoc
      Ngoài ra trong Ruby, khoảng trắng cũng có thể là ký tự trích dẫn. Trong ngữ cảnh không có toán hạng bên trái, % bắt đầu một chuỗi trích dẫn và ký tự tiếp theo biểu thị loại trích dẫn, nên % hello trở thành chuỗi chứa hello. %(this is a string) hay %{this is a string} thì tốt, nhưng tôi chưa từng thấy dùng khoảng trắng ngoài thực tế, irb cũng không xử lý đúng, nên bỏ đi cũng được
    • Nội suy chuỗi của Scala cũng có thể dùng làm đối tượng để pattern match
      Viết val s"${a} + ${b}" = "1 + 2" thì a sẽ là 1, b sẽ là 2
    • Điểm hay của chuỗi nội suy trong C# là nó được đánh giá trì hoãn
      Trước đây, trong các vòng lặp nóng, mã như log.trace($"Entering iteration {i} for customer {c.ID} [{c.ShortName}]"); vẫn gọi string.Concat mỗi lần ngay cả trước khi logger thoát ra, nên nhiều logger đã tự triển khai cơ chế nội suy riêng
      Trong C#, có thể khai báo overload nhận DefaultInterpolatedStringHandler hoặc mẫu handler tùy chỉnh, và overload này được ưu tiên, nhờ đó có thể kiểm tra xem có thật sự cần ghi log không rồi mới trì hoãn việc tạo chuỗi
  • Có một điểm kỳ lạ khác về cú pháp, chưa được nhắc tới ở đây, có thể phá vỡ hầu hết các bộ tô sáng cú pháp
    Trong Java, Unicode escape có thể xuất hiện ở bất kỳ đâu, không chỉ bên trong chuỗi
    Ví dụ class Foo\u007b} là một lớp hợp lệ, và \u000A bên trong chú thích // có thể được xử lý như một ký tự xuống dòng thật, nên ví dụ assert cũng hoạt động khác với dự đoán

    • Việc không thể tô sáng cú pháp này đúng cách cũng có thể xem là vấn đề bảo mật
      Vì có thể kết thúc chú thích khối bằng Unicode escape, nên nếu muốn giấu mã độc trong phần chú thích của tệp nguồn Java, chỉ cần tạo một cái cớ hợp lý để có một khối Unicode escape bên trong chú thích là được
      Một lập trình viên không biết đặc tính này có khả năng sẽ thấy nó đã được chú thích và bỏ qua
    • Tôi chưa từng thấy chuyện này trong Java; liệu nó có công dụng thực tế nào không?
  • Có đoạn nói “Ruby là ngôn ngữ né tránh mọi nỗ lực hiểu cú pháp”, nhưng từ góc nhìn của TeX, vốn có lexer có thể được tái lập trình tùy ý, thì chuyện đó chỉ đáng yêu thôi

    • Reader macro của Lisp cũng có thể lập trình lexer
  • “Mọi lập trình viên C đều biết không thể đặt chú thích nhiều dòng bên trong chú thích nhiều dòng”, nhưng với lập trình viên Standard ML thì đây có thể là một hạn chế bất ngờ
    Chú thích lồng nhau như (* (* Nested (**) *) comment *) là hợp lệ, và sau đó val _ = print "hello, world\n" cũng chạy bình thường
    Nếu nghĩ đến việc C từng được xem là một ngôn ngữ giàu sức biểu đạt khi nó xuất hiện, thì việc chú thích lồng nhau không được đưa vào ngôn ngữ khá thú vị

    • Có ba điểm thú vị trong câu đó
      ML không có chú thích một dòng, nên đó cũng là một hạn chế gây ngạc nhiên tương tự; đây là lần đầu tôi nghe C được gọi là “giàu sức biểu đạt”, nhưng nếu so với assembly vào năm 1972 thì có thể đúng
      Và tôi không rõ cú pháp chú thích có liên quan gì tới sức biểu đạt của ngôn ngữ; theo định nghĩa thì tôi cho là hoàn toàn không
    • Để lex chú thích lồng nhau cần stack hoặc ít nhất là một bộ đếm mức lồng nhau
      Theo truyền thống, phân tích từ vựng được xem là lĩnh vực chỉ dùng automaton trạng thái hữu hạn như biểu thức chính quy, nên tính năng như vậy trông có vẻ nằm ngoài phạm vi của lexing
    • Trong C, dùng #if 0 cũng là một cách để lồng chú thích
      Có thể bọc như #if 0 ... #if 0 ... #endif ... #endif
    • Pascal cũng luôn hỗ trợ cú pháp chú thích lồng nhau như trong ví dụ
    • Điều này không chỉ áp dụng cho Standard ML mà cả ML nói chung
  • Tôi muốn thấy phần triển khai tô sáng cú pháp của joe dưới dạng có thể tái sử dụng
    Định dạng của https://joe-editor.sf.net/ đủ mạnh để tô sáng Python f-string đúng cách
    Tài liệu và ví dụ liên quan có ở https://github.com/cmur2/joe-syntax/blob/joe-4.4/misc/HowItW..., https://gist.github.com/irdc/6188f11b1e699d615ce2520f03f1d0d...

    • Dựa trên cách parsing kiểu DFA của joe, tôi đã thử tạo nhiều lexer và parser
      Cú pháp trạng thái và chuyển tiếp dễ hiểu hơn nhiều so với các công cụ tiêu chuẩn
      Nhược điểm là tập luật trở nên dài dòng và hơi khó cấu trúc lý tưởng, nhưng tôi lại xem đó là ưu điểm ở chỗ dễ suy ra mọi quy tắc sinh từ mã
    • Thú vị là Python f-string đã thay đổi cú pháp trong 3.12, nên việc tô sáng cũng phải khác nhau theo phiên bản
  • Có vẻ tác giả chưa thử tô sáng cú pháp TeX
    Điều đó có thể tốt cho sức khỏe tinh thần, vì với TeX, thông thường không thể tô sáng cú pháp hoàn chỉnh nếu không diễn giải
    Chỉ parsing thôi cũng chưa đủ, vì có thể định nghĩa lại việc từng ký tự làm gì, nên cũng có thể làm những trò như “từ giờ K là { và C là }
    Thực tế trong các bài báo arXiv cũng có những bài dùng tính năng bị nguyền rủa này

    • Tôi đã tạo https://github.com/Mozilla-Ocho/llamafile/blob/main/llamafil..., và với các tệp .tex tìm thấy trên ổ cứng thì nó tô sáng khá ổn mà không bị vỡ
      Mục tiêu là bao phủ 99,9% cách dùng trong thế giới thực. Như vậy thì những gì LLM có thể xuất ra phần lớn cũng có khả năng được bao phủ
      Các cú pháp bí hiểm thường không thành vấn đề lớn, miễn là chuỗi hoặc chú thích không kéo dài tới cuối tệp và che hết phần mã nguồn còn lại
    • Tôi đã rất khó tin khi biết \makeatletter không có nghĩa là “biến thứ gì đó thành ký tự at”, mà là khi parsing thì xem ký tự @ là một chữ cái
    • Common Lisp cũng có thể định nghĩa lại readtable theo cách tương tự. Tuy nhiên khả năng bị lạm dụng trên arXiv thì thấp hơn
  • Tôi nghĩ việc viết một engine tô màu cú pháp tốt như Vim không hề dễ
    Tô màu cú pháp cần xử lý ngữ cảnh, áp dụng các quy tắc khác nhau cho những nội dung được lồng nhau theo một cách nhất định
    Bộ tô sáng cú pháp của Vim cho phép khai báo hai loại mục là matchregion; match là quy tắc từ vựng đơn giản, còn region có các biểu thức để khớp riêng phần bắt đầu, kết thúc và phần giữa
    Một mục có thể được khai báo để chỉ kích hoạt khi nó nằm trong một vùng nhất định, và quan hệ bao hàm cũng có thể được khai báo qua lại
    Trên cấu trúc ngữ nghĩa cơ bản này còn có thêm các tính năng dành cho nhiều tình huống đặc biệt. Ngay cả Justine có lẽ cũng khó mà làm ngay tại buổi phỏng vấn; chắc phải là bài tập qua đêm thì hợp hơn

    • Một ví dụ khó xử lý là TXR Lisp được nhúng bên trong ngôn ngữ TXR
      Có script genman ở đây, nhận kết quả chuyển đổi manpage sang HTML rồi chỉnh thành HTML của tài liệu hướng dẫn TXR: https://www.kylheku.com/cgit/txr/tree/genman.txr
      Phần trông màu trắng là template literal, còn mã Lisp nằm trong các directive như @(do ...). Từ khóa TXR hiển thị màu tím, từ khóa TXR Lisp hiển thị màu xanh lá, và cùng một từ cũng khác nhau tùy theo ngữ cảnh
      Quasi-string cũng có thể chứa cú pháp lồng nhau, và bên trong đó lại có thể có mã nhúng hoặc quasi-string khác. Cả hai tệp định nghĩa cú pháp txr.vimtl.vim đều được tạo từ https://www.kylheku.com/cgit/txr/tree/genvim.txr
    • Ban đầu tôi nghĩ một cách ngây thơ rằng để làm đúng bộ tô sáng cú pháp thì chỉ cần parse thành AST, rồi duyệt các token và cập nhật màu theo loại node
      Nếu không làm như vậy thì có lẽ là vì vấn đề hiệu năng. Chẳng hạn, thay vì chỉ tô sáng phần đang hiển thị trên màn hình, phải parse toàn bộ tệp nên chi phí có thể lớn
    • Tôi sẽ không cá cược rằng Justine không làm được cái đó trong buổi phỏng vấn ;)