다시 첫 정거장
톡톡나 아직 할 말이 남았어. 빈칸 이야기야.
한줄이빈칸? 제1화에서 다 떨어졌잖아.
코드의 일생 07
빈칸과 줄바꿈은 언제 뜻이 될까?
첫 바퀴를 다 돈 한줄이가 다시 첫 정거장에 왔어.
톡톡에게는 아직 못다 한 이야기가 있대.
그렇게 생각했구나.
두 바구니를 같이 들여다보자.
톡톡나 아직 할 말이 남았어. 빈칸 이야기야.
한줄이빈칸? 제1화에서 다 떨어졌잖아.
한줄이빈칸을 빼면 어떻게 돼?
톡톡숫자가 이어져서 이번엔 12 한 조각이네.
한줄이그럼 줄을 바꿔 볼래. 1 하고, 다음 줄에 2.
톡톡줄바꿈은 떨어지지 않네. 바구니에 챙겨 둘게.
가지1 다음에 칸막이가 섰네. 여기서 한 문장이 끝났구나.
한줄이내 마지막 줄바꿈은 어디 갔어?
톡톡정리대에서 빠졌어. 어떤 줄바꿈이 칸막이가 되는지는 같이 찾아보자.
가지1 다음에 바로 2가 왔어. 칸막이가 없으니 어디서 끊어야 할지 모르겠어.
한줄이빈칸이 있었는데도?
톡톡빈칸은 둘을 갈라 놓고 떨어졌어. 내 바구니에는 안 들어가.
어느 틈이 무엇을 남기는지 직접 바꿔 보자.
작은 실험
글자 사이 틈에 무엇을 두느냐에 따라 두 바구니에는 무엇이 남을까?
한줄이의 글자 사이 틈에 무엇을 둘지 골라 봐.
톡톡의 바구니와 가지의 바구니에 무엇이 남는지 비교해 보자.
이 쪽은 컴파일러를 실행하지 않아. 바구니에 담긴 토큰과 검사 결과는 토파즈 5.21.1로 미리 남긴 기록이야.
리본은 파일에 쓴 줄바꿈이야. 마개는 글자를 다 읽은 자리야.
1 줄바꿈 2 줄바꿈 에는 리본이 두 개 있고, 둘째 리본 바로 뒤에 파일 끝 마개가 있어.
토파즈 5.21.1 기록
틈을 골라 봐.
틈에 붙이기, 빈칸, 줄바꿈 가운데 하나를 넣으면 아래 다섯 줄이 그 파일의 기록으로 바뀌어.
빈칸이 없으니 12가 한 토큰이 됐어. 줄 끝 줄바꿈은 정리대에서 빠졌어. 검사는 통과했어.
빈칸이 1과 2를 갈라 놓고 바구니에는 들어가지 않았어. 가지의 바구니에 칸막이가 없어서 검사가 멈췄어.
1 뒤 줄바꿈이 칸막이가 됐어. 맨 끝 줄바꿈은 빠졌어. 검사는 통과했어.
빈칸이 없으니 12가 한 토큰이 됐어. 줄 끝 줄바꿈은 정리대에서 빠졌어. 검사는 통과했어.
한줄이
톡톡의 바구니
정리대
가지의 바구니
검사
검사 통과
main.tpz: types-ok (1 module)
main.tpz: resolve-ok (1 module)
topaz check main.tpztopaz compiler observe inputs/g1/main.tpz --out-dir out/g1 --compiler self --terminal typeda1fb50e6c86fae1679ef3351296fd6713411a08cf8dd1790a4fd05fae86881640같은 판으로 같은 절차를 두 번 따랐어. 두 번의 기록은 바이트까지 같았어.
이 입력에서 두 판(5.21.0, 5.21.1)의 검사 출력과 종료 코드, 대조한 토큰·나무·진단 파일이 바이트까지 같았어.
원문 열기
빈칸이 1과 2를 갈라 놓고 바구니에는 들어가지 않았어. 가지의 바구니에 칸막이가 없어서 검사가 멈췄어.
한줄이
톡톡의 바구니
바구니에 들어가지 않은 빈칸
정리대
가지의 바구니
검사
검사가 멈췄어
error[TPZ2001]: expected a statement separator
--> main.tpz:1:3
|
1 | 1 2
| ^
main.tpz: 1 diagnostic
topaz check main.tpztopaz compiler observe inputs/g2/main.tpz --out-dir out/g2 --compiler self --terminal typedf251ddc12234e0da8d3b778bd0f7463fb477f16f47757f5617dc8b4ff4d4f14a1같은 판으로 같은 절차를 두 번 따랐어. 두 번의 기록은 바이트까지 같았어.
이 입력에서 두 판(5.21.0, 5.21.1)의 검사 출력과 종료 코드, 대조한 토큰·나무·진단 파일이 바이트까지 같았어.
원문 열기
1 뒤 줄바꿈이 칸막이가 됐어. 맨 끝 줄바꿈은 빠졌어. 검사는 통과했어.
한줄이
톡톡의 바구니
정리대
가지의 바구니
검사
검사 통과
main.tpz: types-ok (1 module)
main.tpz: resolve-ok (1 module)
topaz check main.tpztopaz compiler observe inputs/g3/main.tpz --out-dir out/g3 --compiler self --terminal typeda6e2b7a040683432de03a18fd8a1939a2fdf82585b364bfc874bdd4095c4cae10같은 판으로 같은 절차를 두 번 따랐어. 두 번의 기록은 바이트까지 같았어.
이 입력에서 두 판(5.21.0, 5.21.1)의 검사 출력과 종료 코드, 대조한 토큰·나무·진단 파일이 바이트까지 같았어.
원문 열기
토파즈 5.21.1 기록
+ 앞과 뒤, 어느 틈을 줄바꿈으로 바꿔 볼래?
틈을 골라 봐.
두 틈에 각각 빈칸이나 줄바꿈을 넣어 봐. 바구니 둘을 견주면 차이가 보여.
줄바꿈은 맨 끝 하나뿐이고 정리대에서 빠졌어. 가지는 이 토큰들을 한 문장으로 묶었어.
+ 뒤 줄바꿈이 정리대에서 빠졌어. 가지의 바구니는 1 + 2일 때와 자리까지 같아.
1 뒤 줄바꿈이 칸막이가 됐어. 검사는 통과했지만 1과 + 2가 따로 선 두 문장이야.
1 뒤 줄바꿈은 칸막이가 되고 + 뒤 줄바꿈은 빠졌어. 가지의 바구니는 1 줄바꿈 + 2일 때와 같아.
줄바꿈은 맨 끝 하나뿐이고 정리대에서 빠졌어. 가지는 이 토큰들을 한 문장으로 묶었어.
한줄이
톡톡의 바구니
바구니에 들어가지 않은 빈칸
정리대
가지의 바구니
검사
검사 통과
main.tpz: types-ok (1 module)
main.tpz: resolve-ok (1 module)
topaz check main.tpzcb898749d76e51fdaf6c6636920ccdb4f415fc1cffd1e2497636b38cc24698070이 1 + 2 판은 제1화의 토큰 기록과 제5화의 검사 기록을 함께 보여 줘. 두 회차가 읽은 소스는 같은 여섯 바이트야.
원문 열기
+ 뒤 줄바꿈이 정리대에서 빠졌어. 가지의 바구니는 1 + 2일 때와 자리까지 같아.
한줄이
톡톡의 바구니
바구니에 들어가지 않은 빈칸
정리대
가지의 바구니
검사
검사 통과
main.tpz: types-ok (1 module)
main.tpz: resolve-ok (1 module)
topaz check main.tpztopaz compiler observe inputs/g4/main.tpz --out-dir out/g4 --compiler self --terminal typedfc7f432f888fb56e24acee32e9b2fce48ff3b2e819b3fcb181a723a99391e10c0같은 판으로 같은 절차를 두 번 따랐어. 두 번의 기록은 바이트까지 같았어.
이 입력에서 두 판(5.21.0, 5.21.1)의 검사 출력과 종료 코드, 대조한 토큰·나무·진단 파일이 바이트까지 같았어.
원문 열기
1 뒤 줄바꿈이 칸막이가 됐어. 검사는 통과했지만 1과 + 2가 따로 선 두 문장이야.
한줄이
톡톡의 바구니
바구니에 들어가지 않은 빈칸
정리대
가지의 바구니
검사
검사 통과
main.tpz: types-ok (1 module)
main.tpz: resolve-ok (1 module)
topaz check main.tpztopaz compiler observe inputs/g5/main.tpz --out-dir out/g5 --compiler self --terminal typed2df649c6846fb74122764a28a90e1c2e5efce91bdda7dbc15447038a6db4b3480같은 판으로 같은 절차를 두 번 따랐어. 두 번의 기록은 바이트까지 같았어.
이 입력에서 두 판(5.21.0, 5.21.1)의 검사 출력과 종료 코드, 대조한 토큰·나무·진단 파일이 바이트까지 같았어.
원문 열기
1 뒤 줄바꿈은 칸막이가 되고 + 뒤 줄바꿈은 빠졌어. 가지의 바구니는 1 줄바꿈 + 2일 때와 같아.
한줄이
톡톡의 바구니
정리대
가지의 바구니
검사
검사 통과
main.tpz: types-ok (1 module)
main.tpz: resolve-ok (1 module)
topaz check main.tpztopaz compiler observe inputs/g6/main.tpz --out-dir out/g6 --compiler self --terminal typed44d2c4ba874bfb725b76233edb4b4075dd16b6f7e47ba7425940c6ec3700623e0같은 판으로 같은 절차를 두 번 따랐어. 두 번의 기록은 바이트까지 같았어.
이 입력에서 두 판(5.21.0, 5.21.1)의 검사 출력과 종료 코드, 대조한 토큰·나무·진단 파일이 바이트까지 같았어.
원문 열기
세 틈을 모두 한 번씩 골라 보면 발견 카드가 열려.
빈칸은 토큰 사이를 갈라 놓고 톡톡의 바구니에는 들어가지 않았어. 빈칸을 빼면 12 한 토큰이 됐고, 빈칸만 두면 칸막이가 없어 검사가 멈췄어.
줄바꿈은 톡톡의 바구니에 조각으로 남았어. 이 두 실험판에서 1 뒤 줄바꿈은 칸막이가 됐고, + 뒤 줄바꿈과 맨 끝 줄바꿈은 빠졌어.
+ 뒤에서 줄을 바꾸자 가지는 1 + 2를 한 문장으로 묶었어. + 앞에서 줄을 바꾸자 이번에는 1이 한 문장, + 2가 또 한 문장이 됐어. 둘째 문장의 +는 2 하나에 붙어. 두 파일 모두 검사를 통과했어.
기록에서 더 찾아낸 짝
1 2와 1 줄바꿈 2
가지의 바구니가 달라.
1 + 2와 1 + 줄바꿈 2
가지의 바구니가 같아. 토큰의 차례와 종류, 시작과 끝 자리, 글자, 새로 만든 토큰인지까지 견줬어.
1 줄바꿈 + 2와 1 줄바꿈 + 줄바꿈 2
가지의 바구니가 같아. 토큰의 차례와 종류, 시작과 끝 자리, 글자, 새로 만든 토큰인지까지 견줬어.
바구니와 정리대, 칸막이, 리본은 이야기 속 그림이야. 토큰의 종류와 자리, 검사 결과는 기록에서 왔어.
진짜 속사정
톡톡의 바구니에는 글자를 읽으며 찾은 토큰이 담겨. 정리대는 그 토큰을 받아 가지에게 보낼 바구니를 꾸려. 가지는 그 바구니에서 토큰을 꺼내 문장으로 묶어.
토파즈 기록은 첫 바구니의 토큰 흐름을 raw, 정리된 흐름을 layout이라고 불러.
가지의 바구니에서 1과 2 사이에 선 칸막이를 봤지? 기록에서 이 토큰의 이름은 separator야. 여기서는 두 문장의 경계를 알려 줘서 문장 구분자라고 불러.
| 종류 | separator |
|---|---|
| 자리 | 두 번째 바이트 하나, 기록의 범위 [1,2) |
| 첫 줄바꿈 자리에 만든 문장 경계 | true |
마지막 칸이 true인 건 정리대가 첫 줄바꿈 자리에 문장 경계를 표시하면서 이 칸막이가 생겼다는 뜻이야.
공개 소스에는 lex가 찾은 토큰을 바로 다음 줄에서 normalize에 건네는 코드가 있어. 제6화 지도 뒷면 5행에서 불러오던 바로 그 normalize야.
261 let raw = lex(request.source)262 let layout = normalize(raw.tokens, request.source)컴파일러가 프로그램을 읽어 들이는 다른 파일에서는 세 줄이 이어져. lex가 찾은 토큰을 normalize가 정리하고, parse는 정리된 토큰을 받아.
91 let raw = lex(source)92 let layout = normalize(raw.tokens, source)93 let parsed = parse(layout.tokens, source, maxAstNodes, maxAstDepth, languageMode)?아니. 빈칸은 토큰 사이를 갈라. 12는 한 토큰인데 1 2는 두 토큰이야.
다만 빈칸은 문장을 나누지는 못해. 1 2에서 가지는 문장 구분자를 기다리다 expected a statement separator라는 쪽지를 남기고 멈췄어.
아니. 오늘 기록에서 + 뒤의 줄바꿈과 맨 끝의 줄바꿈은 빠졌어.
계산이 줄바꿈에서 끊기면 +나 -의 위치를 보세요. 새 문장 앞에서는 값 하나에 적용하는 단항 연산자로 해석될 수 있습니다.
그래서 1 줄바꿈 + 2는 검사를 통과하지만 1과 + 2가 따로 선 두 문장이야. 계산을 다음 줄로 이으려면 +를 앞줄 끝에 남겨.
빈칸과 줄바꿈을 다루는 규칙은 언어마다 달라. 오늘 본 것은 토파즈 5.21.1의 규칙과 기록이야.
토파즈 5.21.1 기록과 공개 소스
더 들여다보기
토파즈의 토큰 기록은 두 흐름을 한 파일에 함께 적어. 줄마다 stream 칸에 raw나 layout이 있어.
| stream | kind | span | spelling | synthetic |
|---|---|---|---|---|
raw | integer | [0,1) | 1 | false |
raw | newline | [1,2) | 줄바꿈 | false |
raw | integer | [2,3) | 2 | false |
raw | newline | [3,4) | 줄바꿈 | false |
raw | eof | [4,4) | 빈 글자 | false |
layout | integer | [0,1) | 1 | false |
layout | separator | [1,2) | 줄바꿈 | true |
layout | integer | [2,3) | 2 | false |
layout | eof | [4,4) | 빈 글자 | false |
streamkindspanspelling\n으로 보이고, 파일 끝은 빈 글자야synthetictrue. 직접 쓴 ;를 가리키는 구분자는 false야이 사이트가 맞대 본 것 1 + 줄바꿈 2의 layout 흐름은 1 + 2의 layout 흐름과 토큰의 차례, 종류, 시작과 끝, spelling, synthetic이 모두 같았어. 1 줄바꿈 + 2와 1 줄바꿈 + 줄바꿈 2의 layout 흐름도 서로 같았어.
이 사이트가 맞대 본 것 span에는 입력마다 다른 sourceId도 적혀 있어서 그 칸은 견주지 않았어.
normalize 소스 따라 읽기토파즈 공개 소스
숫자 사이의 빈칸을 만나면 글자를 읽는 위치가 한 칸 앞으로 가. 줄바꿈을 만나면 그 자리를 가리키는 newline 토큰이 생겨. 정리 단계에서는 기억해 둔 줄바꿈 자리에 separator를 넣는 코드도 볼 수 있어.
줄 번호는 파일의 줄 번호 그대로야.
610 if isAsciiSpace(value) {611 index += 1612 continue613 }614 if value == 10 {615 let lo = if index > 0 && byteAt(bytes, index - 1) == 13 { index - 1 } else { index }616 tokens.push(token("newline", lo, index + 1))617 index += 1618 continue619 }isAsciiSpace에 걸리는 빈칸은 index += 1로 한 칸 건너뛰고 토큰을 남기지 않아. 줄바꿈 글자(10)를 만나면 "newline" 토큰을 남겨.
304 if !isLeading(current.kind) && !currentPipeContinues &&305 !isCloser(current.kind) && current.kind != "eof" {306 let mut output = state.output307 output.push(token(308 "separator",309 pending.lo,310 pending.hi,311 true,312 ))다음 토큰이 앞에서 이어 주는 기호도, 닫는 괄호도, 파일 끝도 아니고 파이프로 이어지는 자리도 아니면, 기억해 둔 줄바꿈 자리(pending.lo, pending.hi)에 "separator"를 넣어. 마지막 true가 기록의 synthetic 칸이 돼.
245function observeNewline(246 state: LayoutState,247 current: PreviewToken,248 frameDepth: int,249) -> LayoutState {250 let pipeContinues = previousIs(state.previous, "pipe") &&251 patternContinuesAt(state.pattern, frameDepth)252 let frame = topFrame(state.frames)253 if frame.mode == LayoutMode.Separator && frame.hasItem &&254 lastTokenKind(state.output) != "separator" && !previousIsTrailing(state.previous) &&255 !pipeContinues {256 match state.pending {257 case Some(_) => ()258 case None => return LayoutState {259 ...state,260 pending: Some(current),261 }262 }263 }264 state265}observeNewline은 조건이 맞을 때만 줄바꿈을 pending으로 기억해. 바로 앞 토큰이 +처럼 뒤로 이어 주는 기호면(previousIsTrailing) 기억하지 않아. 이미 기억해 둔 줄바꿈이 있으면 그것을 그대로 둬.
=와 +로 끝난 줄에는 값이 더 필요하므로 합계 표현식이 다음 줄로 이어집니다.
*는 이어질까?49function lineContinuation(kind: string) -> Option<LineContinuation> {50 if kind == "gt" || kind == "gt-gt" || kind == "keyword/else" {51 Some(LineContinuation.Leading)52 } else if kind == "plus" || kind == "minus" || kind == "bang" ||53 kind == "tilde" || kind == "comma" || kind == "fat-arrow" ||54 kind == "left-paren" || kind == "left-bracket" {55 Some(LineContinuation.Trailing)56 } else if kind == "star-star" || kind == "star" || kind == "slash" ||57 kind == "percent" || kind == "dot-dot" || kind == "dot-dot-lt" ||58 kind == "lt" || kind == "le" || kind == "ge" || kind == "eq-eq" ||59 kind == "not-eq" || kind == "keyword/in" || kind == "and-and" ||60 kind == "or-or" || kind == "question-question" || kind == "pipe-gt" ||61 kind == "eq" || kind == "plus-eq" || kind == "minus-eq" ||62 kind == "star-eq" || kind == "slash-eq" || kind == "percent-eq" ||63 kind == "question-question-eq" || kind == "dot" ||64 kind == "question-dot" || kind == "keyword/by" {65 Some(LineContinuation.Both)66 } else {67 None68 }69}"plus"는 줄 끝에서 뒤로 이어 주는 목록(Trailing)에만 있어. 그래서 새 줄 맨 앞의 +는 계산을 잇지 못했어. "star"는 양쪽 모두 이어 주는 목록(Both)에 있어.
기록에 적힌 것 1 줄바꿈 * 2도 검사했어. 가지의 바구니에 문장 구분자가 없었어.
이 사이트가 맞대 본 것 이 기록은 소스에서 "star"가 양쪽 모두 이어 주는 목록에 있는 것과 맞아.
1 2의 쪽지는 어디서 나올까?443export function itemBoundary(parser: Parser) -> () {444 if at(parser, "separator") {445 skipSeparators(parser)446 } else if !at(parser, "right-brace") && !at(parser, "eof") {447 addDiagnostic(448 parser,449 "TPZ2001",450 "expected a statement separator",451 current(parser).lo,452 current(parser).hi,453 )454 synchronize(parser)455 }456 ()457}이 예제에서 첫 식 1을 읽은 뒤 2를 만난 자리에 쪽지를 남겼어. 쪽지의 글은 expected a statement separator야.
옮겨 온 소스는 같은 판의 라이선스와 고지를 따라. LICENSE 읽기, 새 창 NOTICE 읽기, 새 창
한 줄 정리
다음 이야기
가지이번엔 내 차례야. 기호 둘이 나오면 나무는 어떻게 생길까?
길의 둘째 정거장에서 가지가 조립대를 다시 펼쳤어.
2 + 3 * 4를 나무로 묶으면, 어느 식이 다른 식 안에 들어갈까?
다음에는 둘째 정거장으로 가 보자.