From c545227f2461e92afc38a0e1317a288e8c653cc6 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Mon, 19 Oct 2026 05:42:00 +0100 Subject: 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 --- CHECKPOINT.md | 2 +- docs/research/140-spec-suite.md | 66 ++++++++- .../044-specs/2026-10-19-linebreak.jsonl | 6 + research/experiments/044-specs/linebreak.py | 110 +++++++++++++++ research/experiments/044-specs/linebreak_check.py | 147 +++++++++++++++++++++ research/experiments/044-specs/linebreak_time.c | 133 +++++++++++++++++++ tests/check.py | 31 ++++- tests/specs/linebreak/README.md | 16 +++ tests/specs/linebreak/lines.bin | Bin 0 -> 399785 bytes tests/specs/linebreak/ranges.bin | Bin 0 -> 133776 bytes 10 files changed, 508 insertions(+), 3 deletions(-) create mode 100644 research/experiments/044-specs/2026-10-19-linebreak.jsonl create mode 100644 research/experiments/044-specs/linebreak.py create mode 100644 research/experiments/044-specs/linebreak_check.py create mode 100644 research/experiments/044-specs/linebreak_time.c create mode 100644 tests/specs/linebreak/README.md create mode 100644 tests/specs/linebreak/lines.bin create mode 100644 tests/specs/linebreak/ranges.bin diff --git a/CHECKPOINT.md b/CHECKPOINT.md index 94f4d6c..e7fa6fd 100644 --- a/CHECKPOINT.md +++ b/CHECKPOINT.md @@ -26,7 +26,7 @@ | 표 크기의 상태 | 목록이 영속 B+트리다(변경·키 탐색·저장본 위치 O(log n), 스냅숏 O(1), 1,600만 항목, 런타임 상태 1 GiB). pgbench의 TPC-B 같은 이체(세 표의 행과 이력, 한 트랜잭션)가 10만 계좌에서 RAM 24.5k/s(전: 3만 계좌 3.2k/s), SD 카드 내구 1,339/s(SQLite WAL FULL 84–153/s) | [090](docs/research/090-tables.md) | | 일반 트랜잭션 | TPC-C의 다섯 트랜잭션을 aup 앱 하나로 쓴다(여러 표 읽기·쓰기, 조각 훑기, 중복 없는 셈, 실패하면 통째 되돌림). 같은 입력에서 SQLite와 아홉 표의 모든 행이 같다(창고 하나와 넷). RAM 초당 40,976건(W=4 35,838; 조율한 SQLite 10,164, 트랜잭션당 CPU 43–50 µs, HTTP·JSON 포함), SD 카드 내구 4,980건(W=4 4,590). SQLite도 카드에 맞게 조율하고 묶어 커밋하면 739건(W=4 426)이다. 남은 차이는 거래당 쓰는 바이트다(SQLite는 페이지째 써서 8–18배) | [092](docs/research/092-tpcc.md), [106](docs/research/106-tpcc-conditions-first.md), [107](docs/research/107-snapshot-nodes-reused.md) | | 질의 | 질의는 앱의 상태 위에 보기를 더한 모듈이다(`aup query`, `PUT·GET /queries/이름`). 검증한 뒤 스냅숏에서 돌고, 열 안의 fold가 그 행을 읽는다(상관 부질의). 잎이 항목을 필드별로 들고, fold와 필터는 잎 하나씩 돌며(결함이 날 잎만 항목별로), 열 안의 그룹·순위 fold는 목록을 한 번 지나 모든 행에 답한다. 10만 계좌에서 지점별 합계 4.9 ms(SQLite 45, 인덱스를 주면 14.3), 조인 0.48 ms(1.47), 자기 조인 순위 6.8 ms(2,840, 인덱스를 주면 7.6). Pure에는 인덱스 선언이 없다 | [091](docs/research/091-queries.md) | -| 방향(1.0 전) | 앱 계약을 1.0에서 얼릴 후보로 적었다: 이벤트는 전함수이고 원자적이며 결정적이다, 시간은 입력이다, 보기는 커밋된 상태의 순수 함수다, 보이는 역사는 커밋의 앞부분이다, 전달은 순서대로 한 번이다. 계산의 언어(명령 90개, 상태 종류 7개)는 패턴의 모음이라 다시 세운다. 분야마다 고른 명세 열한 개의 묶음에서 오늘의 aup는 바이트, 비트 연산, 중첩, 재귀, 갈래, 글자 읽기에 막힌다(탐침 열 개). 1.0 전에는 호환을 챙기지 않고, JSON과 HTTP는 경계에 남는다. 계산의 핵을 다섯 단계로 다시 세운다(027). 1단계(비트 연산, 자리의 바이트, 수의 리스트, 한 번에 주기)로 aup로 쓴 SHA-256이 공식 벡터를 세 실행 길에서 모두 지난다. 2a(형식 지정, 바이트 잇기와 자르기, 바이트와 text의 let, `while`, 단락 평가의 `&&`·`||`)로 쓴 JSON 검사기가 JSONTestSuite에서 하나도 틀리지 않는다. 2b(레코드 안의 레코드, 갈래와 `match`, 레코드 안의 리스트, 수 읽기)로 JSON을 트리로 읽어 다이제스트가 파이썬과 같다(JSONTestSuite, 무작위 문서 300개). 2c(스스로 부르는 함수: 부름마다 제 스택·지역 칸·리스트·레지스터·담은 값, 깊이는 표로 센 프레임 바이트가 몫에서 묶음; 값으로서의 레코드; 글자 읽기)로 JSON 트리를 재귀 하강으로 다시 써 JSONTestSuite와 무작위 문서 300개가 파이썬과 같고, 10만 겹은 `memory`로 거절한다. 반복 아래에 값을 둔 모듈이 검증을 지나 실행기를 죽이던 구멍도 찾아 막았다. 3단계(중첩한 저장 상태: 저장과 transient의 리스트가 리스트를 담은 레코드를 받고, 그 칸은 값을 항목들의 저장 형태로 바이트처럼 담아 이미지·로그·되돌리기·회복이 그대로다)로 열 → 카드 → 태그의 보드가 메모리 앱과 여덟 번 재시작하는 저장 앱에서 매 걸음 같고(스냅숏 2,400개), 커밋의 여섯 단계 어디서 죽여도 전부 아니면 없음이며, 교체는 이름으로 깊이까지 옮긴다. 중첩 리스트를 바꾸는 비용은 그 값의 크기에 비례한다(이미 있는 항목 하나에 170 ns). 줄 수는 검사기만 파이썬 이하이고(80 대 90), SHA-256(42 대 28)과 트리(142 대 105)는 길다. 시간은 C의 40–159배이고 바이트를 다루는 일은 순수 파이썬과 비슷하거나 느리다(0.43–1.6배): 핵이 선 뒤 빠르게 할 과녁이다. 4단계(패턴 모으기)로 메시지의 행이 중첩을 받고, 키별 문서가 키 있는 리스트에 녹고, 커서와 인덱스가 앵커 하나가 되고, 키·text·타이머·프레임·가리킴이 묶인 메시지가 되고, 보기가 아무 값(리스트 보기: 상태 그대로, 걸러짐, 지도, 본문이 지은 리스트; 바이트)이 되고, 고정 한도가 형식과 리스트 엔진의 너비만 남았다: 명령 106 → 95, 상태 종류 7 → 6, 입력 종류 6 → 3, 그동안 다섯 일의 명령 수와 은행 질의는 걸음마다 1% 안, TPC-C는 3단계 끝과 같다(+0.37%). 5단계(수의 모델: number는 binary64로 리터럴과 셈의 기본, i64는 정확한 정수·비트·돈, `/`와 내림한 몫 `//`, i64의 합은 정확하게, 돈은 i64의 센트로 `.2d`에 보이고, 교체는 정확할 때만 옮긴다)로 TPC-C의 돈과 비율이 Pure와 SQLite 모두에서 정수가 되어, 2만 트랜잭션 뒤 모든 행이 오차 없이 같다: 명령 95 → 96, 다섯 일의 명령 수와 은행 질의는 4단계 끝과 1% 안, TPC-C는 런타임이 같고(−0.03%) i64의 돈으로 앱이 2.6% 빠르다. 027의 다섯 단계를 마쳤다. 그 뒤 묶음의 3(inflate)을 새 명령 없이 썼다: zlib가 만든 스트림 640개와 손상된 240개가 파이썬 참조와 같지만, 우회가 셋(리스트를 받지 않는 fn, 바이트 하나인 상태, 블록 안의 리스트)이고 줄은 파이썬의 1.6배, 시간은 같은 알고리즘의 C의 108배다. 묶음의 1(DNS 메시지)도 새 명령 없이 썼다: dnspython이 만든 메시지 400개를 읽고 다시 써 dnspython이 같게 읽고, 손상된 300개가 파이썬 참조와 같다. 우회는 둘(바이트의 `==`, 보기에서 부르는 블록 fn)이고 둘 다 핵의 일이며, 줄은 파이썬의 1.6배, 시간은 C의 67배(파이썬보다 1.9배 느림)다. 묶음의 9(HTTP/1.1 요청을 패킷마다)도 새 명령 없이 썼다: 스트림 200개를 어디서 잘라도 h11과 같다. 이로써 바이트 하나인 상태와 바이트의 `==`는 써서 닿은 명세가 둘이 되어 핵에 들일 자격이 생겼다(140의 결정점, 설계는 아직) | [026](docs/design/026-app-contract.md), [027](docs/design/027-computation-core.md), [140](docs/research/140-spec-suite.md) | +| 방향(1.0 전) | 앱 계약을 1.0에서 얼릴 후보로 적었다: 이벤트는 전함수이고 원자적이며 결정적이다, 시간은 입력이다, 보기는 커밋된 상태의 순수 함수다, 보이는 역사는 커밋의 앞부분이다, 전달은 순서대로 한 번이다. 계산의 언어(명령 90개, 상태 종류 7개)는 패턴의 모음이라 다시 세운다. 분야마다 고른 명세 열한 개의 묶음에서 오늘의 aup는 바이트, 비트 연산, 중첩, 재귀, 갈래, 글자 읽기에 막힌다(탐침 열 개). 1.0 전에는 호환을 챙기지 않고, JSON과 HTTP는 경계에 남는다. 계산의 핵을 다섯 단계로 다시 세운다(027). 1단계(비트 연산, 자리의 바이트, 수의 리스트, 한 번에 주기)로 aup로 쓴 SHA-256이 공식 벡터를 세 실행 길에서 모두 지난다. 2a(형식 지정, 바이트 잇기와 자르기, 바이트와 text의 let, `while`, 단락 평가의 `&&`·`||`)로 쓴 JSON 검사기가 JSONTestSuite에서 하나도 틀리지 않는다. 2b(레코드 안의 레코드, 갈래와 `match`, 레코드 안의 리스트, 수 읽기)로 JSON을 트리로 읽어 다이제스트가 파이썬과 같다(JSONTestSuite, 무작위 문서 300개). 2c(스스로 부르는 함수: 부름마다 제 스택·지역 칸·리스트·레지스터·담은 값, 깊이는 표로 센 프레임 바이트가 몫에서 묶음; 값으로서의 레코드; 글자 읽기)로 JSON 트리를 재귀 하강으로 다시 써 JSONTestSuite와 무작위 문서 300개가 파이썬과 같고, 10만 겹은 `memory`로 거절한다. 반복 아래에 값을 둔 모듈이 검증을 지나 실행기를 죽이던 구멍도 찾아 막았다. 3단계(중첩한 저장 상태: 저장과 transient의 리스트가 리스트를 담은 레코드를 받고, 그 칸은 값을 항목들의 저장 형태로 바이트처럼 담아 이미지·로그·되돌리기·회복이 그대로다)로 열 → 카드 → 태그의 보드가 메모리 앱과 여덟 번 재시작하는 저장 앱에서 매 걸음 같고(스냅숏 2,400개), 커밋의 여섯 단계 어디서 죽여도 전부 아니면 없음이며, 교체는 이름으로 깊이까지 옮긴다. 중첩 리스트를 바꾸는 비용은 그 값의 크기에 비례한다(이미 있는 항목 하나에 170 ns). 줄 수는 검사기만 파이썬 이하이고(80 대 90), SHA-256(42 대 28)과 트리(142 대 105)는 길다. 시간은 C의 40–159배이고 바이트를 다루는 일은 순수 파이썬과 비슷하거나 느리다(0.43–1.6배): 핵이 선 뒤 빠르게 할 과녁이다. 4단계(패턴 모으기)로 메시지의 행이 중첩을 받고, 키별 문서가 키 있는 리스트에 녹고, 커서와 인덱스가 앵커 하나가 되고, 키·text·타이머·프레임·가리킴이 묶인 메시지가 되고, 보기가 아무 값(리스트 보기: 상태 그대로, 걸러짐, 지도, 본문이 지은 리스트; 바이트)이 되고, 고정 한도가 형식과 리스트 엔진의 너비만 남았다: 명령 106 → 95, 상태 종류 7 → 6, 입력 종류 6 → 3, 그동안 다섯 일의 명령 수와 은행 질의는 걸음마다 1% 안, TPC-C는 3단계 끝과 같다(+0.37%). 5단계(수의 모델: number는 binary64로 리터럴과 셈의 기본, i64는 정확한 정수·비트·돈, `/`와 내림한 몫 `//`, i64의 합은 정확하게, 돈은 i64의 센트로 `.2d`에 보이고, 교체는 정확할 때만 옮긴다)로 TPC-C의 돈과 비율이 Pure와 SQLite 모두에서 정수가 되어, 2만 트랜잭션 뒤 모든 행이 오차 없이 같다: 명령 95 → 96, 다섯 일의 명령 수와 은행 질의는 4단계 끝과 1% 안, TPC-C는 런타임이 같고(−0.03%) i64의 돈으로 앱이 2.6% 빠르다. 027의 다섯 단계를 마쳤다. 그 뒤 묶음의 3(inflate)을 새 명령 없이 썼다: zlib가 만든 스트림 640개와 손상된 240개가 파이썬 참조와 같지만, 우회가 셋(리스트를 받지 않는 fn, 바이트 하나인 상태, 블록 안의 리스트)이고 줄은 파이썬의 1.6배, 시간은 같은 알고리즘의 C의 108배다. 묶음의 1(DNS 메시지)도 새 명령 없이 썼다: dnspython이 만든 메시지 400개를 읽고 다시 써 dnspython이 같게 읽고, 손상된 300개가 파이썬 참조와 같다. 우회는 둘(바이트의 `==`, 보기에서 부르는 블록 fn)이고 둘 다 핵의 일이며, 줄은 파이썬의 1.6배, 시간은 C의 67배(파이썬보다 1.9배 느림)다. 묶음의 9(HTTP/1.1 요청을 패킷마다)도 새 명령 없이 썼다: 스트림 200개를 어디서 잘라도 h11과 같다. 이로써 바이트 하나인 상태와 바이트의 `==`는 써서 닿은 명세가 둘이 되어 핵에 들일 자격이 생겼다(140의 결정점). 그 둘을 핵에 들였다: 바이트의 비교(TEXT_COMPARE의 비트)와 바이트 하나인 상태(028, `values`의 표시로 손상과 옮김을 가른다). aup에는 맨 리스트의 인자와 결과, 블록 안의 let 리스트를 더했다(작은 블록 fn의 인라인은 만들어 쟀으나 효과가 없어 넣지 않았다). 세 명세의 우회를 걷어 내니 줄이 파이썬의 1.5–1.9배가 되었고, DNS는 명령이 21% 줄었다. 묶음의 5(UAX #14 줄 나누기)를 데이터를 입력으로 받아 썼다: LineBreakTest.txt 16,672줄 모두와 무작위 2,000줄이 세 길에서 맞고, 시간은 Vega의 C의 199배다. 상자 배치(8)는 참조(Taffy)와 부분집합을, 단발 타이머(10)는 핵의 원시인지를 먼저 정해야 한다 | [026](docs/design/026-app-contract.md), [027](docs/design/027-computation-core.md), [140](docs/research/140-spec-suite.md) | 키로 나눈 소유자와 저장을 넘길 스레드는 하지 않기로 했다. 이 장치는 동기화를 한 줄로 세워, 나누면 쓰기가 23% 줄고 넘겨도 기기의 일은 줄지 않는다([084](docs/research/084-parallel-syncs.md)). 다만 동기화가 공짜인 RAM에서 TPC-C는 소유자 하나가 diff --git a/docs/research/140-spec-suite.md b/docs/research/140-spec-suite.md index 1963c70..1e42a4c 100644 --- a/docs/research/140-spec-suite.md +++ b/docs/research/140-spec-suite.md @@ -51,7 +51,7 @@ | 2 | SHA-256(FIPS 180-4) | 해시 | 32비트 낱말, 회전, xor | NIST CAVP의 SHA 벡터 | 우회로만 될 것으로 보인다(써 보지 않음) | 썼다: NIST 벡터를 세 실행 길에서 지난다(1단계) | | 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). 쓰지 않음 | +| 5 | UTF-8과 줄 나누기(UAX #14) | 텍스트 | 바이트, 코드 포인트, 속성 표 | Unicode의 `LineBreakTest.txt` | 못 쓴다(글자) | 썼다: LineBreakTest.txt 16,672줄 모두와 무작위 2,000줄이 맞다. 줄 1.8배, C의 199배(아래) | | 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을 부르지 못한다(탐침). 쓰지 않음 | @@ -718,6 +718,70 @@ −0.16%, 묶음 BFS −0.12%, 나머지 ±0.03% 안이다(b48 대 b47). - 원본: `2026-10-19-inflate-after.jsonl`, `-dns-after.jsonl`, `-http-after.jsonl`(검사 한 줄과 세 길의 시간). +## 묶음의 5: 줄 나누기 (UAX #14, 2026-10-19) + +결정점 뒤에 처음 쓴 명세다. 새 명령도 코어의 C도 없이 aup와 Pure만으로 썼다(aup의 `examples/linebreak.aup`). 한 줄의 코드 +포인트마다 경계를 표준의 차례대로 첫 규칙이 정한다(Vega의 `text/linebreak.c`와 같은 길). 경계는 0(없음), 1(허락), 2(반드시)이고 +양 끝도 든다. +- **데이터는 입력이다.** 성질은 Vega의 텍스트가 고정한 UCD 16.0에서 짓는다. 범위 2,787개이고, 범위마다 LB1 뒤의 종류, 여는·닫는 + 인용(Pi, Pf), 동아시아(폭 F, W, H), 그림 문자이면서 미할당(Extended_Pictographic과 Cn)을 담는다. 앱은 그것을 load 메시지의 + 행으로 한 번 받아 시작점의 차례로 저장 리스트에 둔다. 코드 포인트의 범위는 위치로 읽는 이분 탐색(파이썬의 `bisect`)으로 찾는다. + 표를 리스트 리터럴로 모듈에 넣지 않은 까닭: 모듈이 곧 표가 되고, 표가 바뀌면 앱을 다시 짓게 된다. +- **위치마다의 값.** Vega의 C처럼 위치마다의 배열(종류, 단위의 종류, 붙은 표시, 단위의 시작, 공백 아닌 앞 단위, 수, 지역 표시의 + 수, 범위)을 레코드 `Text` 하나에 담아 `decide(t, i)`에 넘긴다(C의 `const Text *t`). +- **종류의 이름.** 인자 없는 식 fn(`fn BK = 0; fn CR = 1; ...`)을 한 줄에 여럿 쓴다. + +**맞음.** +- `linebreak_check.py`: LineBreakTest.txt의 16,672줄 모두와 무작위 2,000줄(종류마다의 범위와 규칙이 이름 대는 문자에서 뽑았고, 기대는 + 같은 규칙의 파이썬 참조 `linebreak.py`가 낸다)이 세 실행 길에서 모두 맞고, 세 길의 출력이 같다. 파이썬 참조도 시험 전체를 지난다. + Vega도 시험 전체를 지난다(Vega의 `make check`). +- pure-run은 이제 `--views`로 상태를 빼고 보기만 찍는다. 표를 담은 앱은 명령마다 표 전체를 찍으므로(2,787행 × 16,672줄), 그대로 + 출력을 모은 첫 실행이 메모리를 다 써 Pi가 재설정되었다. +- Pure의 `make check`(`module_specs`)에 들어갔다. 범위와 줄 전부를 `tests/specs/linebreak`에 두었다(UCD에서 파생, Unicode License + v3의 고지와 함께). + +**막대.** + +| 막대 | 잰 것 | | +|---|---|---| +| 쓸 수 있다 | aup와 Pure만. 새 명령 없음 | 지남 | +| 우회가 없다 | 마찰 셋(아래). 우회라 할 꼴은 없다 | 지남(마찰은 적는다) | +| 줄 수 ≤ 파이썬 참조 | aup 176줄(닫는 괄호만인 줄 19) 대 파이썬 99줄 | **못 미침** | +| C 대비 시간 | 줄 18,672개(코드 포인트 61,285개)에서 코드 포인트당 6,920 ns. Vega의 C(34.7 ns)의 **199배**. 순수 파이썬 2,523 ns(Pure가 **2.7배 느림**). 세 길이 같다 | 기록(막대 없음) | +| 견고하다 | 위의 맞음, 세 길이 같음. 세 기계는 아직 | 일부 | + +**마찰 셋.** +1. **16진 리터럴은 언제나 i64다.** 코드 포인트(수)와 견주는 `0x2010`이 섞이지 않아 십진수 `8208`로 썼다. `number(0x2010)`도 i64로 + 남는다. 리터럴이 만나는 것을 따르는 규칙(5단계)이 16진에도 서면 `0x2010`이 수 자리에서 수가 된다. aup의 일이다. +2. `number`는 타입의 이름이라 let의 이름으로 쓸 수 없다(`numeric`으로 썼다). +3. 리스트의 let은 한 줄에 하나다(`let a, b: list of number`가 안 된다). 아홉 줄이 들었다. + +**줄을 길게 한 것.** 176 대 99다. 닫는 괄호의 줄을 빼면 157이다. +- 레코드 셋(`Range`, `Point`, `Text`)이 26줄이다. 파이썬은 튜플과 리스트다. +- 종류의 상수가 5줄, 값의 상수가 1줄이다(파이썬은 2줄과 1줄). +- 위치마다의 리스트를 짓는 고리가 aup에서 리스트마다 `append` 한 줄이다(파이썬은 리스트를 미리 만들고 칸에 쓴다). 리스트의 let 9줄. +- `if … { return … }`의 규칙 줄은 파이썬과 거의 한 줄씩 맞는다. + +**시간이 가는 곳.** 코드 포인트당 Pure 명령이 1,774개이고, 명령 하나에 약 3.9 ns가 든다. +- 위치마다의 리스트 아홉 개를 짓고 레코드로 묶는 데 260개다. 레코드를 지을 때 let의 리스트마다 항목을 복사한다(LIST_VALUE). +- 범위의 이분 탐색이 360개다(열두 번 남짓 저장 리스트의 항목을 읽는다). C는 종류를 두 단계 표에서 한 번에 읽는다(다른 성질은 + 이분 탐색). +- `decide`가 경계마다 1,150개쯤이다. 레코드로 받은 리스트를 읽을 때마다 레지스터에 묶고, 레코드를 받는 작은 fn(`east`, `aksara`)은 + 인라인되지 않고 함수로 돈다. 그 fn들을 손으로 풀어 써도 명령은 1.8%만 준다(1,774 → 1,742). 규칙의 수가 그만큼 많다. +- 원본: `linebreak.py`(파이썬 참조), `linebreak_check.py`(맞음과 줄 수, 경로를 주면 표와 줄을 쓴다), `linebreak_time.c`(Vega의 C와 + Pure의 시간, 둘 다 줄마다의 답이 기대와 같아야 잰다), `2026-10-19-linebreak.jsonl`. + +## 묶음의 8과 10: 쓰기 전의 결정 (2026-10-19) + +- **8, 상자 배치.** 참조 구현이 필요하다. Yoga와 Taffy는 이 기계에 깔려 있지 않지만, cargo로 Taffy(0.15)를 받을 수 있다. 그래서 + Taffy로 무작위 트리의 배치를 내는 작은 Rust 도구가 알려진 구현이 될 수 있다. 다만 flexbox 알고리즘은 크다. 쓸 + 부분집합(방향, grow·shrink·basis, 고정 크기, padding, gap, justify, align, wrap 없이)을 먼저 정해야 막대를 셈할 + 수 있다. 보기로 쓰면 "보기가 블록 fn을 부르지 못한다"에 닿으므로, 핸들러의 꼴로 쓴다. 아직 쓰지 않았다. +- **10, 재시도·물러서기·시간 제한.** 4d 뒤의 타이머는 모듈에 박힌 주기의 입력(`input tick = every 1 second`)이다. 앱이 "T에 한 번"을 + 거는 길은 없다. 그래서 둘 중 하나다. 주기 틱마다 상태의 마감을 보는 꼴로 쓰면(140이 처음에 적은 우회) 새로 보이는 것이 없다. 앱이 + 자기에게 메시지를 걸어 두는 단발 타이머는 핵의 원시(호스트의 일정, 저장, 재생의 결정성)이고, 명세 하나(10)가 닿는다. 쌓임을 + 막는 규칙대로 적어 두고 만들지 않는다. + ## 착수 기준 (다음 일) - **바꿀 결정.** 계산의 핵을 설계한다(027). 위 표의 여덟 후보 가운데 무엇을 어떤 모양으로 받을지 정한다. 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(' 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(' +#include +#include +#include + +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; +} diff --git a/tests/check.py b/tests/check.py index 046e61a..7a5f50e 100644 --- a/tests/check.py +++ b/tests/check.py @@ -1735,7 +1735,8 @@ def module_specs(aup, pure, work): messages dnspython made (tests/specs/dns) read by its examples/dns.aup and written back by its view to the same records, cut ones refused; HTTP/1.1 request streams (tests/specs/http) read by its examples/http.aup a packet - at a time, cut anywhere, to the same requests.""" + at a time, cut anywhere, to the same requests; line breaking (UAX #14) by its + examples/linebreak.aup on LineBreakTest.txt's lines (tests/specs/linebreak).""" import hashlib import os import random @@ -1933,6 +1934,34 @@ def module_specs(aup, pure, work): assert fed(packets, {})[1] == want, ('http cut', len(packets)) cuts += 1 runs['http'] = {'streams': len(made), 'requests': sum(len(w['requests']) for w in reading), 'cuts': cuts} + # Line breaking (UAX #14, its examples/linebreak.aup): the properties' ranges loaded (tests/specs/linebreak, from the + # UCD 16.0 Vega's text is frozen at), then every line of LineBreakTest.txt and 2,000 random ones, each line's + # boundaries as the test says, on the three paths; pure-run prints views alone (the ranges would be in every line). + program = work / 'linebreak.pmod' + compile_module(aup, EXAMPLES / 'linebreak.aup', program) + given, lines = (pure / 'tests/specs/linebreak/lines.bin').read_bytes(), [] + while given: + n = struct.unpack_from('