구문 분석 없이 간단한 토큰화만으로 토큰 간 차이를 색으로 강조하는 ‘어휘적 차등 하이라이팅’을 소개하고, 이를 Words and Buttons에 적용하는 이유와 하이라이터 제너레이터, 자바스크립트용 준-구문 차등 하이라이터 예제를 보여준다.
This is Words and Buttons Online — 대화형 #튜토리얼, #데모, 그리고 #퀴즈 모음으로, #수학, #알고리즘, #프로그래밍에 관한 콘텐츠를 담고 있습니다.
2013년에 저는 원자력 발전소 자동화 일을 하고 있었습니다. 어느 부분이 기밀이었는지 아직도 확실하지 않아 자세히 말할 수는 없지만, 적어도 업무에 어셈블리 코드를 아주 많이 읽는 일이 포함되어 있었다고 말해도 체포되지는 않을 겁니다.
어셈블리를 읽는 일은 훈련되지 않은 사람이 생각하는 것만큼 어렵지는 않습니다. 사실, 누구나 조금은 어셈블리를 읽을 수 있습니다. 그렇다고 대량으로 읽는 게 쉽다는 뜻은 아닙니다. RCR, SHRD, WBINVD, CMPXCHG8B 같은 니모닉은 쓰기는 재미있지만, 읽기는 고역이죠.
더 나쁜 점은, 표준적인 구문 하이라이팅 접근이 전혀 도움이 되지 않는다는 겁니다. mov이 eax와 다르게 보이는 건 좋지만, 나는 pmulhw와 pmulhuw처럼 서로 비슷한 것끼리는 가능한 한 최대한 다르게 보였으면 합니다.
그래서 저는 다른 종류의 하이라이팅을 사용했습니다. 구문(syntax)이 아니라 ‘어휘적 차등 하이라이팅(lexical differential highlighting)’입니다. “어휘적”이라는 것은 진짜 구문 분석이 필요 없고, 원시적인 토큰화와 필터링만으로 충분하다는 뜻입니다. 그리고 “차등”이라는 것은 렉심(lexeme) 사이의 차이를 강조한다는 의미입니다. 이상적으로는, 어휘적 차이가 작을수록 색상의 차이가 더 커져야 합니다.
작동 방식은 이렇습니다.
movq %rax, %r14
.align 16, 0x90
.LBB0_6:
# =>This Inner Loop Header: Depth=1
movl -12(%r12,%rbx,4), %eax
addl $-1, %eax
imull %eax, %eax
movl -8(%r12,%rbx,4), %ecx
addl $-1, %ecx
imull %ecx, %ecx
addl %eax, %ecx
movl -4(%r12,%rbx,4), %eax
addl $-1, %eax
imull %eax, %eax
addl %ecx, %eax
movl (%r12,%rbx,4), %ecx
movl $1, %edx
subl %ecx, %edx
addl $-1, %ecx
imull %edx, %ecx
cmpl %ecx, %eax
jne .LBB0_7
# BB#17: # in Loop: Header=BB0_6 Depth=1
incl 4(%rsp)
.LBB0_7: # %.backedge
# in Loop: Header=BB0_6 Depth=1
addq $1, %rbx
cmpq $160000000, %rbx
jne .LBB0_6
지금은 2019년이고, 저는 이 아이디어로 다시 돌아왔습니다. 어셈블리뿐 아니라 Words and Buttons에 올린 대부분의 코드에도 어휘적 차등 하이라이팅을 사용하고 있습니다. 이렇게 하는 데에는 두 가지 이유가 있습니다. 첫째, 다른 언어에도 잘 동작합니다. 둘째, 트래픽을 절약합니다. 네, 지금 이 글도 포함해서요.
인용부호와 주석까지 고려해도 토크나이저는 대략 30줄 정도의 코드로 구현할 수 있습니다. 그리고 채색 함수는 그보다 더 작습니다. 그래서 구문 하이라이팅을 정적으로 돌리거나 서드파티 라이브러리를 의존성으로 끌어오는 대신, 저는 페이지마다 전용으로 색칠 코드를 간단히 다시 씁니다.
이렇게 하면 어떤 어셈블리 방언이든, 심지어 가장 생소하고 오래된 언어까지도 하이라이트할 수 있습니다. 그리고 인스턴스당 몇 KB면 충분합니다.
물론, 매번 하이라이터를 손으로 다시 쓰는 건 사치스러우니, 대신 그 일을 해 줄 생성기를 만들었습니다.
Separators: Quotes
Comments
Function name:
... 여기 하이라이터 코드가 생성됩니다...
Testing area: 마음껏 사용하세요. Words and Buttons의 다른 모든 코드와 마찬가지로 적절히 무허가(unlicense)되어 있습니다.
매우 생산적인 Reddit 토론 이후, 어휘 하이라이팅과 함께 동작하도록 약간의 구문 하이라이팅을 흉내 내 보았습니다. 그리고 성공했습니다! 여기 자바스크립트를 위한 준-구문-차등 하이라이터가 있습니다. 이 글의 코드도 스스로를 하이라이트합니다.
function colorized_with_js_highlighter(text) {
const separators = ['function ', ' if(', 'return ', 'var ', 'const ', ' for(',
'\n', ' ', '\t', '.', ',', ':', ';', '+', '-', '/', '*', '(', ')', '<', '>', '[', ']', '{', '}'];
const quotes = ['\'', '"'];
const comments = [['//', '\n'], ['/*', '*/']];
function painted_in(line, color) {
return line.length == 0 ? "" : "<span style=\"color:#" + color + "\">" + line + "</span>";
}
function colorized(token) {
var code_sum = 0;
for(var i = 0; i < token.length; ++i)
code_sum += ([1, 7, 11, 13][i % 4] * token.charCodeAt(i));
var zero_channel = code_sum % 3;
var color = '' + (zero_channel == 0 ? '3' : '') + (1 + (code_sum % 5) * 2)
+ (zero_channel == 1 ? '3' : '') + (4 + (code_sum % 2) * 5)
+ (zero_channel == 2 ? '3' : '');
return painted_in(token, color);
}
function separated(line, i) {
if(i == separators.length)
return colorized(line);
return line.split(separators[i]).map(function(subline) {
return separated(subline, i + 1);}).join(separators[i]);
}
function unquoted(line, i) {
if(i == quotes.length)
return separated(line, 0);
var chunk_no = 0;
return line.split('\\' + quotes[i]).join('\0').split(quotes[i]).map(function (chunk) {
return chunk.split('\0').join('\\' + quotes[i]);}).map(function (chunk) {
return ++chunk_no % 2 == 1 ? unquoted(chunk, i + 1) : painted_in(quotes[i] + chunk + quotes[i], "555");}).join('');
}
function uncommented(line, i) {
if(i == comments.length)
return unquoted(line, 0);
var chunks = line.split(comments[i][0]);
return uncommented(chunks[0], i + 1) + chunks.slice(1).map( function(chunk) {
var in_out_comment = chunk.split(comments[i][1]);
return painted_in(comments[i][0] + in_out_comment[0] + (in_out_comment.length > 1 ? comments[i][1] : ''), "555")
+ uncommented(in_out_comment.slice(1).join(comments[i][1]), i + 1);}).join('');
}
return uncommented(text, 0);
}