2P by GN⁺ | ★ favorite | 댓글 1개
  • llamafile용 새 구문 하이라이터를 만들기 위해 42개 언어를 다루자, 단순한 렉싱에도 언어별 예외와 오래된 문법이 촘촘히 숨어 있음이 드러남
  • 구현은 C++와 GNU gperf로 키워드 조회를 빠르게 만들고, 문자열·주석·키워드 중심 처리는 for 루프와 switch 기반 유한 상태 머신으로 해결함
  • C의 trigraph, JavaScript의 u2028·u2029 줄 종결자, Shell·Perl·Ruby의 heredoc, Kotlin·Scala·TypeScript·Swift의 문자열 보간은 렉싱만으로 처리하기 까다로운 사례로 반복 등장함
  • 코드 줄 수 기준으로 FORTH는 125줄, Ruby는 1042줄까지 벌어지며, Ruby는 << 연산자·heredoc·backquote가 충돌해 파싱 없는 하이라이팅이 특히 어려움
  • llamafile의 새 하이라이터는 Windows 10에서 Meta LLaMA 3.2 3B Instruct로 시연됐고 macOS·Linux·FreeBSD·NetBSD에서도 실행 가능해, 구문 하이라이팅이 없는 ollama와 차별화됨

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와 유한 상태 머신

  • 기본 구문 하이라이터에서 가장 큰 병목은 토큰이 키워드인지 판별할 때 반복적인 문자열 비교가 늘어나는 지점임
  • C++와 GNU gperf를 사용해 완전 해시 테이블을 생성함
    • 예시 gperf 입력은 true, false, null 같은 Java 상수를 키워드로 정의함
    • gperf가 생성한 C 파일은 충돌 없는 조회를 위해 단일 문자만 고려하는 해시 함수를 만들 수 있음
  • C 하이라이팅은 약 4,000개 키워드를 정의해도 gperf 덕분에 35 MB/s 속도로 처리 가능함
  • 나머지 처리는 대부분 유한 상태 머신으로 충분함
    • flex, bison, ragel 없이 for 루프와 switch만으로 기본 하이라이터를 만들 수 있음
    • 문자열, 주석, 키워드에 집중하면 대체로 렉싱 수준에서 처리됨
    • C 함수 이름 같은 요소까지 하이라이트하려면 실제 파싱이 필요할 수 있음
  • highlight_ada.cpp가 예시 구현으로 사용됨

llamafile 데모와 사용 환경

  • 새 하이라이터와 챗봇 인터페이스는 llamafile 사용성을 높였고, ollama에는 없는 구문 하이라이팅이 차별점이 됨
  • 데모는 Windows 10에서 Meta LLaMA 3.2 3B Instruct 모델로 실행됨
  • 해당 llamafile은 macOS, Linux, FreeBSD, NetBSD에서도 실행 가능함
  • gemma 27b it 같은 오픈 가중치 모델의 품질이 좋아지면서 Claude를 사용할 유인이 줄어듦

C: 단순한 언어라는 인상과 다른 렉싱 예외

  • C는 단순하다는 인상과 달리 렉시컬 요소가 매우 특이한 편임
  • trigraph#, [, \, ^, {, |, }, ~ 같은 문자를 ??=, ??(, ??/, ??), ??', ??<, ??!, ??>, ??-로 대체할 수 있음
    • C23 표준에서 제거됐지만, 레거시 소프트웨어 때문에 컴파일러는 계속 지원할 것으로 보임
    • 좋은 구문 하이라이터라면 이 문법도 처리해야 함
  • C의 universal characterint \uFEB2 = 1;처럼 ASCII 소스 코드 안에서 유니코드 식별자를 표현할 수 있음
    • GCC는 표준 위원회가 허용한 특정 유니코드 plane이 아니면 오류를 냄
  • C의 한 줄 주석은 줄 끝 백슬래시를 사용하면 여러 줄에 걸칠 수 있음
    • Perl, Ruby, Shell처럼 소스 코드에서 백슬래시 이스케이프를 허용하는 언어도 이 C식 동작은 지원하지 않음
    • Tcl과 GNU Make는 이 동작을 지원하는 것으로 파악됨
    • Emacs와 Pygments는 이를 틀리게 처리하는 경우가 있고, Vim은 백슬래시 처리를 항상 맞게 하는 것으로 보임
  • C에는 null preprocessor directive도 있음
    • 초기 v6 소스 코드의 .c 파일은 # 한 줄로 시작하는 경우가 많음
    • 현재도 유효한 코드이며, cc -C -E로 주석 보존을 요청해도 전처리기가 특정 주석을 제거하게 만드는 용도로 쓸 수 있음

주석 문법: Haskell과 D

  • C에서는 여러 줄 주석 안에 다시 여러 줄 주석을 중첩할 수 없음
  • Haskell은 {- ... {- ... -} ... -} 형태로 중첩 주석을 지원함
  • D는 C의 //, /* ... */ 주석을 그대로 채택하고, 별도로 /+ ... +/ 형태의 재귀 주석 문법을 도입함
  • D의 렉시컬 문법 문서는 형식적이고 상세해 구현에 필요한 정보를 잘 제공함
    • D lexical syntax 문서는 hex 문자열과 heredoc 문자열 같은 세부 사항을 다룸
    • D에는 "...", backtick 문자열, r"...", q"...", x"..." 등 다양한 문자열 형태가 있음

Tcl과 JavaScript: 따옴표와 보이지 않는 줄 종결자

  • Tcl에서는 식별자 안에 따옴표가 들어갈 수 있음
    • puts a"ba"b를 출력함
    • 변수 이름에도 따옴표를 넣을 수 있지만, 참조할 때는 $a"b가 아니라 ${a"b} 표기를 사용해야 함
  • JavaScript는 정규식을 위한 내장 렉시컬 문법을 갖고 있음
    • /[/]/g처럼 문자 집합 대괄호 안의 /는 이스케이프하지 않아도 됨
    • 닫는 슬래시만 단순히 스캔하는 방식은 minified 코드에서 틀릴 수 있음
  • ECMAScript는 u2028 LINE SEPARATOR와 u2029 PARAGRAPH SEPARATOR를 줄 종결자로 정의함
    • 이 문자는 사실상 \n처럼 동작함
    • Trojan Source 문자이므로 Emacs에서는 각각 , 로 보이도록 설정함
    • 많은 소프트웨어는 이 문자를 인식하지 못해 물음표로 렌더링하는 경우가 있음
    • D를 제외하면 이런 처리를 하는 다른 언어는 알지 못한다고 함
  • 이 특성을 이용해 SectorLISP에서 C와 JavaScript polyglot을 만들 수 있었음
    • lisp.js는 브라우저에서 실행되고 GCC로 컴파일해 로컬에서도 실행될 수 있음
    • llamafile은 이런 코드를 올바르게 하이라이트하지만, 다른 하이라이터에서는 같은 처리를 찾지 못했다고 함

Shell과 heredoc의 모서리 사례

  • Shell의 heredoc은 cat <<EOF ... EOF 형태로 여러 줄 문자열을 작성함
  • quoted heredoc 문법인 cat <<'END'는 변수 치환을 비활성화함
  • heredoc 마커를 빈 문자열로 두면 다음 빈 줄에서 heredoc이 끝남
    • 예시 프로그램은 helloworld를 두 줄로 출력함
  • Shell, Ruby, Perl처럼 heredoc을 지원하는 언어에서는 같은 줄에 여러 heredoc을 둘 수 있음
  • Shell은 Tcl처럼 #가 항상 주석을 시작하지 않음
    • ${x#hi-}에서 #는 접두사를 제거하는 데 쓰이며, 예시는 there를 출력함

문자열 보간: 상태 스택이 필요한 언어들

  • Kotlin 문자열은 "로 시작했더라도 문자열 보간 때문에 {에서 다른 상태로 들어갈 수 있음
  • TypeScript, Swift, Kotlin, Scala는 문자열 안에 실제 코드를 넣는 방향으로 문자열 보간을 강하게 지원함
  • Kotlin, Scala, TypeScript를 하이라이트하려면 중괄호를 세고 파서 상태 스택을 유지해야 함
    • TypeScript는 비교적 단순해 유한 상태 머신에 몇 가지 상태만 추가하면 됨
    • Kotlin과 Scala는 double quote와 triple quote 문법 모두에서 보간을 지원하므로 문자열 렉싱에만 약 13개 독립 상태가 필요했음
    • Swift는 "\(var)" 보간 문법과 triple quote를 지원하지만, 구현에는 10개 상태가 필요했음

Swift, C#, FORTH: 문자열을 감싸는 방식의 차이

  • Swift는 "...", """...""", /regex/ 문자열을 임의 개수의 #로 감싸는 문법을 지원함
    • 양쪽의 # 개수는 서로 맞아야 함
    • 이 방식은 문자열 안에 따옴표나 정규식 경계를 넣는 문제를 해결함
  • C#은 Python의 triple quote 다중 줄 문자열과 비슷하지만, 더 많은 개수의 따옴표를 시작과 끝에 맞춰 사용할 수 있음
    • 왼쪽에 둔 따옴표 개수가 오른쪽 종료 조건이 됨
    • 임의 개수의 따옴표를 허용하면 고전적인 Python triple quote보다 유효성 규칙이 줄어 유한 상태 머신으로 더 단순하게 디코딩할 수 있다고 봄
  • FORTH는 공백 경계로 모든 것을 토큰화함
    • 문자열 시작 문법인 c"도 하나의 토큰임
    • c" hello world"는 다른 언어의 "hello world"와 같은 의미임

FORTRAN과 COBOL: 고정 컬럼 규칙

  • llamafile은 FORTRAN과 COBOL 프로그래머가 은퇴한 뒤에도 은행 시스템 유지보수에 도움을 줄 수 있는 사용 사례로 제시됨
  • 에어갭 환경에서 제어 가능한 AI인 Gemma 27b에 COBOL과 FORTRAN 코드를 요청할 수 있다고 함
  • FORTRAN의 고정 컬럼 규칙은 다음과 같음
    • 1열에 *, c, C를 두면 그 줄은 주석이 됨
    • 6열에 공백이 아닌 문자를 두면 80자를 넘는 줄을 이어 쓸 수 있음
    • 1~5열에 숫자를 두면 라벨이 됨
  • COBOL의 규칙은 다음과 같음
    • 7열에 *를 두면 주석이 됨
    • 7열에 -를 두면 80자를 넘는 줄을 이어 쓸 수 있음
    • 1~6열에는 줄 번호가 들어감

Zig와 Lua: 다중 줄 문자열의 다른 해법

  • Zig는 두 개의 백슬래시로 시작하는 다중 줄 문자열 문법을 가짐
    • 이 문법은 Python triple quote 문자열에서 textwrap.dedent()를 호출해야 하는 필요를 없앰
    • 단점으로 세미콜론이 보기 좋지 않다고 봄
    • Go, Scala, Python처럼 세미콜론이 필요 없는 언어가 고려할 만한 문자열 문법으로 제안됨
  • Lua의 다중 줄 문자열은 [[...]]를 기반으로 하며, 대괄호 사이에 임의 개수의 =를 넣을 수 있음
    • [==[ ... ]==]처럼 시작과 끝의 = 개수를 맞춤
    • 같은 방식을 주석에도 사용할 수 있음
    • --[[ ... ]]--[==[ ... ]==] 형태가 모두 가능함

Assembly: 방언과 전처리기의 조합

  • Assembly는 여러 방언이 갈라져 있어 하이라이트하기 어려운 언어 중 하나임
  • llamafile은 AT&T, nasm 등 여러 문법을 합리적으로 처리하는 것을 목표로 함
  • 키워드는 줄에서 첫 번째 식별자이며 콜론 뒤에 오지 않는 항목으로 처리하면 대부분의 assembly 코드를 괜찮게 보이게 할 수 있음
  • 주석 문법도 단순하지 않음
    • 원래 UNIX 주석은 단일 /만 필요했음
    • GNU as는 줄 시작 위치에서만 이 주석을 아직 지원함
    • Clang은 fixed comment를 지원하지 않아 오픈소스 코드에서 실용적으로 쓰기 어렵다고 함
  • 원래 UNIX assembler는 문자 리터럴에 닫는 따옴표를 쓰지 않았음
    • 'x가 문자 x의 값 0x78을 나타냄
    • GNU as는 이를 계속 지원하지만 LLVM은 지원하지 않음
    • 기존 코드에 이 문법이 있으므로 좋은 하이라이터는 지원해야 함
  • GNU assembler는 인용된 식별자를 허용해 심볼에 거의 모든 문자를 넣을 수 있음
  • Assembly는 C 전처리기나 m4와 함께 쓰이는 경우가 많음
    • dnl, m4_dnl, C로 시작하는 줄도 주석으로 처리해야 함

Ada와 BASIC: 작은 문법이 렉싱을 흔드는 사례

  • Ada는 렉싱하기 상당히 단순하지만, 작은따옴표 사용이 특이함
    • C처럼 'x' 문자 리터럴을 가질 수 있음
    • Foo'Size처럼 속성을 참조할 때도 작은따옴표를 사용함
    • Character'(')')'Image는 문자를 선언하고 Image 함수를 통해 문자열 표현으로 변환함
  • Commodore BASIC 예시는 구문 하이라이팅의 여러 가정을 깨뜨림
    • 문자열은 줄 끝에서 닫는 따옴표를 생략할 수 있음
    • 변수 이름에는 $ 같은 sigil이 붙음
    • goto 같은 키워드는 식별자 안에서도 적극적으로 렉싱됨
  • Visual BASIC에는 #1/1/2024# 같은 날짜 리터럴 문법이 있음
  • Visual BASIC은 #If DEBUG Then, #Else, #End If 같은 전처리기 지시문도 갖고 있어 렉싱이 까다로움

Perl: Shell과 프로그래밍 언어 사이의 복잡성

  • Perl은 Shell과 프로그래밍 언어 사이에 있어 양쪽의 복잡성을 모두 물려받음
  • 정규식을 언어의 일급 요소로 만들었고, 그 영향은 Python 등 다른 언어에도 이어짐
  • Perl의 치환 문법은 s/hello/Perl/i처럼 sed와 유사함
    • / 대신 임의의 구두점 문자를 구분자로 쓸 수 있음
    • s!hello!Perl!i처럼 슬래시가 정규식 안에 들어갈 때 편리함
    • s{hello}{Perl}i처럼 짝이 있는 문자를 쓰면 추가 문자가 필요함
  • Perl에는 문자열처럼 하이라이트해야 하는 마법 접두사가 많음
    • m, s, y, qr, qw, qq, qx 등이 다양한 구분자와 함께 쓰임
  • y/x/y/를 나눗셈으로 잘못 판단하지 않으려면 문맥을 봐야 함
    • Perl 변수는 스칼라 $, 배열 @, 해시 % 같은 sigil을 갖기 때문에 문법 전체를 파싱하지 않고도 구분에 도움이 됨
  • Perl은 소스 코드 안에 man page용 POD 문서를 넣는 관례가 있음
    • 줄 시작의 =word가 POD 문서를 시작하고 =cut이 끝냄

Ruby: 가장 어려웠던 렉싱 대상

  • Ruby는 이전 언어들의 합집합처럼 보이며, 문법이 형식적으로 충분히 문서화돼 있지 않다고 평가됨
  • Ruby 매뉴얼의 syntax 문서는 세부 사항이 가벼운 편으로 봄
  • Ruby는 backquote 문법을 지원하면서 동시에 backquote를 메서드 이름처럼 사용할 수 있어 하이라이터가 문자열인지 아닌지 판단하기 어려움
  • Ruby에는 << 연산자와 heredoc이 모두 있음
    • 실제 코드에서 options[:includes] <<arg; true 같은 형태가 존재함
    • 이 코드는 heredoc처럼 보일 수 있으며 Emacs도 이를 잘못 처리함
  • puts "This is #{<<HERE.strip} evil"처럼 문자열 보간 안에 heredoc이 들어가는 코드도 유효함
  • 42개 언어 중 Ruby가 가장 큰 충격으로 꼽혔고, Ruby는 파싱 없이 렉싱할 수 없을 수 있으며 파싱을 해도 해석 가능 여부를 확신하기 어렵다고 봄

구현 코드 줄 수로 본 복잡도

  • 각 언어 하이라이터 구현의 코드 줄 수를 기준으로 하면 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줄임

댓글과 토론

Hacker News 의견들
  • C 삼중자(trigraph) 중 제일 마음에 드는 건 do_action() ??!??! handle_error() 같은 형태임
    특수한 오류 처리 문법처럼 보이지만, 알고 보면 ??!??!|| 논리 OR로 바뀌고 단락 평가 규칙으로 do_action()이 0이 아닌 값을 반환할 때 handle_error()를 실행하는 구조라 만족스러움

  • 재미있게 읽었지만 오히려 Lisp 쪽 관점에 더 공감하게 됨
    이해한 바로는 문법은 언어에서 그다지 중요한 부분이 아니고, 도움보다 장애물이 되기 쉬우니 최대한 단순하고 균일해야 다른 것에 집중할 수 있다는 입장임
    다만 Lisp에서 구조적 편집을 배우는 건 지금까지는 도움보다 장애물에 가까웠고, 언젠가는 보상이 있겠지 싶음

    • 단순한 문법은 컴퓨터에는 괜찮을 수 있지만, 문법은 주로 사람이 읽고 쓰도록 설계됨
      Lisp처럼 단순하게 만들면 문법 논의가 의미론 문제로 밀려날 뿐이고, 계층만 바뀌는 셈임
      복잡한 문법이 복잡한 의미론을 가진 단순 문법보다 읽고 쓰기 훨씬 쉽다고 봄. 문법 오류는 빠르게 피드백을 받지만 의미 오류는 실행 시점까지 숨어 있을 수도 있음
    • Lisp에는 렉서를 재프로그래밍할 수 있는 리더 매크로가 있고, 보이는 구조를 구문 트리로 바꾸는 과정도 매크로로 다룰 수 있음
      예를 들면 https://pyret.org/ 같은 것도 있음. 실제로 Lisp가 단순하거나 반드시 균일하다고 보긴 어려움
    • 구조적 편집이 장애물이었다니 조금 놀랍고, 조언을 하나 하자면 parinfer를 쓰고 slurp, barf, raise 세 명령의 키 바인딩만 알면 됨
      이 네 가지만으로도 paredit의 장점을 복잡함 없이 95% 정도 누릴 수 있고, 나머지 요령은 익숙해진 뒤 배워도 됨
  • “누가 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에는 편리한 달러 인용 문자열이 있어서 https://www.postgresql.org/docs/current/sql-syntax-lexical.h...처럼 'Dianne''s horse', $$Dianne's horse$$, $SomeTag$Dianne's horse$SomeTag$가 같은 의미가 됨

    • Perl도 가능함
      "I have $foo $bar's: @{[$bar x $foo]}" 같은 문자열은 I have 5 x's: xxxxx를 출력할 수 있음
      @{[...]} 문법은 스칼라뿐 아니라 배열 보간도 가능한 Perl의 특성을 이용한 것이고, 안쪽 [...]는 배열 참조를 만들며 바깥 @{...}가 이를 역참조함. Perl 인터프리터는 그 안쪽 표현식에 임의 코드를 허용함
    • “문자열 안에 코드가 들어간다”기보다는 특수한 문자열 연결 문법으로 보는 편이 맞다고 생각함
      그러면 "foo { toUpper("bar { x + y } bar") } foo"처럼 중첩할 수 있고, +가 문자열 연결이라면 "foo " + toUpper("bar " + (x + y) + " bar") + " foo"와 사실상 같아짐
      실제로 이렇게 동작하는 언어가 있는지는 모르겠음
    • Ruby는 이걸 극한까지 밀어붙임
      Ruby를 좋아하지만, 문자열 보간과 heredoc을 섞은 puts "This is #{< 같은 유효한 Ruby는 변호하기 어려움
      또 Ruby에서는 공백도 인용 문자가 될 수 있음. 왼쪽 피연산자가 없는 문맥에서 %는 인용 문자열을 시작하고 다음 문자가 인용 종류를 나타내므로 % hello hello를 담은 문자열이 됨. %(this is a string)이나 %{this is a string}는 좋지만 공백을 쓰는 건 야생에서 본 적도 없고, irb도 제대로 처리하지 못하니 제거돼도 좋겠음
    • Scala의 문자열 보간은 패턴 매치 대상으로도 쓸 수 있음
      val s"${a} + ${b}" = "1 + 2"를 쓰면 a1, b2가 됨
    • C# 보간 문자열의 멋진 점은 지연 평가된다는 것임
      예전에는 뜨거운 루프에서 log.trace($"Entering iteration {i} for customer {c.ID} [{c.ShortName}]"); 같은 코드가 로거가 빠져나가기 전에도 매번 string.Concat을 호출했기 때문에 많은 로거가 자체 보간을 구현했음
      C#에서는 DefaultInterpolatedStringHandler나 커스텀 핸들러 패턴을 받는 오버로드를 선언할 수 있고, 이 오버로드가 우선되어 실제 로그가 필요한지 확인한 뒤 문자열 생성을 늦출 수 있음
  • 여기서 언급되지 않은, 대부분의 구문 강조기를 깨뜨리는 또 다른 문법 특이점이 있음
    Java에서는 유니코드 이스케이프가 문자열 안뿐 아니라 어디에나 올 수 있음
    예를 들어 class Foo\u007b}는 유효한 클래스이고, // 주석 안의 \u000A가 실제 줄바꿈으로 처리될 수 있어서 assert 예시도 예상과 다르게 동작함

    • 이걸 제대로 구문 강조하지 못하는 건 보안 문제라고도 봄
      유니코드 이스케이프로 블록 주석을 종료할 수 있으니, Java 소스 파일 안 주석에 악성 코드를 숨기려면 주석 안에 유니코드 이스케이프 블록이 있을 만한 핑계만 만들면 됨
      이 특성을 모르는 개발자는 주석 처리된 것으로 보고 그냥 넘길 가능성이 큼
    • Java에서 이런 건 한 번도 못 봤는데, 실제로 유용한 용도가 있나?
  • “Ruby는 문법 이해 시도를 모두 피해 가는 언어”라는 대목이 있지만, 임의로 재프로그래밍 가능한 렉서를 가진 TeX 입장에서는 귀여운 수준임

    • Lisp 리더 매크로도 렉서를 프로그래밍할 수 있음
  • “모든 C 프로그래머는 여러 줄 주석 안에 여러 줄 주석을 넣을 수 없다는 걸 안다”지만, Standard ML 프로그래머에게는 의외의 제한일 수 있음
    (* (* Nested (**) *) comment *) 같은중첩 주석은 유효하고, 뒤이어 val _ = print "hello, world\n"도 정상 실행됨
    C가 등장했을 때 표현력 있는 언어로 여겨졌던 걸 생각하면, 중첩 주석이 언어에 들어가지 않은 점이 흥미로움

    • 그 말에서 재미있는 점이 세 가지 있음
      ML에는 한 줄 주석이 없었으니 그것도 비슷하게 놀라운 제한이고, C를 “표현력 있다”고 부르는 건 처음 듣지만 1972년에 어셈블리와 비교하면 그랬을 수도 있음
      그리고 주석 문법이 언어의 표현력과 무슨 관련이 있는지는 모르겠고, 정의상 전혀 없다고 봄
    • 중첩 주석을 렉싱하려면 스택이나 최소한 중첩 수준 카운터가 필요함
      전통적으로 어휘 분석은 정규식처럼 유한 상태 오토마턴만 쓰는 영역으로 여겨졌기 때문에, 그런 기능은 렉싱의 범위 밖으로 보였음
    • C에서도 #if 0을 쓰면 주석을 중첩하는 방법이 하나 있긴 함
      #if 0 ... #if 0 ... #endif ... #endif처럼 감쌀 수 있음
    • Pascal도 예시와 같은 중첩 주석 문법을 항상 지원했음
    • 이건 Standard ML뿐 아니라 일반 ML에도 해당됨
  • 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...에 있음

    • joe의 DFA 방식 파싱을 바탕으로 렉서와 파서를 여러 개 만들어 봤음
      상태와 전이 문법은 표준 도구들보다 훨씬 이해하기 쉬웠음
      단점은 규칙 집합이 장황해지고 이상적인 구조화가 조금 어려워진다는 점인데, 코드에서 모든 생성 규칙을 추론하기 쉬워진다는 점에서는 오히려 장점이라고 봄
    • 흥미롭게도 Python f-string은 3.12에서 문법이 바뀌었기 때문에, 강조도 버전에 따라 달라져야 함
  • 글쓴이는 TeX 구문 강조를 시도해 보지 않은 듯함
    정신 건강에는 좋은 일일 수 있는데, TeX는 보통 해석하지 않고는 완전한 구문 강조가 불가능
    파싱만으로도 부족한 게, 각 문자가 하는 일을 재정의할 수 있어서 “이제 K는 {이고 C는 }” 같은 짓도 가능함
    실제로 arXiv 논문 중에도 이 저주받은 기능을 쓰는 것들이 있음

    • https://github.com/Mozilla-Ocho/llamafile/blob/main/llamafil...를 만들었고, 하드 드라이브에서 찾은 .tex 파일들에 대해서는 깨지지 않고 꽤 괜찮게 강조함
      목표는 현실 세계 사용의 99.9% 를 커버하는 것임. 그러면 LLM이 출력할 만한 것도 대체로 커버될 가능성이 큼
      난해한 문법도 문자열이나 주석이 파일 끝까지 이어져 나머지 소스 코드를 덮어버리지만 않으면 보통 큰 문제는 아님
    • \makeatletter가 “무언가를 at 문자로 만든다”가 아니라, 파싱할 때 @ 문자를 문자로 취급한다는 뜻이라는 걸 알고 믿기 어려웠음
    • Common Lisp도 비슷하게 읽기 테이블을 재정의할 수 있음. 다만 arXiv에서는 덜 남용될 가능성이 큼
  • Vim 같은 좋은 구문 색칠 엔진을 작성하는 건 쉽지 않다고 봄
    구문 색칠은 특정 방식으로 중첩된 내용에 서로 다른 규칙을 적용하는 문맥 처리가 필요함
    Vim 구문 강조기는 matchregion 두 종류의 항목을 선언하게 해주고, match는 단순한 어휘 규칙이며 region은 시작·끝·중간을 각각 매칭하는 표현식을 가짐
    항목은 자신이 특정 영역 안에 있을 때만 활성화되도록 선언할 수 있고, 포함 관계도 서로 선언할 수 있음
    이 기본 의미 구조 위에 여러 특수 상황을 위한 기능들이 덧붙어 있음. Justine이라도 면접 자리에서 바로 만들기는 어렵고, 하룻밤 과제 정도는 돼야 할 듯함

    • 다루기 어려운 예로 TXR 언어 안에 내장된 TXR Lisp가 있음
      manpage HTML 변환 결과를 받아 TXR 매뉴얼 HTML로 다듬는 genman 스크립트가 여기 있음: https://www.kylheku.com/cgit/txr/tree/genman.txr
      흰색으로 보이는 부분은 리터럴 템플릿이고, Lisp 코드는 @(do ...) 같은 지시문 안에 들어감. TXR 키워드는 보라색, TXR Lisp 키워드는 초록색으로 보이며 같은 단어라도 문맥에 따라 달라짐
      준문자열도 중첩 문법을 담을 수 있고, 그 안에 다시 내장 코드나 준문자열이 들어갈 수 있음. txr.vimtl.vim 구문 정의 파일은 둘 다 https://www.kylheku.com/cgit/txr/tree/genvim.txr에서 생성됨
    • 순진하게는 구문 강조기를 제대로 만들려면 AST로 파싱한 뒤 토큰을 순회하면서 노드 타입에 따라 색을 갱신하면 된다고 생각했음
      그렇게 하지 않는다면 효율 문제 때문일 듯함. 예를 들어 화면에 보이는 부분만 강조하는 대신 전체 파일을 파싱해야 해서 비용이 커질 수 있음
    • Justine이 면접에서 그걸 못 만들 거라는 데 돈을 걸지는 않겠음 ;)