aboutsummaryrefslogtreecommitdiffstats
path: root/research
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-19 05:42:00 +0100
committereunhhu <[email protected]>2026-10-19 05:42:00 +0100
commitc545227f2461e92afc38a0e1317a288e8c653cc6 (patch)
treef236f78a7c0770a5ab07df525d7403077d6b56b5 /research
parentfe0e33b509b2e4a649647cccbecffa25c754c053 (diff)
downloadpure-c545227f2461e92afc38a0e1317a288e8c653cc6.tar.gz
pure-c545227f2461e92afc38a0e1317a288e8c653cc6.zip
140's 5, line breaking (UAX #14, Unicode 16.0), written in aup with no new opcode (aup's examples/linebreak.aup, the properties as data: 2,787 ranges from the UCD 16.0 Vega's text is frozen at, built by Vega's text/unicode.py, loaded once): every line of LineBreakTest.txt (16,672) and 2,000 random lines (their boundaries as linebreak.py, the Python reference, which passes the test whole, works them out) right on the three paths, the paths' outputs the same; module_specs in make check: the ranges and the lines kept in tests/specs/linebreak (derived from the UCD, the Unicode License v3's notice), run by pure-run --views; bars: written, no workaround (three frictions: hex literals always i64, number no let's name, a let's list one a line), 176 lines against Python's 99, 6,920 ns a code point against Vega's C 34.7 (199x), pure Python 2,523 (Pure 2.7x slower), 1,774 instructions a code point (setup 260, the ranges' binary search 360, decide about 1,150 a boundary); the decisions before 8 (box layout: Taffy can be fetched by cargo, a subset to choose first; as a handler) and 10 (a one-shot timer is a core primitive one spec needs: noted, not made); the suite table's row 5; CHECKPOINT's direction row
Diffstat (limited to 'research')
-rw-r--r--research/experiments/044-specs/2026-10-19-linebreak.jsonl6
-rw-r--r--research/experiments/044-specs/linebreak.py110
-rw-r--r--research/experiments/044-specs/linebreak_check.py147
-rw-r--r--research/experiments/044-specs/linebreak_time.c133
4 files changed, 396 insertions, 0 deletions
diff --git a/research/experiments/044-specs/2026-10-19-linebreak.jsonl b/research/experiments/044-specs/2026-10-19-linebreak.jsonl
new file mode 100644
index 0000000..763ee5b
--- /dev/null
+++ b/research/experiments/044-specs/2026-10-19-linebreak.jsonl
@@ -0,0 +1,6 @@
+{"ranges": 2787, "test_lines": 16672, "random_lines": 2000, "right": {"batches": 18672, "rounds": 18672, "all": 18672}, "paths_same": true, "aup_lines": 176, "aup_brace_lines": 19, "python_lines": 99}
+{"lines":18672,"code_points":61285,"right":18672,"jit":"0","batch":"","c_ns_per_point":34.7,"pure_ns_per_point":6942.5,"to_c":199.8,"instructions":108735033,"instructions_per_point":1774.3}
+{"lines":18672,"code_points":61285,"right":18672,"jit":"0","batch":"0","c_ns_per_point":34.5,"pure_ns_per_point":6921.6,"to_c":200.4,"instructions":108735033,"instructions_per_point":1774.3}
+{"lines":18672,"code_points":61285,"right":18672,"jit":"all","batch":"","c_ns_per_point":34.8,"pure_ns_per_point":6913.7,"to_c":198.5,"instructions":108735033,"instructions_per_point":1774.3}
+{"python_ns_per_point": 2523.1, "lines": 18672, "points": 61285}
+{"parts_instructions_per_point": {"setup": 260.3, "range_search": 360.0, "decide": 1154.0}}
diff --git a/research/experiments/044-specs/linebreak.py b/research/experiments/044-specs/linebreak.py
new file mode 100644
index 0000000..3d08958
--- /dev/null
+++ b/research/experiments/044-specs/linebreak.py
@@ -0,0 +1,110 @@
+# Line breaking (UAX #14, Unicode 16.0) in plain Python: the reference 140 counts aup's lines against, the rules in the
+# standard's order as Vega's text/linebreak.c has them. A character's properties come from a table of ranges (the
+# check builds it from the UCD): its class after LB1, whether it is an initial or a final quotation mark (Pi, Pf), East
+# Asian (width F, W or H), and pictographic and unassigned (Extended_Pictographic and Cn).
+import bisect
+
+(BK, CR, LF, NL, SP, ZW, ZWJ, CM, WJ, GL, BA, BB, B2, HY, CB, CL, CP, EX, IN, NS, OP, QU, IS, NU, PO, PR, SY, AK, AL, AP,
+ AS, EB, EM, H2, H3, HL, ID, JL, JV, JT, RI, VF, VI) = range(43)
+NONE, ALLOWED, MANDATORY = 0, 1, 2
+ENDS = (BK, CR, LF, NL)
+
+
+def breaks(codes, starts, rows):
+ """Each boundary of codes, the ends included: NONE, ALLOWED or MANDATORY."""
+ n = len(codes)
+ props = [rows[bisect.bisect_right(starts, c) - 1] for c in codes] # (class, pi, pf, east, pictographic)
+ cls = [p[0] for p in props]
+ eff, joined, start, nonspace, number, ri = [0] * n, [False] * n, [0] * n, [None] * n, [False] * n, [0] * n
+ for i in range(n):
+ mark = cls[i] in (CM, ZWJ)
+ previous = eff[i - 1] if i else SP
+ joined[i] = mark and i > 0 and previous not in ENDS + (SP, ZW) # LB9, LB10
+ eff[i] = previous if joined[i] else AL if mark else cls[i]
+ start[i] = start[i - 1] if joined[i] else i
+ nonspace[i] = start[i] if eff[i] != SP else nonspace[i - 1] if i else None
+ e = eff[i]
+ number[i] = number[i - 1] if joined[i] else e == NU or (e in (SY, IS) and i > 0 and number[i - 1])
+ ri[i] = ri[i - 1] if joined[i] else (ri[i - 1] if i else 0) + 1 if e == RI else 0
+
+ def next_unit(i):
+ j = i + 1
+ while j < n and joined[j]:
+ j += 1
+ return j
+
+ def aksara(u):
+ return eff[u] in (AK, AS) or codes[u] == 0x25CC
+
+ def decide(i):
+ a, b, A, B = cls[i - 1], cls[i], eff[i - 1], eff[i]
+ unit, before = start[i - 1], nonspace[i - 1]
+ base = codes[unit]
+ pi, pf, east, pictographic = lambda k: props[k][1], lambda k: props[k][2], lambda k: props[k][3], lambda k: props[k][4]
+ if a == BK: return MANDATORY # LB4
+ if a == CR and b == LF: return NONE # LB5
+ if a in (CR, LF, NL): return MANDATORY
+ if b in ENDS: return NONE # LB6
+ if b in (SP, ZW): return NONE # LB7
+ if before is not None and eff[before] == ZW: return ALLOWED # LB8
+ if a == ZWJ: return NONE # LB8a
+ if joined[i]: return NONE # LB9
+ if WJ in (A, B): return NONE # LB11
+ if A == GL: return NONE # LB12
+ if B == GL and A not in (SP, BA, HY): return NONE # LB12a
+ if B in (CL, CP, EX, SY): return NONE # LB13
+ if before is not None and eff[before] == OP: return NONE # LB14
+ if before is not None and eff[before] == QU and pi(before): # LB15a
+ if not before or eff[before - 1] in ENDS + (OP, QU, GL, SP, ZW): return NONE
+ if B == QU and pf(i): # LB15b
+ j = next_unit(i)
+ if j == n or eff[j] in (SP, GL, WJ, CL, QU, CP, EX, IS, SY, BK, CR, LF, NL, ZW): return NONE
+ if A == SP and B == IS: # LB15c
+ j = next_unit(i)
+ if j < n and eff[j] == NU: return ALLOWED
+ if B == IS: return NONE # LB15d
+ if B == NS and before is not None and eff[before] in (CL, CP): return NONE # LB16
+ if B == B2 and before is not None and eff[before] == B2: return NONE # LB17
+ if A == SP: return ALLOWED # LB18
+ if B == QU and not pi(i): return NONE # LB19
+ if A == QU and not pf(unit): return NONE
+ if B == QU: # LB19a
+ j = next_unit(i)
+ if not east(unit) or j == n or not east(j): return NONE
+ if A == QU and (not east(i) or not unit or not east(start[unit - 1])): return NONE
+ if CB in (A, B): return ALLOWED # LB20
+ if (A == HY or base == 0x2010) and B == AL: # LB20a
+ if (eff[unit - 1] if unit else SP) in ENDS + (SP, ZW, CB, GL): return NONE
+ if B in (BA, HY, NS) or A == BB: return NONE # LB21
+ if (A == HY or (A == BA and not east(unit))) and unit and eff[unit - 1] == HL and B != HL: return NONE # LB21a
+ if A == SY and B == HL: return NONE # LB21b
+ if B == IN: return NONE # LB22
+ if (A in (AL, HL) and B == NU) or (A == NU and B in (AL, HL)): return NONE # LB23
+ if (A == PR and B in (ID, EB, EM)) or (A in (ID, EB, EM) and B == PO): return NONE # LB23a
+ if (A in (PR, PO) and B in (AL, HL)) or (A in (AL, HL) and B in (PR, PO)): return NONE # LB24
+ if B in (PO, PR) and (number[i - 1] or (A in (CL, CP) and unit and number[unit - 1])): return NONE # LB25
+ if A in (PO, PR):
+ if B == NU: return NONE
+ if B == OP:
+ j = next_unit(i)
+ if j < n and eff[j] == IS: j = next_unit(j)
+ if j < n and eff[j] == NU: return NONE
+ if (A in (HY, IS) or number[i - 1]) and B == NU: return NONE
+ if A == JL and B in (JL, JV, H2, H3): return NONE # LB26
+ if (A in (JV, H2) and B in (JV, JT)) or (A in (JT, H3) and B == JT): return NONE
+ if (A in (JL, JV, JT, H2, H3) and B == PO) or (A == PR and B in (JL, JV, JT, H2, H3)): return NONE # LB27
+ if A in (AL, HL) and B in (AL, HL): return NONE # LB28
+ if A == AP and aksara(i): return NONE # LB28a
+ if aksara(unit) and B in (VF, VI): return NONE
+ if A == VI and unit and aksara(start[unit - 1]) and (B == AK or codes[i] == 0x25CC): return NONE
+ if aksara(unit) and aksara(i):
+ j = next_unit(i)
+ if j < n and eff[j] == VF: return NONE
+ if A == IS and B in (AL, HL): return NONE # LB29
+ if A in (AL, HL, NU) and B == OP and not east(i): return NONE # LB30
+ if A == CP and not east(unit) and B in (AL, HL, NU): return NONE
+ if A == RI and B == RI and ri[i - 1] % 2: return NONE # LB30a
+ if B == EM and (A == EB or pictographic(unit)): return NONE # LB30b
+ return ALLOWED # LB31
+
+ return [NONE] + [decide(i) for i in range(1, n)] + [MANDATORY] if n else [NONE]
diff --git a/research/experiments/044-specs/linebreak_check.py b/research/experiments/044-specs/linebreak_check.py
new file mode 100644
index 0000000..3de4ccf
--- /dev/null
+++ b/research/experiments/044-specs/linebreak_check.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+"""Research 140: line breaking (UAX #14, Unicode 16.0) written in aup (aup's examples/linebreak.aup).
+
+Builds the properties' table from the UCD Vega's text is frozen at (its text/unicode.py reads the derived files): ranges
+of code points, each its class after LB1, initial or final quotation (Pi, Pf), East Asian (F, W, H), and pictographic and
+unassigned (Extended_Pictographic and Cn). Loads it into the app (a load message's rows), then gives it every line of
+LineBreakTest.txt and random lines (code points drawn from every class), a line a message, on Pure's three paths
+(batches, rounds, all compiled): each line's boundaries must be the test's (the random lines': linebreak.py's, which
+passes the test whole), the paths the same. pure-run prints views alone (--views): the table would be in every line.
+Then counts lines (code lines: neither blank nor a comment; brace lines: a closing brace alone) of linebreak.aup and of
+linebreak.py. One JSON line; given a directory, it writes the table and the lines there for linebreak_time.c.
+
+ python3 linebreak_check.py [DIRECTORY] [LINES] (LINES: the test's lines to take, all by default)"""
+import json, os, random, struct, subprocess, sys, tempfile
+from pathlib import Path
+
+here = os.path.dirname(os.path.abspath(__file__))
+pure = os.path.normpath(os.path.join(here, '../../..'))
+aup = os.environ.get('AUP', os.path.join(pure, '..', 'aup'))
+vega = os.environ.get('VEGA', os.path.join(pure, '..', 'vega'))
+ucd_path = Path(vega) / 'build/upstream/ucd-16.0.0'
+sys.path.insert(0, here)
+import linebreak # noqa: E402
+
+CLASSES = 'BK CR LF NL SP ZW ZWJ CM WJ GL BA BB B2 HY CB CL CP EX IN NS OP QU IS NU PO PR SY AK AL AP AS EB EM H2 H3 HL ID JL JV JT RI VF VI'.split()
+
+
+def lines(path, stop=None):
+ code = braces = 0
+ for line in open(path):
+ if stop and line.startswith(stop): break
+ text = line.strip()
+ if not text or text.startswith('#'): continue
+ code += 1
+ braces += text == '}'
+ return code, braces
+
+
+def table():
+ """The ranges' starts and their rows (class, pi, pf, east, pictographic), every code point in one."""
+ argv, sys.argv = sys.argv, ['unicode.py', str(ucd_path)]
+ sys.path.insert(0, os.path.join(vega, 'text'))
+ import unicode as ucd
+ sys.argv = argv
+ line = ucd.derived('extracted/DerivedLineBreak.txt', ucd.LINE, ucd.aliases('lb'))
+ category = ucd.derived('extracted/DerivedGeneralCategory.txt', ucd.CATEGORIES)
+ width = ucd.derived('extracted/DerivedEastAsianWidth.txt', ucd.WIDTHS, ucd.aliases('ea'))
+ pictographic = bytearray(ucd.LAST + 1)
+ for first, last, fields, missing in ucd.ranges('emoji/emoji-data.txt'):
+ if not missing and fields[0] == 'Extended_Pictographic':
+ pictographic[first:last + 1] = b'\1' * (last - first + 1)
+ starts, rows = [], []
+ for c in range(ucd.LAST + 1):
+ name, g = ucd.LINE[line[c]], ucd.CATEGORIES[category[c]]
+ if name in ('AI', 'SG', 'XX'): name = 'AL' # LB1
+ elif name == 'SA': name = 'CM' if g in ('Mn', 'Mc') else 'AL'
+ elif name == 'CJ': name = 'NS'
+ row = (CLASSES.index(name), g == 'Pi', g == 'Pf', ucd.WIDTHS[width[c]] in ('F', 'W', 'H'), bool(pictographic[c]) and g == 'Cn')
+ if not rows or rows[-1] != row:
+ starts.append(c)
+ rows.append(row)
+ return starts, rows
+
+
+def test_lines(limit=None):
+ out = []
+ for text in open(ucd_path / 'auxiliary/LineBreakTest.txt', encoding='utf-8'):
+ body = text.split('#', 1)[0].split()
+ if body:
+ out.append(([int(x, 16) for x in body if x not in ('÷', '×')], [x == '÷' for x in body if x in ('÷', '×')]))
+ return out[:limit] if limit else out
+
+
+def random_lines(chooser, starts, rows, count):
+ """Lines of code points drawn from each class's ranges (and from the specials the rules name), their boundaries
+ as linebreak.py works them out."""
+ by_class = {}
+ for k, (start, row) in enumerate(zip(starts, rows)):
+ end = starts[k + 1] if k + 1 < len(starts) else 0x110000
+ by_class.setdefault(row[0], []).append((start, end))
+ specials = [0x2010, 0x25CC, 0x0020, 0x000A, 0x000D, 0x200D, 0x00AB, 0x00BB, 0x201C, 0x201D, 0x1F1E6, 0x1F3FB, 0x1F466]
+ out = []
+ for _ in range(count):
+ codes = []
+ for _ in range(chooser.randrange(1, 9)):
+ if chooser.random() < 0.15:
+ codes.append(chooser.choice(specials))
+ else:
+ start, end = chooser.choice(by_class[chooser.choice(list(by_class))])
+ codes.append(chooser.randrange(start, end))
+ out.append((codes, [b != linebreak.NONE for b in linebreak.breaks(codes, starts, rows)]))
+ return out
+
+
+def run(module, load, given, env):
+ """Each line's boundaries as the app gives them (None where its event failed), pure-run's output streamed."""
+ commands = load + '.commands'
+ with open(commands, 'w') as f:
+ f.write(f'message load @{load}\n')
+ for codes, _ in given:
+ f.write(f"message line {b''.join(struct.pack('<d', c) for c in codes).hex() or '-'}\n")
+ with open(commands) as stdin:
+ process = subprocess.Popen([os.path.join(pure, 'build', 'pure-run'), module, '--views'], stdin=stdin,
+ stdout=subprocess.PIPE, text=True, env=dict(os.environ, **env))
+ got, digest = [], 0
+ for k, line in enumerate(process.stdout):
+ digest = hash((digest, line))
+ if k == 0: continue # the load
+ state = json.loads(line)
+ got.append([b != 0 for b in bytes.fromhex(state['views']['boundaries'])] if state['fault'] == 'none' else None)
+ process.wait()
+ return got, digest
+
+
+if __name__ == '__main__':
+ starts, rows = table()
+ for codes, want in test_lines(): # the reference passes the test whole first
+ assert [b != linebreak.NONE for b in linebreak.breaks(codes, starts, rows)] == want, codes
+ given = test_lines(int(sys.argv[2]) if len(sys.argv) > 2 else None)
+ given += random_lines(random.Random(140), starts, rows, 2000)
+ with tempfile.TemporaryDirectory() as work:
+ load = os.path.join(work, 'ranges.bin')
+ with open(load, 'wb') as f:
+ f.write(b''.join(struct.pack('<6d', start, *row) for start, row in zip(starts, rows)))
+ if len(sys.argv) > 1 and sys.argv[1]:
+ os.makedirs(sys.argv[1], exist_ok=True)
+ with open(os.path.join(sys.argv[1], 'ranges.bin'), 'wb') as f, open(load, 'rb') as g:
+ f.write(g.read())
+ with open(os.path.join(sys.argv[1], 'lines.bin'), 'wb') as f: # each: u32 count, its code points, count + 1 bytes
+ for codes, want in given:
+ f.write(struct.pack('<I', len(codes)) + struct.pack(f'<{len(codes)}I', *codes) + bytes(want))
+ module = os.path.join(work, 'linebreak.pmod')
+ subprocess.run([os.path.join(aup, 'build', 'aup'), 'build', os.path.join(aup, 'examples', 'linebreak.aup'), '-o', module],
+ check=True)
+ right, digests = {}, set()
+ for name, env in (('batches', {'PURE_JIT': '0'}), ('rounds', {'PURE_JIT': '0', 'PURE_BATCH': '0'}), ('all', {'PURE_JIT': 'all'})):
+ got, digest = run(module, load, given, env)
+ right[name] = sum(g == want for g, (_, want) in zip(got, given)) if len(got) == len(given) else 0
+ digests.add(digest)
+ tested = len(given) - 2000
+ aup_code, aup_braces = lines(os.path.join(aup, 'examples', 'linebreak.aup'))
+ python_code, _ = lines(os.path.join(here, 'linebreak.py'))
+ ok = all(n == len(given) for n in right.values()) and len(digests) == 1
+ print(json.dumps({'ranges': len(starts), 'test_lines': tested, 'random_lines': 2000, 'right': right,
+ 'paths_same': len(digests) == 1, 'aup_lines': aup_code, 'aup_brace_lines': aup_braces,
+ 'python_lines': python_code}))
+ sys.exit(0 if ok else 1)
diff --git a/research/experiments/044-specs/linebreak_time.c b/research/experiments/044-specs/linebreak_time.c
new file mode 100644
index 0000000..7b9fe42
--- /dev/null
+++ b/research/experiments/044-specs/linebreak_time.c
@@ -0,0 +1,133 @@
+/* Research 140: line breaking (UAX #14, Unicode 16.0) in aup (aup's examples/linebreak.aup, run by Pure) against the
+ * same algorithm in C (Vega's text/linebreak.c, with its own tables), on the lines linebreak_check.py writes
+ * (LineBreakTest.txt's and its random ones). The app's properties come in as a load message (RANGES: rows of six
+ * binary64s, start, class, pi, pf, east, pictographic). Pure's and Vega's boundaries must be each line's; then each is
+ * timed over all the lines (the median of seven runs of rounds passes).
+ *
+ * cc -O2 -std=c17 -I../../../core -I../../../../vega/text linebreak_time.c ../../../../vega/text/linebreak.c \
+ * ../../../../vega/text/unicode-data.c ../../../build/libpure.a -lm -pthread -o linebreak_time
+ * ./linebreak_time linebreak.pmod RANGES LINES ROUNDS (LINES: each a u32 count, its u32 code points, count + 1
+ * bytes: 1 where a line may break)
+ *
+ * One JSON line: the lines and code points, the nanoseconds per code point of each, Pure's ratio and the instructions
+ * Pure counted for one pass. */
+#define _POSIX_C_SOURCE 200809L
+#include "module.h"
+#include "text.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+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);
+}
+
+static unsigned char *whole(const char *path, size_t *size) {
+ FILE *f = fopen(path, "rb");
+ if (!f) return NULL;
+ fseek(f, 0, SEEK_END);
+ *size = (size_t)ftell(f);
+ rewind(f);
+ unsigned char *bytes = malloc(*size ? *size : 1);
+ if (fread(bytes, 1, *size, f) != *size) { free(bytes); bytes = NULL; }
+ fclose(f);
+ return bytes;
+}
+
+typedef struct { uint32_t count; const uint32_t *codes; const unsigned char *want; unsigned char *payload; } Line;
+
+/* A line's boundaries from Pure: the bytes state "breaks", compared as breaks or not with want. */
+typedef struct { const unsigned char *want; size_t at; bool same; } Compare;
+static bool compare(void *context, PureSpan span) {
+ Compare *c = context;
+ for (size_t i = 0; i < span.size; ++i, ++c->at) c->same = c->same && (span.data[i] != 0) == c->want[c->at];
+ return true;
+}
+
+int main(int argc, char **argv) {
+ if (argc != 5) { fprintf(stderr, "usage: %s MODULE RANGES LINES ROUNDS\n", argv[0]); return 2; }
+ size_t ranges_size, lines_size;
+ unsigned char *ranges = whole(argv[2], &ranges_size), *bytes = whole(argv[3], &lines_size);
+ int rounds = atoi(argv[4]);
+ if (!ranges || !bytes) { fprintf(stderr, "cannot read the ranges or the lines\n"); return 1; }
+ static Line lines[1 << 16];
+ size_t count = 0, points = 0;
+ for (size_t at = 0; at + 4 <= lines_size && count < sizeof(lines) / sizeof(*lines); ++count) {
+ Line *l = &lines[count];
+ memcpy(&l->count, bytes + at, 4);
+ l->codes = (const uint32_t *)(bytes + at + 4);
+ l->want = bytes + at + 4 + 4 * (size_t)l->count;
+ at += 4 + 4 * (size_t)l->count + l->count + 1;
+ l->payload = malloc(8 * (size_t)l->count + 1); /* the message's rows: each code point a binary64 */
+ for (uint32_t i = 0; i < l->count; ++i) {
+ double c = l->codes[i];
+ memcpy(l->payload + 8 * i, &c, 8);
+ }
+ points += l->count;
+ }
+
+ PureModule module = {0};
+ 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 load = pure_module_input(&module, "load"), line = pure_module_input(&module, "line"),
+ breaks = pure_module_state(&module, "breaks");
+ if (pure_app_message(&app, (uint32_t)load, (PureSpan){ranges, ranges_size}) != PURE_OK) { fprintf(stderr, "load failed\n"); return 1; }
+ size_t pure_right = 0, vega_right = 0;
+ uint64_t work = 0;
+ static uint8_t got[4096];
+ for (size_t k = 0; k < count; ++k) {
+ const Line *l = &lines[k];
+ bool done = pure_app_message(&app, (uint32_t)line, (PureSpan){l->payload, 8 * (size_t)l->count}) == PURE_OK;
+ work += app.work;
+ Compare c = {l->want, 0, true};
+ if (done && pure_draft_read(app.texts[breaks], 0, pure_draft_size(app.texts[breaks]), compare, &c) == PURE_OK &&
+ c.same && c.at == l->count + 1)
+ ++pure_right;
+ bool same = l->count < sizeof(got) && vega_line_breaks(l->codes, l->count, got);
+ for (uint32_t i = 0; same && i <= l->count; ++i) same = (got[i] != VEGA_BREAK_NONE) == l->want[i];
+ vega_right += same;
+ }
+ if (pure_right != count || vega_right != count) {
+ fprintf(stderr, "lines %zu: Pure right %zu, Vega right %zu\n", count, pure_right, vega_right);
+ return 1;
+ }
+ double vega_runs[7], pure_runs[7];
+ volatile unsigned sink = 0;
+ for (int r = 0; r < 7 && rounds; ++r) {
+ double start = now_ns();
+ for (int i = 0; i < rounds; ++i)
+ for (size_t k = 0; k < count; ++k) {
+ vega_line_breaks(lines[k].codes, lines[k].count, got);
+ sink += got[0];
+ }
+ vega_runs[r] = (now_ns() - start) / rounds;
+ start = now_ns();
+ for (int i = 0; i < rounds; ++i)
+ for (size_t k = 0; k < count; ++k)
+ if (pure_app_message(&app, (uint32_t)line, (PureSpan){lines[k].payload, 8 * (size_t)lines[k].count}) != PURE_OK)
+ return 1;
+ pure_runs[r] = (now_ns() - start) / rounds;
+ }
+ if (rounds) {
+ qsort(vega_runs, 7, sizeof(double), by_value);
+ qsort(pure_runs, 7, sizeof(double), by_value);
+ }
+ double n = (double)points;
+ const char *jit = getenv("PURE_JIT"), *batch = getenv("PURE_BATCH");
+ printf("{\"lines\":%zu,\"code_points\":%zu,\"right\":%zu,\"jit\":\"%s\",\"batch\":\"%s\",\"c_ns_per_point\":%.1f,"
+ "\"pure_ns_per_point\":%.1f,\"to_c\":%.1f,\"instructions\":%llu,\"instructions_per_point\":%.1f}\n",
+ count, points, pure_right, jit ? jit : "", batch ? batch : "", rounds ? vega_runs[3] / n : 0,
+ rounds ? pure_runs[3] / n : 0, rounds ? pure_runs[3] / vega_runs[3] : 0, (unsigned long long)work, (double)work / n);
+ pure_app_stop(&app);
+ return 0;
+}