aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research/108-one-value-every-path.md
blob: 7a241fe94fafe188f9f6b252286482fcc5fa6708 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# 108 — 경계에서 값이 그대로인가: 한 값, 모든 길

2026-09-30. [외부 리뷰 004](../outside-feedback/feedback-004.md)의 맺음말에서 시작한다. 리뷰는 "앱 코드와 저장·전달·화면
사이의 중복 상태와 변환을 실제로 줄인 것"을 가장 중요한 성과로 꼽고, "그 경계에서 의미가 유지되는지부터 고정하는 일"을 다음
연구의 기반으로 권했다.

변환은 줄었지만 없어지지는 않았다.

- 들어올 때: HTTP의 JSON 본문과 쿼리가 메시지의 필드가 된다(JSON 문자열의 이스케이프를 풀고, 수의 글자를 읽는다).
- 저장할 때: 상태가 저장 형식이 된다(위치는 varint, 2^53 미만의 정수는 zigzag varint, 그 밖의 수는 표시와 8바이트).
  체크포인트는 그것을 다시 이미지로 쓴다.
- 보일 때: 스냅숏의 상태가 보기의 JSON이 된다(수는 가장 짧은 글자로, text는 JSON 문자열로).
- 보낼 때: 보냄이 outbox의 기록이 되고, 링크의 기록이 되고, 받는 쪽 입력의 배치로 옮겨진다(모양이 다르면 이름으로,
  [014](../design/014-fields-by-name.md)).

변환마다 단위 검사는 있다. 그러나 한 값이 한 길로 들어가 다른 모든 길로 나올 때 같은지를 보는 검사는 없다.

착수 기준(검사를 짜기 전에 정해 커밋했다):

- **바꿀 결정.**
  - 어떤 길이 값을 바꾸면(수의 비트, text의 바이트, 필드가 있고 없음) 다른 일보다 먼저 고친다.
  - 형식이 싣지 못해 바꿀 수 없는 것은 계약으로 적는다.
  - 경계마다의 계약을 설계 문서로 두고, 이 검사를 `make check`에 넣는다.
- **길.**
  - 들어가는 길: HTTP의 JSON 본문, HTTP의 쿼리와 날 본문(text), 링크 전달, HTTP 전달.
  - 나오는 길: 보기(연결 스레드가 스냅숏에서), 등록한 질의, 재시작(저장소의 로그에서), 체크포인트 뒤의 재시작(이미지에서),
    모듈 교체(필드 순서를 바꾸고 u32를 number로 넓힌 모듈), 중계(A에서 B로 링크, B에서 C로 HTTP).
- **값.**
  - 수: 0, −0, 2^53 부근, 가장 큰 수, 가장 작은 정규 수와 비정규 수, 1/3, 글자가 지수로 바뀌는 1e21 부근.
  - u32: 0, 2^31, 2^32 − 1.
  - text: 빈 것, 한글, 4바이트 UTF-8, 따옴표·역슬래시·제어 문자, NUL, U+2028, 4 KiB.
  - 그룹 안의 필드.
- **통과 조건.**
  - 받아들인 값은 모든 나오는 길에서 같아야 한다. 수는 비트까지(보기의 글자를 다시 읽은 binary64), text는 바이트까지.
  - 거절할 값(넘치는 수, 음수·소수·2^32 이상의 u32, UTF-8이 아닌 text, 짝 없는 서로게이트)은 모든 들어가는 길이 거절해야
    한다. 거절한 사건은 아무것도 남기지 않는다(보기에도, 보냄에도).
- **범위.** Pure의 경계다. 화면(Vega가 보기를 그리는 것)은 다음 단계다.

## 쉬운 말로

- **어긋난 곳은 하나였고, 고쳤다.** 입력 필드의 쿼리 값은 퍼센트 인코딩을 풀지 않았다. 같은 pure-serve가 보기의 인자는
  풀었다. 그래서 파이썬의 `urlencode`나 브라우저처럼 지수의 `+`를 `%2B`로 보내는 클라이언트는 `?n=1e%2B21`이 거절됐다. 이제
  입력 필드도 보기의 인자처럼 푼다.
- **0에는 부호가 없다는 것을 계약으로 적었다.** `-0`은 받아들이지만 보기에는 `0`으로 나온다. 글자로 쓰는 함수가 그렇게
  정해져 있고, `distinct`도 둘을 한 번 센다. aup에는 둘을 가르는 연산이 없으니 −0은 aup의 별개 값이 아니다.
- **aup 문서의 예가 컴파일되지 않았다.** 문서는 `message { title: text, points: number }`처럼 선언의 필드를 쉼표로 적었는데,
  컴파일러는 줄바꿈이나 세미콜론만 받았다. 값(`Kept { id: 1 }`)은 쉼표로 나누므로, 이제 선언도 쉼표를 받는다(aup `be1122a`).
- **나머지는 모든 길에서 같았다.**
  - 수는 어려운 경우까지 파이썬의 정확한 반올림과 비트가 같았다(2^53+1의 짝수 반올림, `2.2250738585072011e-308`, 비정규 수,
    반쯤에서의 언더플로).
  - text는 NUL, 제어 문자, U+2028, BOM, 4 KiB까지 바이트가 같았다.
  - 거절할 값은 JSON, 쿼리, 날 본문, 행이 모두 거절했다.
  - 재시작, 체크포인트, 모듈 교체, 두 번의 중계를 지나도 같았다.
- **검사는 `make check`에 들어갔다.** `server_meaning`이 1.3초에 값 67개를 나가는 길 7개에서 보고, 거절할 값 39가지를 본다.
  경계마다의 계약은 [015](../design/015-boundary-meaning.md)에 적었다.

## 한 것

1. **값을 하나씩 더듬었다.** 노드 하나에 경계값을 넣고 보기로 읽었다. 수 40가지(거절할 것 포함), text 25가지(서로게이트와
   UTF-8이 아닌 바이트 포함), 쿼리와 날 본문 47가지다. 수는 파이썬의 `float()`이 기대값이다.
2. **어긋난 곳을 찾아 고쳤다.** 쿼리 값의 해독이 보기와 입력에서 달랐다(pure-serve `serve.c`의 `resolve`).
3. **한 번에 보는 검사를 짰다** ([`tests/check.py`](../../tests/check.py)의 `server_meaning`).
   - A가 JSON(ASCII로 이스케이프한 것과 UTF-8), 행, 쿼리와 날 본문으로 받아 쥐고, 링크로 B에 보낸다.
   - B는 필드 순서를 바꾸고 u32를 number로 넓힌 입력으로 받아 HTTP로 C에 보낸다.
   - 나오는 길 일곱: A의 보기, A의 질의, B와 C의 보기, 재시작한 A, 체크포인트 뒤에 재시작한 A, 모듈을 바꾼 B. 그리고 `put`의
     답은 들어온 text의 바이트다.
   - A에서만 한 행을 4 KiB로 400번 고쳐, 목록의 이미지를 새로 쓰게 했다(체크포인트 1번).
4. **검사가 헛돌지 않는지 봤다.** 고치기 전의 pure-serve로 돌리면 쿼리 경로(`1E+2`를 `1E%2B2`로 보낸 것)에서 실패한다.
   B가 받은 수에 1 ulp를 곱하게 앱을 바꾸면 B에서 잡힌다.

## 판정

- 통과 조건을 채웠다. 받아들인 값은 나가는 길 일곱에서 수는 비트까지(0의 부호만 빼고), text는 바이트까지 같다. 거절할 값은
  모든 들어가는 길이 거절하고, 어디에도 남지 않는다.
- 착수 기준은 −0을 내다보지 못했다(수를 비트까지 비교한다고만 적었다). 0의 부호는 aup에서 뜻이 없으므로(가르는 연산이
  없고, 글자와 `distinct`가 이미 같게 본다) 바꾸지 않고 계약으로 적었다(015). 검사는 0끼리를 같게 본다.

## 화면

범위 밖으로 남겼던 화면(Vega가 보기를 그리는 경계)도 같은 방식으로 이어서 봤다. 무엇이든 받는 목록 하나에 위험한
text를 넣고 표면마다 출력을 떴다.

- **CLI와 TUI는 이미 막고 있었다.** ESC, C0, DEL, C1이 그림으로 나왔다. 그러나 두 가지가 그대로 나갔다.
  - 줄 구분자(U+2028)가 그대로 나갔다. CLI의 코드는 "값이 줄을 끊지 않게"라고 적었는데, 파이썬의 `splitlines()` 같은
    읽는 쪽에는 U+2028이 줄바꿈이다.
  - 방향 오버라이드(U+202E)가 그대로 나갔다. 양방향을 처리하는 터미널은 같은 줄의 나머지를 뒤집는다. 표의 옆 칸, 곧 다른
    값까지 뒤집힌다.
- **문장(NUI)은 0x20 미만만 공백으로 바꿨다.** C1 제어(일부 터미널에 U+009B는 CSI다), DEL, 줄 구분자, 방향 제어를
  그대로 냈다.
- **웹 페이지는 제어 문자를 그대로 보였고,** 레이블의 오버라이드가 같은 항목의 배지까지 뒤집을 수 있었다.
- 메신저처럼 다른 노드가 보낸 글을 보이는 앱이라면, 보낸 쪽이 받는 쪽의 화면을 이렇게 속일 수 있다.

고쳤다(Vega `611b54f`). 모든 표면이 같은 그림을 쓰고(`vega_visible`), 터미널의 칸은 방향 제어도 �로 보인다
(`vega_cell`). 문장은 공백으로 말하거나 말하지 않는다. 웹 페이지는 그림으로 보이고 값마다 격리한다. `/screen`의 JSON은
값을 그대로 싣는다. Vega의 `screen_meaning`이 CLI, TUI, 문장, JSON을 값 16개로 보고, `browser`가 페이지를 본다.

## 다음

- bytes 필드는 목록의 레코드에 담기지 않으므로 여기서 보지 않았다. 문서 모델의 검사(`server_parity`)가 본다.
- 넓은 글자와 ZWJ 이모지 묶음의 칸 너비는 근사다(Vega 001: grapheme 단위 자르기가 아직 없다). 뜻이 아니라 모양의 일이라
  여기서 다루지 않았다.