aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-13 07:40:30 +0100
committereunhhu <[email protected]>2026-10-13 07:40:30 +0100
commit010fa06adc3b05e3e164309ee34e200f5d858fa2 (patch)
tree4fafacc583806ae1426215c14757588a444ec761
parent8481b12fb794690cce0d9c54032ffba3de1fe31d (diff)
downloadpure-010fa06adc3b05e3e164309ee34e200f5d858fa2.tar.gz
pure-010fa06adc3b05e3e164309ee34e200f5d858fa2.zip
Research 140, 027's stage 2b measured: JSON read into a tree (aup's examples/json_tree.aup: a variant, lists in records, match, appends into a field's list, number(t)) and walked for a digest, against Python's reading: JSONTestSuite's 95 y_ taken alike, no n_ taken, 6 i_ taken; 300 random nested documents alike; in make check (module_specs, the suite and 100 random documents, three paths); 167 lines against the same design's 133 in Python (the bar missed: the type declarations), 40 times cJSON's time and 1.3 times plain Python's (556 ns a byte, 525 instructions); json_tree.py, json_tree_time.c (cJSON linked, digests compared first)
-rw-r--r--docs/design/027-computation-core.md10
-rw-r--r--docs/research/140-spec-suite.md46
-rw-r--r--research/experiments/044-specs/2026-10-13-stage2b.jsonl6
-rw-r--r--research/experiments/044-specs/json_tree.py164
-rw-r--r--research/experiments/044-specs/json_tree_time.c119
-rw-r--r--tests/check.py53
6 files changed, 397 insertions, 1 deletions
diff --git a/docs/design/027-computation-core.md b/docs/design/027-computation-core.md
index cd8dd44..7b851f6 100644
--- a/docs/design/027-computation-core.md
+++ b/docs/design/027-computation-core.md
@@ -137,7 +137,15 @@
- 시간은 C의 116배, 파이썬의 1.2배다.
- 쓰다가 드러난 뜻의 틈 하나를 메웠다. `&&`가 양쪽을 다 계산해서 `i < n && d[i]`가 실패했다. 이제 단락 평가이고, 오른쪽이
실패할 수 없을 때는 지금처럼 AND 하나를 낸다. 그래서 TPC-C를 포함한 기존 모듈 35개가 바이트까지 같다.
-- **2b:** 중첩(레코드 안의 레코드와 리스트), 갈래와 `match`, 값으로서의 리스트, 메시지 필드의 아무 값. JSON을 트리로 읽고 묻는다.
+- **2b(만듦):** 레코드 안의 레코드와 리스트, 갈래와 `match`, 값으로서의 리스트, 수 읽기. 결과는 140의 2b절이다.
+ - **꼴.** 레코드와 갈래는 aup가 경로 이름의 열로 펼쳐, 런타임에는 레코드 값이 없다. 리스트만 런타임의 묶음 값이다.
+ - **칸.** 칸에는 이벤트가 만든 리스트의 번호(i64)를 담는다. 리스트의 내부, 묶음 실행기, JIT는 그대로다. JIT의 덧붙이기 빠른 길만
+ 리스트 필드를 비킨다.
+ - **값의 뜻.** 나눠 가진 리스트는 바뀌기 전에 복사되므로 값의 뜻이 지켜진다. 읽기는 레지스터에 묶어 기존의 걷기, 접기, 자리
+ 읽기를 그대로 쓴다.
+ - **JSON 트리.** JSONTestSuite에서 틀린 것이 없고, 무작위 문서 300개의 다이제스트가 파이썬과 같다.
+ - **막대.** 줄 수는 못 미쳤다(167 대 133: 타입 선언). 시간은 C(cJSON)의 40배이고, 순수 파이썬보다도 1.3배 느리다.
+ - **남은 것.** 메시지 필드의 아무 값(JSON 경계에서 바로 트리로)과 저장되는 리스트의 리스트는 3·4단계로 남긴다.
- **2c:** 재귀(부르기와 돌아가기, 함수마다의 검증), 글자로 읽기.
- 레코드의 필드와 리스트의 원소가 아무 값을 가진다.
diff --git a/docs/research/140-spec-suite.md b/docs/research/140-spec-suite.md
index 6231c33..67d1cec 100644
--- a/docs/research/140-spec-suite.md
+++ b/docs/research/140-spec-suite.md
@@ -245,6 +245,52 @@
- 남은 차이는 리스트를 값으로 짓지 못해 생기는 것(메시지 스케줄의 미리 채움), 낱말 만들기, 선언이다.
- 시간은 바이트당 1,437 ns(C의 159배)다. 블록당 명령이 16,476개로 줄었다.
+## 027의 2b: JSON을 트리로 (2026-10-13)
+
+2b에서 더한 것:
+- **aup.**
+ - 레코드 안의 레코드(경로 이름의 열로 펼침), 갈래(`variant`), `match`
+ - 리스트를 담은 필드(`items: list of Json`)와 그 덧붙이기(`x.items.append`)
+ - 키 없는 리스트의 `xs[i]` 항목, `number(t)`
+- **Pure.**
+ - 레코드 필드의 타입 LIST: 칸에는 이벤트가 만든 리스트의 번호를 담는다.
+ - 명령 넷(LIST_VALUE, LIST_BIND, APPEND_IN, NUM_PARSE)과 레지스터 플래그, 경로 이름의 레코드 필드
+- 나눠 가진 리스트는 바뀌기 전에 복사된다. 그래서 다른 값은 바뀌지 않는다(단위 시험이 복사, 나눔 뒤의 덧붙이기, 묶인 레지스터를
+ 본다). ASan·UBSan 빌드의 단위 시험도 지났다. UBSan이 레코드 종류 표의 빈자리를 잡아 고쳤다.
+
+**JSON 트리.** aup의 `examples/json_tree.aup`는 JSON 텍스트를 `Json` 갈래의 트리로 읽는다.
+- 열린 배열과 객체는 스택(`list of Open`)에 두고, 값은 그 맨 위 항목의 리스트 필드에 덧붙인다. 닫을 때는 그 리스트를 값으로
+ 부모에게 넘긴다(나눠 가짐, 복사 없음). 문자열의 이스케이프(서로게이트 쌍 포함)를 풀고, 수는 `number(t)`로 읽는다.
+- 그다음 트리를 너비 우선으로 돌아 다이제스트를 낸다: 종류별 수, 수의 합, 문자열과 키의 바이트.
+
+**맞음.** 파이썬의 `json.loads`(같은 키가 둘인 객체를 지키려고 쌍 목록으로)를 같은 순서로 돈 다이제스트와 견준다.
+- JSONTestSuite에서 틀린 것이 없다. `y_` 95개를 모두 받아 다이제스트가 같고(수의 합은 비트까지), `n_`은 하나도 받지 않는다.
+- `i_`는 6개를 받는다: 아주 큰 정수, 언더플로, 500겹. 범위를 넘는 지수(유한하지 않은 수)와 짝 없는 서로게이트(UTF-8이 아닌 text)는
+ 실패로 거절한다.
+- 무작위 중첩 문서 300개(10만 바이트: 이스케이프, 이모지, 제어 문자, 깊이 5)가 모두 같다.
+- 세 실행 길이 같다. Pure의 `make check`(`module_specs`)에 JSONTestSuite와 무작위 문서 100개로 들어갔다.
+
+**막대.**
+
+| 막대 | 잰 것 | |
+|---|---|---|
+| 공식 케이스 | `y_` 모두 받음, `n_` 모두 거절 | 지남 |
+| 줄 수 ≤ 파이썬 참조 | aup 167줄(닫는 괄호만인 줄 20) 대 같은 설계의 파이썬 133줄 | **못 미침** |
+| 시간 | 바이트당 556 ns. C(cJSON, 같은 다이제스트) 14 ns(40배), 같은 설계의 순수 파이썬 428 ns(Pure가 1.3배 느림), 파이썬의 C `json` 모듈 61 ns | 기록 |
+
+**줄을 길게 한 것.**
+- 타입 선언이다: `Json` 갈래 8줄, `Member`와 `Open` 10줄, 다이제스트의 상태 11줄. 파이썬은 동적 타입이라 쓰지 않는다.
+- `match`의 갈래 블록 괄호도 있다.
+- 리스트를 다루는 줄 자체는 짧아졌다(닫을 때 한 줄, 덧붙이기 한 줄).
+
+**시간이 가는 곳.**
+- 바이트당 Pure 명령이 525개다. 바이트를 하나 읽을 때마다 지역 칸 읽기, 자리 읽기, fn의 인자 꺼내기, 비교가 이어진다.
+- 검사기(339개)보다 많은 것은 문자열을 바이트로 모으고 값을 리스트에 놓는 일이다.
+- 바이트와 텍스트의 일이 실행기에서 파이썬보다 느리다는 것이 2a(1.2배)에 이어 다시 드러났다. 핵이 선 뒤 빠르게 할 과녁의 첫째다.
+
+원본: `research/experiments/044-specs/json_tree.py`(파이썬 참조), `json_tree_time.c`(cJSON과 견줌, 다이제스트가 같아야 잰다),
+`2026-10-13-stage2b.jsonl`.
+
## 착수 기준 (다음 일)
- **바꿀 결정.** 계산의 핵을 설계한다(027). 위 표의 여덟 후보 가운데 무엇을 어떤 모양으로 받을지 정한다.
diff --git a/research/experiments/044-specs/2026-10-13-stage2b.jsonl b/research/experiments/044-specs/2026-10-13-stage2b.jsonl
new file mode 100644
index 0000000..27de90b
--- /dev/null
+++ b/research/experiments/044-specs/2026-10-13-stage2b.jsonl
@@ -0,0 +1,6 @@
+{"size":103781,"jit":"0","batch":"0","c_ns_per_byte":14.717,"pure_ns_per_byte":554.145,"ratio":37.7,"instructions":54505171,"instructions_per_byte":525.2}
+{"size":103781,"jit":"0","batch":"","c_ns_per_byte":13.756,"pure_ns_per_byte":555.296,"ratio":40.4,"instructions":54505171,"instructions_per_byte":525.2}
+{"size":103781,"jit":"all","batch":"","c_ns_per_byte":14.223,"pure_ns_per_byte":555.444,"ratio":39.1,"instructions":54505171,"instructions_per_byte":525.2}
+{"size":103781,"jit":"","batch":"","c_ns_per_byte":13.899,"pure_ns_per_byte":555.628,"ratio":40.0,"instructions":54505171,"instructions_per_byte":525.2}
+{"size":103781,"python_ns_per_byte":426.5,"python_json_module_ns_per_byte":62.3}
+{"json_tree_aup_lines": 167, "aup_brace_lines": 20, "python_lines": 133, "jsontestsuite": {"y_taken": 95, "n_taken": 0, "i_taken": 6}, "random_documents": 300, "wrong": 0}
diff --git a/research/experiments/044-specs/json_tree.py b/research/experiments/044-specs/json_tree.py
new file mode 100644
index 0000000..8807cdc
--- /dev/null
+++ b/research/experiments/044-specs/json_tree.py
@@ -0,0 +1,164 @@
+# A JSON text read into a tree and walked breadth first for a digest: the design of aup's examples/json_tree.aup in
+# plain Python (the reference 140 counts its lines against; the tree is Python's own values, a dict kept as pairs).
+def space(b):
+ return b in (0x20, 0x09, 0x0a, 0x0d)
+
+
+def digit(b):
+ return 0x30 <= b <= 0x39
+
+
+def hexdigit(b):
+ return digit(b) or 0x61 <= b <= 0x66 or 0x41 <= b <= 0x46
+
+
+class Pairs(list):
+ pass
+
+
+def read(d):
+ try:
+ d.decode('utf-8')
+ except UnicodeDecodeError:
+ return None
+ n, i, ok, mode, root, open_, v = len(d), 0, True, 0, [], [], []
+ while ok:
+ while i < n and space(d[i]):
+ i += 1
+ if i == n:
+ ok = mode == 1 and not open_
+ break
+ b = d[i]
+ if mode == 1:
+ if not open_:
+ ok = False
+ elif b == 0x2c:
+ mode = 2 if open_[-1]['object'] else 0
+ elif b == 0x5d and not open_[-1]['object']:
+ v.append(open_[-1]['items'])
+ elif b == 0x7d and open_[-1]['object']:
+ v.append(Pairs(open_[-1]['members']))
+ else:
+ ok = False
+ if v:
+ open_.pop()
+ i += 1
+ elif mode == 3 and b == 0x7d or mode == 4 and b == 0x5d:
+ v.append(Pairs(open_[-1]['members']) if b == 0x7d else open_[-1]['items'])
+ open_.pop()
+ i += 1
+ elif mode in (2, 3):
+ ok = b == 0x22
+ mode = 5
+ elif b in (0x5b, 0x7b):
+ open_.append({'items': [], 'members': [], 'name': '', 'object': b == 0x7b})
+ mode = 4 if b == 0x5b else 3
+ i += 1
+ elif b == 0x22 or mode == 5:
+ i += 1
+ s = bytearray()
+ while ok and i < n and d[i] != 0x22:
+ start = i
+ while i < n and d[i] != 0x22 and d[i] != 0x5c and d[i] >= 0x20:
+ i += 1
+ s += d[start:i]
+ if i < n and d[i] < 0x20:
+ ok = False
+ elif i + 1 < n and d[i] == 0x5c:
+ e = d[i + 1]
+ if e == 0x75:
+ ok = i + 5 < n and all(hexdigit(x) for x in d[i + 2:i + 6])
+ u = int(d[i + 2:i + 6], 16) if ok else 0
+ i += 6
+ if 0xd800 <= u < 0xdc00 and i + 5 < n and d[i] == 0x5c and d[i + 1] == 0x75 and all(hexdigit(x) for x in d[i + 2:i + 6]):
+ low = int(d[i + 2:i + 6], 16)
+ if 0xdc00 <= low < 0xe000:
+ u = 0x10000 + ((u - 0xd800) << 10 | low - 0xdc00)
+ i += 6
+ s += chr(u).encode('utf-8', 'surrogatepass')
+ else:
+ ok = e in b'"\\/bfnrt'
+ s += {0x62: b'\b', 0x66: b'\f', 0x6e: b'\n', 0x72: b'\r', 0x74: b'\t'}.get(e, bytes([e]))
+ i += 2
+ elif i < n and d[i] == 0x5c:
+ ok = False
+ ok = ok and i < n
+ i += 1
+ try:
+ t = s.decode('utf-8')
+ except UnicodeDecodeError:
+ return None
+ if mode == 5:
+ open_[-1]['name'] = t
+ while i < n and space(d[i]):
+ i += 1
+ ok = ok and i < n and d[i] == 0x3a
+ i += 1
+ mode = 0
+ else:
+ v.append(t)
+ elif b == 0x2d or digit(b):
+ start = i
+ if b == 0x2d:
+ i += 1
+ while i < n and (digit(d[i]) or d[i] in b'.eE+-'):
+ i += 1
+ text = d[start:i].decode()
+ import re
+ if not re.fullmatch(r'-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?', text) or float(text) in (float('inf'), -float('inf')):
+ return None
+ v.append(float(text))
+ else:
+ word = b'true' if b == 0x74 else b'false' if b == 0x66 else b'null'
+ ok = d[i:i + len(word)] == word
+ i += len(word)
+ if ok:
+ v.append({b'true': True, b'false': False, b'null': None}[word])
+ if ok and v:
+ if not open_:
+ ok = not root
+ root.append(v[-1])
+ elif open_[-1]['object']:
+ open_[-1]['members'].append((open_[-1]['name'], v[-1]))
+ else:
+ open_[-1]['items'].append(v[-1])
+ v.pop()
+ mode = 1
+ return root if ok else None
+
+
+def digest(tree):
+ d = dict(nulls=0, trues=0, falses=0, numbers=0, strings=0, arrays=0, objects=0, sum=0.0, text_bytes=0, key_bytes=0)
+ q, at = [tree], 0
+ while at < len(q):
+ x = q[at]
+ if x is None: d['nulls'] += 1
+ elif x is True: d['trues'] += 1
+ elif x is False: d['falses'] += 1
+ elif isinstance(x, float): d['numbers'] += 1; d['sum'] += x
+ elif isinstance(x, str): d['strings'] += 1; d['text_bytes'] += len(x.encode('utf-8'))
+ elif isinstance(x, Pairs):
+ d['objects'] += 1
+ for k, y in x: d['key_bytes'] += len(k.encode('utf-8')); q.append(y)
+ else: d['arrays'] += 1; q.extend(x)
+ at += 1
+ return d
+
+
+if __name__ == '__main__':
+ import json, os, sys, time
+ suite = sys.argv[1]
+ for name in sorted(os.listdir(suite)):
+ tree = read(open(os.path.join(suite, name), 'rb').read())
+ assert not name.startswith('y_') or tree is not None, name
+ assert not name.startswith('n_') or tree is None, name
+ big = b'[' + b','.join(b'{"key%d": [1.5e3, true, null, "text \\u00e9 %d"]}' % (k, k) for k in range(2000)) + b']'
+ runs, loads = [], []
+ for _ in range(7):
+ start = time.perf_counter()
+ digest(read(big)[0])
+ runs.append((time.perf_counter() - start) * 1e9 / len(big))
+ start = time.perf_counter()
+ digest(json.loads(big, object_pairs_hook=Pairs))
+ loads.append((time.perf_counter() - start) * 1e9 / len(big))
+ print('{"size":%d,"python_ns_per_byte":%.1f,"python_json_module_ns_per_byte":%.1f}' % (len(big), sorted(runs)[3], sorted(loads)[3]))
diff --git a/research/experiments/044-specs/json_tree_time.c b/research/experiments/044-specs/json_tree_time.c
new file mode 100644
index 0000000..1cac21a
--- /dev/null
+++ b/research/experiments/044-specs/json_tree_time.c
@@ -0,0 +1,119 @@
+/* Research 140, design 027 stage 2b: aup's examples/json_tree.aup (run by
+ * Pure: a JSON text read into a tree, then a digest of the tree) against C:
+ * cJSON (the system's libcjson.so.1, its few declarations written here)
+ * reading the same document into its tree, and the same digest. Both digests
+ * must be equal; then each is timed (the median of seven runs of rounds).
+ *
+ * cc -O2 -std=c17 -I../../../core json_tree_time.c ../../../build/libpure.a -l:libcjson.so.1 -lm -pthread -o json_tree_time
+ * ./json_tree_time json_tree.pmod ROUNDS
+ *
+ * The document is json_time.c's: 2,000 objects of a key and an array of a
+ * number, true, null and a string with an escape. One JSON line. */
+#define _POSIX_C_SOURCE 200809L
+#include "module.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+typedef struct cJSON {
+ struct cJSON *next, *prev, *child;
+ int type;
+ char *valuestring;
+ int valueint;
+ double valuedouble;
+ char *string;
+} cJSON;
+cJSON *cJSON_Parse(const char *value);
+void cJSON_Delete(cJSON *item);
+enum { FALSE_ = 1, TRUE_ = 2, NULL_ = 4, NUMBER = 8, STRING = 16, ARRAY = 32, OBJECT = 64 };
+
+/* nulls, trues, falses, numbers, strings, arrays, objects, sum, text bytes, key bytes: breadth first, as json_tree.aup. */
+typedef struct { double count[7], sum, text_bytes, key_bytes; } Digest;
+static Digest digest(const char *text, cJSON **queue) {
+ Digest d = {{0}, 0, 0, 0};
+ cJSON *root = cJSON_Parse(text);
+ size_t at = 0, end = 0;
+ queue[end++] = root;
+ while (at < end) {
+ cJSON *x = queue[at++];
+ switch (x->type & 0xFF) {
+ case NULL_: d.count[0]++; break;
+ case TRUE_: d.count[1]++; break;
+ case FALSE_: d.count[2]++; break;
+ case NUMBER: d.count[3]++, d.sum += x->valuedouble; break;
+ case STRING: d.count[4]++, d.text_bytes += (double)strlen(x->valuestring); break;
+ case ARRAY: d.count[5]++; for (cJSON *y = x->child; y; y = y->next) queue[end++] = y; break;
+ default: d.count[6]++; for (cJSON *y = x->child; y; y = y->next) d.key_bytes += (double)strlen(y->string), queue[end++] = y;
+ }
+ }
+ cJSON_Delete(root);
+ return d;
+}
+
+static double now_ns(void) {
+ struct timespec t;
+ clock_gettime(CLOCK_MONOTONIC, &t);
+ return (double)t.tv_sec * 1e9 + (double)t.tv_nsec;
+}
+
+static int by_value(const void *a, const void *b) {
+ double x = *(const double *)a, y = *(const double *)b;
+ return (x > y) - (x < y);
+}
+
+int main(int argc, char **argv) {
+ if (argc != 3) { fprintf(stderr, "usage: %s MODULE ROUNDS\n", argv[0]); return 2; }
+ int rounds = atoi(argv[2]);
+ static char doc[1 << 18];
+ static cJSON *queue[1 << 16];
+ size_t n = 0;
+ doc[n++] = '[';
+ for (int k = 0; k < 2000; ++k)
+ n += (size_t)sprintf(doc + n, "%s{\"key%d\": [1.5e3, true, null, \"text \\u00e9 %d\"]}", k ? "," : "", k, k);
+ doc[n++] = ']';
+ doc[n] = 0;
+ PureModule module;
+ char error[160];
+ if (pure_module_read(argv[1], &module, error, sizeof(error)) != PURE_OK) { fprintf(stderr, "%s\n", error); return 1; }
+ PureApp app;
+ if (pure_app_start(&app, &module, (PureData){0}) != PURE_OK) return 1;
+ int check = pure_module_input(&module, "check");
+ if (pure_app_message(&app, (uint32_t)check, NULL, -1, (PureSpan){(const unsigned char *)doc, n}) != PURE_OK) {
+ fprintf(stderr, "event failed\n");
+ return 1;
+ }
+ uint64_t work = app.work;
+ Digest c = digest(doc, queue);
+ /* The states after valid: nulls, trues, falses, numbers, strings, arrays, objects, sum, text_bytes, key_bytes. */
+ const char *names[] = {"nulls", "trues", "falses", "numbers", "strings", "arrays", "objects", "sum", "text_bytes", "key_bytes"};
+ double want[] = {c.count[0], c.count[1], c.count[2], c.count[3], c.count[4], c.count[5], c.count[6], c.sum, c.text_bytes, c.key_bytes};
+ for (unsigned k = 0; k < 10; ++k) {
+ int state = -1;
+ for (uint32_t s = 0; s < module.states; ++s) if (!strcmp(module.state_names[s], names[k])) state = (int)s;
+ if (state < 0 || app.values[state].num != want[k]) {
+ fprintf(stderr, "%s: %g against %g\n", names[k], state < 0 ? -1 : app.values[state].num, want[k]);
+ return 1;
+ }
+ }
+ double c_runs[7], pure_runs[7];
+ volatile double sink = 0;
+ for (int r = 0; r < 7; ++r) {
+ double start = now_ns();
+ for (int i = 0; i < rounds; ++i) sink += digest(doc, queue).sum;
+ c_runs[r] = (now_ns() - start) / rounds;
+ start = now_ns();
+ for (int i = 0; i < rounds; ++i)
+ if (pure_app_message(&app, (uint32_t)check, NULL, -1, (PureSpan){(const unsigned char *)doc, n}) != PURE_OK) return 1;
+ pure_runs[r] = (now_ns() - start) / rounds;
+ }
+ qsort(c_runs, 7, sizeof(double), by_value);
+ qsort(pure_runs, 7, sizeof(double), by_value);
+ const char *jit = getenv("PURE_JIT"), *batch = getenv("PURE_BATCH");
+ printf("{\"size\":%zu,\"jit\":\"%s\",\"batch\":\"%s\",\"c_ns_per_byte\":%.3f,\"pure_ns_per_byte\":%.3f,\"ratio\":%.1f,"
+ "\"instructions\":%llu,\"instructions_per_byte\":%.1f}\n",
+ n, jit ? jit : "", batch ? batch : "", c_runs[3] / (double)n, pure_runs[3] / (double)n, pure_runs[3] / c_runs[3],
+ (unsigned long long)work, (double)work / (double)n);
+ pure_app_stop(&app);
+ return 0;
+}
diff --git a/tests/check.py b/tests/check.py
index bf77246..9386ced 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -1738,6 +1738,59 @@ def module_specs(aup, pure, work):
assert accepted == (name[0] == 'y') or name[0] == 'i', (name, accepted)
taken[name[0]] += accepted
runs['json'] = {'cases': len(cases), 'taken': taken}
+ # A JSON text read into a tree of values (its examples/json_tree.aup, 027's 2b: a variant, lists in records, match),
+ # walked breadth first for a digest, against Python's reading walked so: JSONTestSuite's cases and random documents.
+ class Pairs(list):
+ pass
+
+ def digest(data):
+ doc = json.loads(data.decode('utf-8'), object_pairs_hook=Pairs)
+ d = dict(nulls=0, trues=0, falses=0, numbers=0, strings=0, arrays=0, objects=0, sum=0.0, text_bytes=0, key_bytes=0)
+ q, at = [doc], 0
+ while at < len(q):
+ x = q[at]
+ if x is None: d['nulls'] += 1
+ elif x is True: d['trues'] += 1
+ elif x is False: d['falses'] += 1
+ elif isinstance(x, (int, float)): d['numbers'] += 1; d['sum'] += float(x)
+ elif isinstance(x, str): d['strings'] += 1; d['text_bytes'] += len(x.encode('utf-8'))
+ elif isinstance(x, Pairs):
+ d['objects'] += 1
+ for k, v in x: d['key_bytes'] += len(k.encode('utf-8')); q.append(v)
+ else: d['arrays'] += 1; q.extend(x)
+ at += 1
+ return d
+
+ def value(depth):
+ pick = chooser.random()
+ if depth > 4 or pick < 0.35:
+ return chooser.choice([None, True, False, chooser.randrange(-10 ** 6, 10 ** 6), chooser.uniform(-1e3, 1e3),
+ chooser.choice(['', 'a', 'é ü', 'line\nbreak', 'quote"back\\', '\U0001f600', '\u0001'])])
+ if pick < 0.7:
+ return [value(depth + 1) for _ in range(chooser.randrange(0, 6))]
+ return {f'k{chooser.randrange(50)}' if chooser.random() < 0.8 else '': value(depth + 1) for _ in range(chooser.randrange(0, 6))}
+
+ for k in range(100):
+ cases[f'r_{k}.json'] = json.dumps(value(0), ensure_ascii=chooser.random() < 0.5, indent=chooser.choice([None, 1, 2])).encode()
+ (work / f'r_{k}.json').write_bytes(cases[f'r_{k}.json'])
+ program = work / 'json_tree.pmod'
+ compile_module(aup, EDITOR.parent / 'json_tree.aup', program)
+ given = ''.join(f'message check @{work / name}\n' for name in cases)
+ outputs = {name: subprocess.run([str(pure / 'build/pure-run'), str(program)], input=given, capture_output=True, text=True,
+ timeout=600, env=dict(os.environ, **env)).stdout
+ for name, env in (('batches', {'PURE_JIT': '0'}), ('rounds', {'PURE_JIT': '0', 'PURE_BATCH': '0'}), ('all', {'PURE_JIT': 'all'}))}
+ for name in ('rounds', 'all'):
+ assert outputs[name] == outputs['batches'], ('json_tree', name)
+ keys = ('nulls', 'trues', 'falses', 'numbers', 'strings', 'arrays', 'objects', 'sum', 'text_bytes', 'key_bytes')
+ trees = {'y': 0, 'n': 0, 'i': 0, 'r': 0}
+ for name, line in zip(cases, outputs['batches'].splitlines()):
+ state = json.loads(line)
+ accepted = state['fault'] == 'none' and state['states']['valid'] is True
+ assert accepted == (name[0] in 'yr') or name[0] == 'i', (name, state['fault'])
+ if accepted:
+ assert {k: state['states'][k] for k in keys} == digest(cases[name]), name
+ trees[name[0]] += accepted
+ runs['json_tree'] = {'cases': len(cases), 'taken': trees}
return runs