aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/044-specs/json_tree.py
diff options
context:
space:
mode:
Diffstat (limited to 'research/experiments/044-specs/json_tree.py')
-rw-r--r--research/experiments/044-specs/json_tree.py164
1 files changed, 164 insertions, 0 deletions
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]))