From 481642fbdc94db37ae42ded7244674054b6377c3 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Wed, 30 Sep 2026 16:38:06 +0100 Subject: Pin an i64's meaning on every path (015) server_meaning gives every value an i64 field, 13 values from 0 and -0 to both ends of the range and just inside them, in over JSON, rows and a query, over the link (where B takes id, a u32, as an i64) and HTTP, and out at all seven exits with every digit. 32 more refusals, each with its reason on JSON, a query and rows; an i64 view's text at both ends and an i64 argument that counts exactly and refuses the rest. --- docs/design/015-boundary-meaning.md | 32 +++++++-- tests/check.py | 129 ++++++++++++++++++++++++------------ 2 files changed, 112 insertions(+), 49 deletions(-) diff --git a/docs/design/015-boundary-meaning.md b/docs/design/015-boundary-meaning.md index 7789919..e11ddc4 100644 --- a/docs/design/015-boundary-meaning.md +++ b/docs/design/015-boundary-meaning.md @@ -7,7 +7,8 @@ 값의 뜻이 무엇이고, 무엇을 그대로 옮기고 무엇을 거절하는지를 정한다. 한 줄로는 이렇다. **받아들인 값은 어느 길로 나오든 같다. 수는 비트까지(0의 부호만 빼고), text는 바이트까지, bool은 -`true`나 `false`로. 받아들일 수 없는 값은 모든 들어오는 길이 거절하고, 거절한 사건은 아무것도 남기지 않는다.** +`true`나 `false`로, i64는 자릿수 하나까지. 받아들일 수 없는 값은 모든 들어오는 길이 거절하고, 거절한 사건은 아무것도 남기지 +않는다.** ## 수 @@ -45,6 +46,23 @@ - **모듈 교체와 다시 열기.** 수가 bool이 되면 0(−0도)과 1은 `false`와 `true`가 되고, 다른 수는 새 기본값이 된다. bool이 수가 되면 0과 1이다. 교체가 출력의 필드를 바꾸면 기다리던 보냄의 값도 같은 규칙으로 옮긴다. +## i64 + +[016](016-bool-and-i64.md)이 더한 정확한 64비트 정수다. 수가 아니다. + +- **들어올 때.** JSON과 쿼리의 값은 정수의 자릿수만이다(`-?(0|[1-9][0-9]*)`). 부호 `+`, 앞의 0, 점(`1.0`도), 지수(`1e3`도), + 문자열(`"1"`)은 받지 않고, −2^63부터 2^63−1까지다. `-0`은 0이다. JSON의 수이지만 i64가 아니면 `json_type`, JSON이 아니면 + (`+1`, `01`) `json`, 쿼리는 `query`로 거절한다. 메시지의 행도 같다. 페이로드에서는 접두부의 8바이트(부호 있는 + little-endian)다. +- **저장.** 상태는 `values` 문서의 종류 6과 8바이트 정수다. 목록은 종류 4(i64)의 열이다. 이미지는 8바이트, 로그는 zigzag + varint다. +- **보일 때.** 모든 10진 자릿수다. 보기의 글자, 표의 JSON(JSON의 수로), HTTP 전달의 JSON, `pure-run`, 화면이 같다. JSON을 + binary64로 읽는 쪽(자바스크립트의 `JSON.parse`)은 2^53 너머에서 값을 잃는다. 그런 쪽에 보일 i64는 앱이 글자로 보인다(보간). +- **보낼 때.** 링크는 i64를 i64인 필드로 옮기고, u32는 i64로 넓힌다(정확하다). i64와 수, bool 사이는 넓히지 않는다(받지 않고 + 기다린다). HTTP 전달의 JSON은 자릿수다. +- **모듈 교체와 다시 열기.** 수가 i64가 되면 정수이고 범위 안일 때 그 값(−0은 0), 아니면 새 기본값이다. i64가 수가 되면 + |i| ≤ 2^53일 때 그 값, 아니면 새 기본값이다. bool과 i64 사이는 0과 1만 옮긴다. 기다리던 보냄의 값도 같은 규칙으로 옮긴다. + ## text - **들어올 때.** JSON 문자열은 이스케이프를 풀고 서로게이트 짝을 합친다. 날 본문은 바이트 그대로다. 결과는 UTF-8이어야 @@ -74,12 +92,16 @@ [`tests/check.py`](../../tests/check.py)의 `server_meaning`이 한 번에 본다. - 값: 수 22개(0과 −0, 2^53 부근, 가장 큰 수, 정규와 비정규의 경계, 반쯤에서의 언더플로, 1e21 부근), u32 넷, text 12개(한글, - 4바이트 UTF-8, 이스케이프, NUL, U+2028, BOM, 비문자, 4 KiB), 그리고 모든 값에 `true`나 `false`인 bool 필드 하나. -- 들어오는 길: JSON(ASCII로 이스케이프한 것과 UTF-8), 행, 쿼리와 날 본문, 링크(다른 순서, 넓힌 필드), HTTP 전달. + 4바이트 UTF-8, 이스케이프, NUL, U+2028, BOM, 비문자, 4 KiB), 그리고 모든 값에 `true`나 `false`인 bool 필드 하나와 i64 필드 + 하나(i64 13개: 0과 −0, ±1, 2^53과 2^53+1, −(2^53+1), 2^63−1과 −2^63과 그 옆, 2^32, 19자리). +- 들어오는 길: JSON(ASCII로 이스케이프한 것과 UTF-8), 행, 쿼리와 날 본문, 링크(다른 순서, 넓힌 필드: u32를 수로, u32를 + i64로), HTTP 전달. - 나오는 길: 보기, 질의, 답, 재시작, 체크포인트 뒤의 재시작, 모듈 교체, 두 번 중계한 노드. -- 거절할 값 52가지(수, u32, text, bool, 행)가 모든 들어오는 길에서 거절되고, 어디에도 남지 않는다. bool은 JSON, 쿼리, 행에서 - 거절의 종류까지 본다. +- 거절할 값 84가지(수, u32, text, bool, i64, 행)가 모든 들어오는 길에서 거절되고, 어디에도 남지 않는다. bool과 i64는 JSON, + 쿼리, 행에서 거절의 종류까지 본다. - bool 보기의 글자(`true`)와 bool 인자(`?want=true`, `?want=false`, 그 밖은 400 `invalid`). +- i64 보기의 글자(`max`는 2^63−1, `min`은 −2^63)와 i64 인자(끝값마다 센 수가 맞고, 소수·지수·범위 밖·`+1`·`01`·빈 값은 400 + `invalid`). 전달이 한 번씩, 순서대로 적용되는 것은 링크의 검사들(`server_links_udp_lossy`, `server_links_moved`, `server_upgrade`)과 [012](012-link-protocol.md)이 지킨다. 필드를 이름으로 받는 규칙은 `server_fields`가 지킨다. diff --git a/tests/check.py b/tests/check.py index 0c16802..b982256 100644 --- a/tests/check.py +++ b/tests/check.py @@ -3306,6 +3306,7 @@ MEANING_KEPT = """record Kept { x = 0 label: text ok = false + big: i64 } state kept: list of Kept by id """ @@ -3317,6 +3318,7 @@ MEANING_ROWS = """view rows = each k in kept { x = k.x label = k.label ok = k.ok + big = k.big } """ # A: what comes in (a JSON body, its rows, or a query and a plain body) is kept, and sent on; a put is answered @@ -3327,6 +3329,7 @@ MEANING_ORIGIN = MEANING_KEPT + """record Part { u = 0 t: text ok = false + big: i64 } input put = message { id: u32 @@ -3334,6 +3337,7 @@ input put = message { u: u32 t: text ok: bool + big: i64 at: { x: number label: text @@ -3345,6 +3349,7 @@ input put_plain = message { u: u32 t: text ok: bool + big: i64 } input put_rows = message { parts: list of Part @@ -3355,28 +3360,35 @@ input touch = message { } output passed = message like put on put { - kept.add(Kept { id: put.id, n: put.n, u: put.u, t: put.t, x: put.at.x, label: put.at.label, ok: put.ok }) + kept.add(Kept { id: put.id, n: put.n, u: put.u, t: put.t, x: put.at.x, label: put.at.label, ok: put.ok, big: put.big }) send passed { ..put } reply put.t } on put_rows { for p in put_rows.parts { - kept.add(Kept { id: p.id, n: p.n, u: p.u, t: p.t, x: 0, label: "rows", ok: p.ok }) - send passed { id: p.id, n: p.n, u: p.u, t: p.t, ok: p.ok, at: { x: 0, label: "rows" } } + kept.add(Kept { id: p.id, n: p.n, u: p.u, t: p.t, x: 0, label: "rows", ok: p.ok, big: p.big }) + send passed { id: p.id, n: p.n, u: p.u, t: p.t, ok: p.ok, big: p.big, at: { x: 0, label: "rows" } } } } on put_plain { - kept.add(Kept { id: put_plain.id, n: put_plain.n, u: put_plain.u, t: put_plain.t, x: 0, label: "", ok: put_plain.ok }) - send passed { id: put_plain.id, n: put_plain.n, u: put_plain.u, t: put_plain.t, ok: put_plain.ok, at: { x: 0, label: "" } } + kept.add(Kept { id: put_plain.id, n: put_plain.n, u: put_plain.u, t: put_plain.t, x: 0, label: "", ok: put_plain.ok, + big: put_plain.big }) + send passed { id: put_plain.id, n: put_plain.n, u: put_plain.u, t: put_plain.t, ok: put_plain.ok, big: put_plain.big, + at: { x: 0, label: "" } } } on touch { kept[touch.id].t = touch.t } view any_ok = count(k in kept where k.ok) > 0 view with_ok(want: bool) = count(k in kept where k.ok == want) +view biggest = max(k.big for k in kept) +view smallest = min(k.big for k in kept) +view with_big(want: i64) = count(k in kept where k.big == want) """ + MEANING_ROWS -# B: A's sends over the link, taken by name (fields in another order, u a number), kept and sent on over HTTP. +# B: A's sends over the link, taken by name (fields in another order, u a number, id an i64), kept and sent on over +# HTTP. MEANING_MIRROR = MEANING_KEPT + """input passed = message { + big: i64 ok: bool at: { label: text @@ -3385,12 +3397,12 @@ MEANING_MIRROR = MEANING_KEPT + """input passed = message { t: text u: number n: number - id: u32 + id: i64 } output onward = message like passed on passed { - kept.add(Kept { id: passed.id, n: passed.n, u: passed.u, t: passed.t, x: passed.at.x, label: passed.at.label, - ok: passed.ok }) + kept.add(Kept { id: number(passed.id), n: passed.n, u: passed.u, t: passed.t, x: passed.at.x, label: passed.at.label, + ok: passed.ok, big: passed.big }) send onward { ..passed } } """ + MEANING_ROWS @@ -3405,10 +3417,11 @@ MEANING_END = MEANING_KEPT + """input onward = message { label: text } ok: bool + big: i64 } on onward { kept.add(Kept { id: onward.id, n: onward.n, u: onward.u, t: onward.t, x: onward.at.x, label: onward.at.label, - ok: onward.ok }) + ok: onward.ok, big: onward.big }) } """ + MEANING_ROWS # The value set: every number a JSON reader must round correctly, u32s to its ends, texts JSON escapes and UTF-8. @@ -3417,6 +3430,8 @@ MEANING_NUMBERS = ['0', '-0', '1', '-1', '0.1', '0.3333333333333333', '900719925 '1.7976931348623157e308', '1.7976931348623158e308', '2.2250738585072014e-308', '2.2250738585072011e-308', '5e-324', '2.4703282292062328e-324', '1E+2', '-2.5e-10'] MEANING_U32 = [0, 1, 2147483648, 4294967295] +MEANING_I64 = ['0', '-0', '1', '-1', '9007199254740992', '9007199254740993', '-9007199254740993', '9223372036854775807', + '-9223372036854775808', '9223372036854775806', '-9223372036854775807', '4294967296', '1234567890123456789'] MEANING_TEXTS = ['', 'a', '한글 텍스트', 'emoji 🎉 and 𝄞', 'quote " backslash \\ slash /', 'line\nfeed\ttab\rreturn', 'control \x01\x1f\x7f', 'nul \x00 inside', 'separator \u2028 and \u2029', '\ufeffbom', '\uffff not a character', 'x' * 4096] @@ -3426,13 +3441,13 @@ def server_meaning(aup, pure, work): """One value, every path (docs/research/108): values at the edges of what JSON, a query and a plain body can say come in to A; A keeps them and sends them over a link to B, which takes them by name (fields in another order, - u widened to a number) and sends them over HTTP to C. Each value must come - out the same at every exit: A's view, a query on A, B's and C's views, A - after a restart and after a checkpoint, B after its module was replaced. - A number keeps its bits (zero has no sign: -0 shows as 0 and counts as 0), - a text its bytes, a bool is true or false (never 1 or 0). What must be - refused is refused on every way in, and leaves nothing behind (no row, no - send).""" + u widened to a number, id to an i64) and sends them over HTTP to C. Each + value must come out the same at every exit: A's view, a query on A, B's + and C's views, A after a restart and after a checkpoint, B after its + module was replaced. A number keeps its bits (zero has no sign: -0 shows + as 0 and counts as 0), a text its bytes, a bool is true or false (never 1 + or 0), an i64 every digit (past 2^53, to its ends). What must be refused + is refused on every way in, and leaves nothing behind (no row, no send).""" import socket import struct import time @@ -3440,8 +3455,8 @@ def server_meaning(aup, pure, work): from urllib.parse import quote modules = {} for name, text in (('origin', MEANING_ORIGIN), ('mirror', MEANING_MIRROR), ('end', MEANING_END), - ('mirror-2', MEANING_MIRROR.replace(' id = 0\n n = 0\n u = 0\n t: text\n x = 0\n label: text\n ok = false\n', - ' ok = false\n label: text\n x = 0\n t: text\n u = 0\n n = 0\n id = 0\n'))): + ('mirror-2', MEANING_MIRROR.replace(' id = 0\n n = 0\n u = 0\n t: text\n x = 0\n label: text\n ok = false\n big: i64\n', + ' big: i64\n ok = false\n label: text\n x = 0\n t: text\n u = 0\n n = 0\n id = 0\n'))): source = work / f'meaning-{name}.aup' source.write_text(text) compile_module(aup, source, source.with_suffix('.pmod')) @@ -3498,10 +3513,11 @@ def server_meaning(aup, pure, work): return float(shown) == want == 0 or struct.pack('