aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/research/140-spec-suite.md29
-rw-r--r--research/experiments/044-specs/2026-10-19-dns-after.jsonl4
-rw-r--r--research/experiments/044-specs/2026-10-19-http-after.jsonl4
-rw-r--r--research/experiments/044-specs/2026-10-19-inflate-after.jsonl4
-rw-r--r--research/experiments/044-specs/http_check.py2
-rw-r--r--research/experiments/044-specs/http_time.c2
-rw-r--r--research/experiments/044-specs/inflate_check.py3
-rw-r--r--research/experiments/044-specs/inflate_time.c18
-rw-r--r--tests/check.py6
9 files changed, 59 insertions, 13 deletions
diff --git a/docs/research/140-spec-suite.md b/docs/research/140-spec-suite.md
index 187e0cf..1963c70 100644
--- a/docs/research/140-spec-suite.md
+++ b/docs/research/140-spec-suite.md
@@ -47,15 +47,15 @@
| # | 명세 | 분야 | 처음 필요한 것 | Pure 안의 검증 | 10-13 | 027 뒤(10-19) |
|---|---|---|---|---|---|---|
-| 1 | DNS 메시지(RFC 1035) | 와이어 프로토콜 | 바이트 읽고 쓰기, 빅엔디언, 비트 필드, 이름 압축(바이트 안의 참조) | 알려진 구현이 만든 질의·응답 바이트와 대조 | 못 쓴다(바이트, 비트) | 썼다: dnspython의 메시지 400개를 읽고 다시 써 같다. 우회 둘, 줄 수 못 미침, C의 67배(아래) |
+| 1 | DNS 메시지(RFC 1035) | 와이어 프로토콜 | 바이트 읽고 쓰기, 빅엔디언, 비트 필드, 이름 압축(바이트 안의 참조) | 알려진 구현이 만든 질의·응답 바이트와 대조 | 못 쓴다(바이트, 비트) | 썼다: dnspython의 메시지 400개를 읽고 다시 써 같다. 우회 하나(보기의 블록 fn), 줄 1.5배, C의 61배(아래) |
| 2 | SHA-256(FIPS 180-4) | 해시 | 32비트 낱말, 회전, xor | NIST CAVP의 SHA 벡터 | 우회로만 될 것으로 보인다(써 보지 않음) | 썼다: NIST 벡터를 세 실행 길에서 지난다(1단계) |
-| 3 | inflate(RFC 1951) | 압축 | 비트 읽기, 허프만 표, 뒤 참조 | zlib로 압축한 말뭉치를 풀어 원본과 비교 | 못 쓴다(바이트, 비트) | 썼다: zlib 스트림 640개, 손상 240개가 파이썬 참조와 같다. 우회 셋, 줄 수 못 미침, C의 108배(아래) |
+| 3 | inflate(RFC 1951) | 압축 | 비트 읽기, 허프만 표, 뒤 참조 | zlib로 압축한 말뭉치를 풀어 원본과 비교 | 못 쓴다(바이트, 비트) | 썼다: zlib 스트림 640개, 손상 240개가 파이썬 참조와 같다. 우회 없음, 줄 1.5배, C의 102배(아래) |
| 4 | JSON(RFC 8259) → 트리와 질의 | 깊이를 모르는 데이터 | 글자 읽기, 중첩, 재귀, 갈래 | JSONTestSuite | 못 쓴다(글자, 중첩, 재귀, 갈래) | 썼다: JSONTestSuite와 무작위 문서가 파이썬과 같다(2a–2c) |
| 5 | UTF-8과 줄 나누기(UAX #14) | 텍스트 | 바이트, 코드 포인트, 속성 표 | Unicode의 `LineBreakTest.txt` | 못 쓴다(글자) | 글자 읽기가 컴파일된다(2c). 쓰지 않음 |
| 6 | 관계 질의, 그래프 탐색 | 데이터 모델 | 리스트, 키, 접기, 이벤트의 리스트 | TPC-C, 은행 질의, 파이썬 모델 | 된다(기준선: [092](092-tpcc.md), 040의 은행 질의, aup `routes.aup`) | 된다. TPC-C가 SQLite와 오차 없이 같다(5단계) |
| 7 | 텍스트 편집 | UI 개념 | text, 커서 | 편집기의 참조 모델 대조(aup `make check`) | 된다 | 된다 |
| 8 | 상자 배치(flex 같은) | UI 개념 | 중첩, 재귀 | 기존 배치 구현(Yoga, Taffy)의 결과와 대조 | 못 쓴다(중첩, 재귀) | 중첩과 재귀는 핸들러에서 된다. 보기에서 재귀 fn을 부르지 못한다(탐침). 쓰지 않음 |
-| 9 | HTTP/1.1 요청을 패킷마다(RFC 9112) | 스트림의 상태 기계 | 바이트, 이벤트를 넘는 상태 | 경계를 아무 데서나 자른 요청 모음 | 못 쓴다(바이트). 지금은 `serve/`의 C다 | 썼다: 스트림 200개를 어디서 잘라도 h11과 같다. 우회 셋, 줄 수 못 미침, C의 85배(아래) |
+| 9 | HTTP/1.1 요청을 패킷마다(RFC 9112) | 스트림의 상태 기계 | 바이트, 이벤트를 넘는 상태 | 경계를 아무 데서나 자른 요청 모음 | 못 쓴다(바이트). 지금은 `serve/`의 C다 | 썼다: 스트림 200개를 어디서 잘라도 h11과 같다. 우회 하나(리스트 값의 대입), 줄 1.9배, C의 83배(아래) |
| 10 | 재시도, 물러서기, 시간 제한 | 시간 | 시각, 한 번의 타이머 | 시각을 준 시뮬레이션 | 우회(주기 틱 `every`와 `now`) | 다시 보지 않음 |
| 11 | X25519, ChaCha20-Poly1305 | 상수 시간 암호 | 데이터에 따라 달라지지 않는 시간 | RFC 7748, RFC 8439의 벡터 | aup 밖(설계상, 아래) | 그대로(설계상 aup 밖) |
@@ -695,6 +695,29 @@
- 시간은 명령당 3.8–6.0 ns가 든다. 바이트 일을 C에 가깝게 하려면 명령의 수(바이트 찾기 같은 일을 명령 하나로 할지)와 명령당
비용(기계어 계층이 맡지 못하는 명령들) 둘 다를 봐야 한다. 이것도 따로 정한다.
+## 우회를 걷어 내고 다시 잼 (2026-10-19)
+
+결정점 뒤에 만든 넷(바이트의 `==`, 바이트 하나인 상태, fn의 맨 리스트, 블록 안의 let 리스트)으로 세 명세의 우회를 걷어 냈다. 앱은
+파이썬 참조의 꼴에 더 가까워졌다. inflate의 블록마다의 표는 핸들러 안에서 짓고, HTTP의 남은 바이트는 바이트 상태다. 세 명세의 검사가
+세 실행 길에서 그대로 지난다(zlib 스트림 640개와 손상 240개, dnspython의 메시지 400개와 손상 300개, h11의 스트림 200개를 모든
+자름으로, 손상 200개).
+
+| 명세 | 줄(닫는 괄호) 전 → 뒤 | 파이썬 | 바이트당 ns 전 → 뒤 | 같은 알고리즘의 C 대비 | 바이트당 명령 | 남은 우회 |
+|---|---|---|---|---|---|---|
+| inflate | 132(18) → 121(15) | 81 | 1,152 → 1,139 | 108배 → 102배 | 191.2 → 191.2 | 없음 |
+| DNS | 115(21) → 106(18) | 72 | 244 → 221 | 67배 → 61배 | 63.6 → 49.9 | 보기가 블록 fn을 부르지 못한다(압축을 보기 안에 풀어 쓴다) |
+| HTTP/1.1 | 159(24) → 148(21) | 79 | 278 → 270 | 85배 → 83배 | 61.4 → 61.5 | let의 리스트에 리스트 값을 대입하지 못해 트레일러를 하나씩 붙인다(2줄) |
+
+- **줄.** 파이썬의 1.6–2.0배에서 1.5–1.9배로 줄었다. 남은 차이의 대부분은 닫는 괄호의 줄과 레코드의 선언, 그리고 바이트마다 도는
+ 고리(파이썬의 `find`, `strip`, `partition`이 C로 도는 곳)다.
+- **시간.** DNS의 압축이 바이트를 한 바이트씩 견주던 것을 `==` 한 번으로 하니 명령이 21% 줄었다. inflate와 HTTP는 명령 수가 거의
+ 같다: 우회의 값은 줄에 있었지 시간에 있지 않았다.
+- **빈 잇기.** HTTP의 남은 바이트를 바이트 상태로 두자, 비어 있을 때도 `pending + packet.data`가 패킷을 통째로 복사해 같은 명령 수에
+ 5.6% 느려졌다(273 → 288 ns). Pure의 CONCAT이 한쪽이 비면 다른 쪽을 복사 없이 그대로 두게 했다(뜻은 같다: 값은 바뀌지 않는다).
+ 그 뒤 옛 앱과 새 앱이 같은 시간이다(270.0 대 270.6 ns, 같은 자리에서 번갈아). 041의 다섯 일은 실행기만의 knapsack −0.42%, join
+ −0.16%, 묶음 BFS −0.12%, 나머지 ±0.03% 안이다(b48 대 b47).
+- 원본: `2026-10-19-inflate-after.jsonl`, `-dns-after.jsonl`, `-http-after.jsonl`(검사 한 줄과 세 길의 시간).
+
## 착수 기준 (다음 일)
- **바꿀 결정.** 계산의 핵을 설계한다(027). 위 표의 여덟 후보 가운데 무엇을 어떤 모양으로 받을지 정한다.
diff --git a/research/experiments/044-specs/2026-10-19-dns-after.jsonl b/research/experiments/044-specs/2026-10-19-dns-after.jsonl
new file mode 100644
index 0000000..529ebbc
--- /dev/null
+++ b/research/experiments/044-specs/2026-10-19-dns-after.jsonl
@@ -0,0 +1,4 @@
+{"messages": 400, "bytes": 148582, "records": 3466, "right": {"batches": 400, "rounds": 400, "all": 400}, "paths_same": true, "damaged": 300, "damaged_refused": 185, "damaged_agreed": 300, "aup_lines": 106, "aup_brace_lines": 18, "python_lines": 72}
+{"messages":400,"bytes":148582,"jit":"0","batch":"","c_ns_per_byte":3.657,"resolver_read_ns_per_byte":4.406,"pure_ns_per_byte":221.336,"to_c":60.5,"to_resolver_read":50.2,"instructions":7409761,"instructions_per_byte":49.9}
+{"messages":400,"bytes":148582,"jit":"0","batch":"0","c_ns_per_byte":3.656,"resolver_read_ns_per_byte":4.402,"pure_ns_per_byte":221.159,"to_c":60.5,"to_resolver_read":50.2,"instructions":7409761,"instructions_per_byte":49.9}
+{"messages":400,"bytes":148582,"jit":"all","batch":"","c_ns_per_byte":3.658,"resolver_read_ns_per_byte":4.401,"pure_ns_per_byte":203.141,"to_c":55.5,"to_resolver_read":46.2,"instructions":7409761,"instructions_per_byte":49.9}
diff --git a/research/experiments/044-specs/2026-10-19-http-after.jsonl b/research/experiments/044-specs/2026-10-19-http-after.jsonl
new file mode 100644
index 0000000..d3c57ef
--- /dev/null
+++ b/research/experiments/044-specs/2026-10-19-http-after.jsonl
@@ -0,0 +1,4 @@
+{"streams": 200, "bytes": 132623, "requests": 578, "right": {"whole": 200, "random": 200, "bytes": 50}, "paths": {"batches": 200, "rounds": 200, "all": 200}, "paths_same": true, "damaged": 200, "damaged_refused": 35, "damaged_agreed": 200, "aup_lines": 148, "aup_brace_lines": 21, "python_lines": 79}
+{"streams":151,"bytes":98838,"requests":444,"jit":"0","batch":"","c_ns_per_byte":3.249,"pure_ns_per_byte":270.433,"to_c":83.2,"instructions":6074414,"instructions_per_byte":61.5}
+{"streams":151,"bytes":98838,"requests":444,"jit":"0","batch":"0","c_ns_per_byte":3.250,"pure_ns_per_byte":270.680,"to_c":83.3,"instructions":6074414,"instructions_per_byte":61.5}
+{"streams":151,"bytes":98838,"requests":444,"jit":"all","batch":"","c_ns_per_byte":3.245,"pure_ns_per_byte":270.454,"to_c":83.3,"instructions":6074414,"instructions_per_byte":61.5}
diff --git a/research/experiments/044-specs/2026-10-19-inflate-after.jsonl b/research/experiments/044-specs/2026-10-19-inflate-after.jsonl
new file mode 100644
index 0000000..179d2de
--- /dev/null
+++ b/research/experiments/044-specs/2026-10-19-inflate-after.jsonl
@@ -0,0 +1,4 @@
+{"streams": 640, "bytes": 6488480, "right": {"batches": 640, "rounds": 640, "all": 640}, "paths_same": true, "damaged": 240, "damaged_refused": 165, "damaged_agreed": 240, "aup_lines": 121, "aup_brace_lines": 15, "python_lines": 81}
+{"size":65536,"stream":22991,"jit":"0","batch":"","c_ns_per_byte":11.140,"zlib_ns_per_byte":3.499,"pure_ns_per_byte":1138.637,"to_c":102.2,"to_zlib":325.4,"instructions":12528479,"instructions_per_byte":191.2}
+{"size":65536,"stream":22991,"jit":"0","batch":"0","c_ns_per_byte":11.121,"zlib_ns_per_byte":3.505,"pure_ns_per_byte":1139.351,"to_c":102.5,"to_zlib":325.1,"instructions":12528479,"instructions_per_byte":191.2}
+{"size":65536,"stream":22991,"jit":"all","batch":"","c_ns_per_byte":10.649,"zlib_ns_per_byte":3.499,"pure_ns_per_byte":1136.972,"to_c":106.8,"to_zlib":324.9,"instructions":12528479,"instructions_per_byte":191.2}
diff --git a/research/experiments/044-specs/http_check.py b/research/experiments/044-specs/http_check.py
index e3982c3..305f783 100644
--- a/research/experiments/044-specs/http_check.py
+++ b/research/experiments/044-specs/http_check.py
@@ -137,7 +137,7 @@ def run(module, packets, env, work):
pairs = lambda fs: [(bytes.fromhex(f['name']), bytes.fromhex(f['value'])) for f in fs]
requests = [(bytes.fromhex(r['method']), bytes.fromhex(r['target']), r['minor'], pairs(r['fields']), bytes.fromhex(r['body']),
pairs(r['trailers'])) for r in s['requests']]
- left = bytes.fromhex(s['pending'][0]['data']) if s['pending'] else b''
+ left = bytes.fromhex(s['pending']) # a bytes state (028)
return out, (requests, left), any(json.loads(line)['fault'] != 'none' for line in out)
diff --git a/research/experiments/044-specs/http_time.c b/research/experiments/044-specs/http_time.c
index 51bd595..dc85fc1 100644
--- a/research/experiments/044-specs/http_time.c
+++ b/research/experiments/044-specs/http_time.c
@@ -217,7 +217,7 @@ int main(int argc, char **argv) {
}
work += app.work;
}
- if ((long)pure_list_count(app.lists[requests]) != c_requests || pure_list_count(app.lists[left])) {
+ if ((long)pure_list_count(app.lists[requests]) != c_requests || pure_draft_size(app.texts[left])) {
fprintf(stderr, "Pure read %zu requests, C %ld\n", pure_list_count(app.lists[requests]), c_requests);
return 1;
}
diff --git a/research/experiments/044-specs/inflate_check.py b/research/experiments/044-specs/inflate_check.py
index c3f37b4..d71e59e 100644
--- a/research/experiments/044-specs/inflate_check.py
+++ b/research/experiments/044-specs/inflate_check.py
@@ -72,8 +72,7 @@ def run(module, packed_streams, env, work):
got = []
for line in out:
state = json.loads(line)
- items = state['states']['inflated']
- got.append(bytes.fromhex(items[-1]['data']) if state['fault'] == 'none' and items else None)
+ got.append(bytes.fromhex(state['states']['inflated']) if state['fault'] == 'none' else None) # a bytes state (028)
return out, got
diff --git a/research/experiments/044-specs/inflate_time.c b/research/experiments/044-specs/inflate_time.c
index 34cc2d5..8463a22 100644
--- a/research/experiments/044-specs/inflate_time.c
+++ b/research/experiments/044-specs/inflate_time.c
@@ -159,6 +159,15 @@ static int by_value(const void *a, const void *b) {
return (x > y) - (x < y);
}
+/* Each span of a draft read in order against the original's bytes from where the last ended. */
+static size_t compared;
+static bool compare_original(void *context, PureSpan span) {
+ const unsigned char *original = context;
+ if (memcmp(original + compared, span.data, span.size)) return false;
+ compared += span.size;
+ return true;
+}
+
static unsigned char *whole(const char *path, size_t *size) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
@@ -199,9 +208,12 @@ int main(int argc, char **argv) {
return 1;
}
uint64_t work = app.work - before;
- const PureList *list = app.lists[state];
- PureCell cell = pure_list_count(list) == 1 ? pure_list_get(list, 0, 0) : (PureCell){0};
- if (cell.size != original_size || memcmp(cell.text, original, original_size)) { fprintf(stderr, "Pure's bytes differ\n"); return 1; }
+ /* The bytes state (028): its draft's bytes. */
+ if (pure_draft_size(app.texts[state]) != original_size ||
+ pure_draft_read(app.texts[state], 0, original_size, compare_original, original) != PURE_OK) {
+ fprintf(stderr, "Pure's bytes differ\n");
+ return 1;
+ }
double c_runs[7], zlib_runs[7], pure_runs[7];
volatile unsigned char sink = 0;
diff --git a/tests/check.py b/tests/check.py
index b3b4750..046e61a 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -1864,9 +1864,9 @@ def module_specs(aup, pure, work):
shown = [json.loads(line) for line in outputs['batches'].splitlines()]
assert len(shown) == len(streams) + len(cut), ('inflate', len(shown))
for (data, _), state in zip(streams, shown):
- assert state['fault'] == 'none' and state['states']['inflated'][-1]['data'] == data.hex(), ('inflate', len(data))
+ assert state['fault'] == 'none' and state['states']['inflated'] == data.hex(), ('inflate', len(data))
for state in shown[len(streams):]:
- assert state['fault'] != 'none' and state['states']['inflated'][-1]['data'] == streams[-1][0].hex(), ('inflate cut', state['fault'])
+ assert state['fault'] != 'none' and state['states']['inflated'] == streams[-1][0].hex(), ('inflate cut', state['fault'])
runs['inflate'] = {'streams': len(streams), 'bytes': sum(len(d) for d, _ in streams), 'cut': len(cut)}
# DNS messages (RFC 1035, its examples/dns.aup): messages dnspython made (tests/specs/dns), each read without fault on
# the three paths, what its view wire writes back read to the same records and written again to the same bytes; a
@@ -1920,7 +1920,7 @@ def module_specs(aup, pure, work):
s = json.loads(out[-1])['states']
pairs = lambda fs: [[f['name'], f['value']] for f in fs]
return out[-1], {'requests': [[r['method'], r['target'], r['minor'], pairs(r['fields']), r['body'], pairs(r['trailers'])]
- for r in s['requests']], 'left': s['pending'][0]['data'] if s['pending'] else ''}
+ for r in s['requests']], 'left': s['pending']}
cuts = 0
for stream, want in zip(made, reading):