From ce7c12cda3a2b0cc3b2c1f28b1fea5b343c704ff Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:56:57 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(jlreq):=20stop=20=C2=A73.8.3's=20ladder?= =?UTF-8?q?=20at=20the=20block=20a=20warichu=20makes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `reduction_sites` offered the ladder every interior boundary of a stacked structure. Two characters of one subline are one such boundary, and so is the SEAM where a note's two sublines meet -- the last character of the first and the first character of the second are adjacent in the text, so Table 1 states an amount between them like anywhere else. That amount stands nowhere. The character that ends a subline reports its body alone and the character that begins the next one begins it at its own row's origin, so the line was never composed from the seam and there is nothing there to give back. Offering it to the ladder divides the line's arrears over a site that cannot pay them: `※〈〉あ※` at a three-em measure needs 250 units back, has one boundary on the line that can give them and one seam that cannot, halves the debt between the two, recovers 125 and reports `layout.overfull` about a line it could have made fit. The expansion ladder already stopped at the block, through the same construct-interior test `boundary_expansion_site` runs; the reduction ladder now asks the same question of a warichu and a furawake. Neither public API nor any other engine changed, and no other census coordinate moves: this is the only shape where a boundary Table 1 names is not a boundary the line was composed from. Fixes #26 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01N8KNy2ejPqy81P2hVcRUjs --- crates/jlreq/src/pipeline.rs | 27 ++++++++- crates/jlreq/tests/public_api.rs | 94 ++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/crates/jlreq/src/pipeline.rs b/crates/jlreq/src/pipeline.rs index 5bd9a4a..bfb7e9c 100644 --- a/crates/jlreq/src/pipeline.rs +++ b/crates/jlreq/src/pipeline.rs @@ -583,6 +583,29 @@ fn is_internal_jidori_boundary(paragraph: &Paragraph, ordinal: usize) -> bool { }) } +/// Whether the boundary after `ordinal` falls inside a warichu or a furawake. +/// +/// Both structures set their text on sublines that run *beside* the line, so a boundary +/// with the clusters on either side of it inside the same structure is no part of what the +/// line was composed from: the space there is the block's own, the seam where two sublines +/// meet carries nothing on the line at all, and §3.8.3's ladder adjusts the spacing of the +/// line (`docs/decisions/stacked-structure-geometry.md`). A boundary with one cluster +/// inside the structure and the next outside it is the line's — that is where the block +/// ends and the line resumes — and this is false there. +fn is_internal_stacked_boundary(paragraph: &Paragraph, ordinal: usize) -> bool { + let Some(cluster) = paragraph.text.clusters().get(ordinal) else { + return false; + }; + let boundary = cluster.range().end; + paragraph.constructs.iter().any(|construct| { + matches!( + construct.kind(), + ConstructKind::Warichu(range) | ConstructKind::Furawake { range, .. } + if range.start < boundary && boundary < range.end + ) + }) +} + fn formula_cluster_range(paragraph: &Paragraph, ordinal: usize) -> Option> { let cluster = paragraph.text.clusters().get(ordinal)?.range(); let range = paragraph.constructs.iter().find_map(|construct| { @@ -949,7 +972,9 @@ fn reduction_sites( ) -> Vec { let mut sites = Vec::new(); for ordinal in line_start..line_end.saturating_sub(1) { - if is_internal_jidori_boundary(paragraph, ordinal) { + if is_internal_jidori_boundary(paragraph, ordinal) + || is_internal_stacked_boundary(paragraph, ordinal) + { continue; } if is_western_word_space(paragraph, ordinal) { diff --git a/crates/jlreq/tests/public_api.rs b/crates/jlreq/tests/public_api.rs index aa845ae..dc0ffd6 100644 --- a/crates/jlreq/tests/public_api.rs +++ b/crates/jlreq/tests/public_api.rs @@ -895,6 +895,100 @@ fn warichu_builds_two_balanced_sublines_and_can_straddle_main_lines() { ); } +/// §3.8.3's ladder stops where the line stops: at the block a warichu makes. +/// +/// `※〈〉あ※`, a three-member half-em note between two full-em characters. §3.4.2's +/// balance divides the note two and one, so `〈〉` share the first subline and `あ` has +/// the second, and Table 1 states exactly two nonzero amounts on the line — both 250 +/// units, half of the note's own half em. One of them, `cl-19`→`cl-01`, stands before the +/// block and is on the line. The other, `cl-02`→`cl-15`, falls on the *seam* where the +/// note's two sublines meet: the character that ends a subline reports its body alone, so +/// nothing of that amount was ever placed. +/// +/// The line's natural width is 3,250. At a 3,000 measure §3.8.3 has 250 units to give +/// back and exactly one boundary to give them back at. Offering it the seam as well — the +/// two are the same `1/2-0 stage 5` cell of Table 3 — halves what the line recovers and +/// leaves it overfull by the other half, which is the defect issue #26 records and +/// `docs/decisions/stacked-structure-geometry.md` settles. +#[test] +fn the_reduction_ladder_does_not_reach_the_seam_between_a_warichu_s_sublines() { + fn note() -> ShapedText { + let clusters = + "※〈〉あ※" + .char_indices() + .enumerate() + .map(|(ordinal, (start, character))| { + let inside = (1..=3).contains(&ordinal); + let advance = if inside { 500 } else { 1_000 }; + let cluster = + Cluster::new(start..start.saturating_add(character.len_utf8()), advance); + if inside { + cluster.with_size(Size::square(500).expect("positive note size")) + } else { + cluster + } + }); + ShapedText::new( + "※〈〉あ※", + Size::square(1_000).expect("positive main-text size"), + Frame::FullEm, + clusters, + ) + .expect("valid seam fixture") + } + + fn compose_at(measure: i32) -> jlreq::Layout { + let paragraph = Paragraph::builder(note(), measure) + .constructs([Construct::warichu(3..12)]) + .build() + .expect("valid seam paragraph"); + jlreq::compose(¶graph, &Style::default()) + } + + let natural = compose_at(16_000); + assert_eq!(natural.lines()[0].inline_extent(), 3_250); + assert_eq!( + natural.lines()[0] + .clusters() + .iter() + .map(jlreq::ClusterPlacement::inline) + .collect::>(), + vec![0, 1_250, 1_750, 1_250, 2_250], + "the seam's own amount stands nowhere on the line even where nothing adjusts" + ); + + let reduced = compose_at(3_000); + assert_eq!(reduced.lines().len(), 1); + let line = &reduced.lines()[0]; + assert_eq!( + line.inline_extent(), + 3_000, + "the whole 250 units come off the one boundary the line was composed from" + ); + assert_eq!(line.block_extent(), 1_000); + assert_eq!( + line.clusters() + .iter() + .map(|cluster| (cluster.inline(), cluster.block(), cluster.advance())) + .collect::>(), + vec![ + (0, 0, 1_250), + (1_000, 0, 500), + (1_500, 0, 500), + (1_000, 500, 500), + (2_000, 0, 1_000) + ] + ); + assert!( + reduced.diagnostics().is_empty(), + "a line that gave back everything it needed is not overfull" + ); + + let halfway = compose_at(3_125); + assert_eq!(halfway.lines()[0].inline_extent(), 3_125); + assert!(halfway.diagnostics().is_empty()); +} + #[test] fn furawake_aligns_declared_sublines_and_never_becomes_an_outer_break() { for mode in [WritingMode::HorizontalTb, WritingMode::VerticalRl] { From 0f8d34d11cfc6eedbfb8b422bfc61846ffd6f786 Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:57:19 +0900 Subject: [PATCH 2/3] fix(racket): give a furawake its columns' own spacing and its own far edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `compose.rkt` gathers a §3.7.2 furawake into ONE item of the line, where a §3.4.2 warichu's members stay items of their own. Two things followed from that and both were wrong. A column was measured from bare cluster advances, so a member standing before a Table 1 amount inside its own column -- `cl-02` then `cl-01`, half of the member's own em -- reported 1000 while the same engine placed the next member 1000 units on and charged the line 1500 for the step. That is the defect #24 fixed for a warichu, at the other structure the same reading is addressed to: a column is ordinary text of the structure, so a member's advance is its body plus the amount after it inside its row, and the member that ends a row reports its body alone. And the one item carried the class of its FIRST cluster, so the boundary AFTER the block was answered at the wrong end of it. A structure has two edges and they are two different characters: the amount before the block is Table 1's answer at the block's first character and the amount after it the answer at its last. The `item` struct now carries a `trailing` edge -- the occurrence a boundary after the item is read against, which is the item itself for anything one character wide -- and every reading of the before side of a boundary asks for it. A tate-chu-yoko run carries none: §3.2.5 makes it cl-30 at both of its edges however many characters it holds. The two errors are equal and opposite wherever the two amounts are, which is why `※〉〈日※` comes out 4500 wide either way and `※・・日※` does not. Both halves had to move together. Neither public API nor any other engine changed, and `across` in `furawake-item` -- computed and read by nobody since the block's depth moved to `block-room` -- goes with them. Fixes #27 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01N8KNy2ejPqy81P2hVcRUjs --- engines/racket/compose.rkt | 103 +++++++++++++++++--------- engines/racket/spacing.rkt | 27 ++++++- engines/racket/tests/test-compose.rkt | 71 ++++++++++++++++++ 3 files changed, 166 insertions(+), 35 deletions(-) diff --git a/engines/racket/compose.rkt b/engines/racket/compose.rkt index 9ff6ccc..fafc473 100644 --- a/engines/racket/compose.rkt +++ b/engines/racket/compose.rkt @@ -143,11 +143,13 @@ [(eq? (cluster-frame-of para one) 'proportional) 'rotate-clockwise] [else 'identity])) -(define (piece-of para one index transform writing-mode [inline 0] [block 0]) +;; `advance` overrides the caller's own where a member of a block reports the step +;; that reaches the member beside it rather than its bare body. +(define (piece-of para one index transform writing-mode [inline 0] [block 0] [advance #f]) (piece index (cluster-start one) (cluster-end one) - (cluster-advance one) + (or advance (cluster-advance one)) (cluster-size-of para one) (cluster-frame-of para one) transform @@ -215,7 +217,9 @@ (>= (cluster-end one) (construct-end each)) (<= (cluster-start one) (construct-start each))))) => values] - [else #f]))) + [else #f]) + ;; One character is its own far edge. + #f)) (define (tab-character? para one) (define text (source-slice (paragraph-source para) (cluster-start one) (cluster-end one))) @@ -269,7 +273,10 @@ 'tate-chu-yoko 'tate-chu-yoko members - #f #f 0 0 '() #f)) + ;; §3.2.5 makes the whole run cl-30, so a run is the same class at both of + ;; its edges however many characters it holds and carries no far edge of its + ;; own. + #f #f 0 0 '() #f #f)) ;; The things that stand on the line: the caller's clusters, with each construct's ;; own clusters gathered into the one item that construct is. @@ -293,7 +300,7 @@ at)) (define held (for/list ([at (in-list inside)]) (vector-ref clusters at))) (set! out (cons (if (eq? (construct-kind (cdr found)) 'furawake) - (furawake-item para style (cdr found) held inside next) + (furawake-item para style (cdr found) held inside next formulae) (run-item para held inside next)) out)) (walk (+ index (length inside)) (add1 next))] @@ -315,21 +322,45 @@ ;; break opportunities inside it: the whole structure is one position on the main ;; line however many rows it holds, so a break the caller stated inside one is the ;; column division and not a place the main line may end. -(define (furawake-item para style one clusters indices index) +(define (furawake-item para style one clusters indices index formulae) (define columns (cdr (assq 'columns (construct-payload one)))) (define gap (cdr (assq 'line-gap (construct-payload one)))) (define stated (for/list ([each (in-list (paragraph-breaks para))] - #:when (for/or ([piece (in-list clusters)] [rank (in-naturals)]) - (and (> rank 0) (= (brk-offset each) (cluster-start piece))))) - (for/first ([piece (in-list clusters)] [rank (in-naturals)] - #:when (= (brk-offset each) (cluster-start piece))) + #:when (for/or ([held (in-list clusters)] [rank (in-naturals)]) + (and (> rank 0) (= (brk-offset each) (cluster-start held))))) + (for/first ([held (in-list clusters)] [rank (in-naturals)] + #:when (= (brk-offset each) (cluster-start held))) rank))) (define rows (rows-of columns (length clusters) (sort (remove-duplicates stated) <))) - (define widths + ;; The block's own characters, as items of their own. §3.7.2's columns are + ;; ordinary text of the structure, so the boundary between two members of one row + ;; is a boundary between two characters like any other -- and Table 1 is asked of + ;; occurrences rather than of clusters. + (define held + (for/vector ([each (in-list clusters)] [at (in-list indices)]) + (plain-item para style each at formulae))) + ;; What a member reports, and what the row it stands on is measured from: its own + ;; body plus the amount Table 1 states after it INSIDE its row. The member that + ;; ends a row reports its body alone, because nothing of that row stands after it + ;; -- what Table 1 states past the block's last character stands beside the whole + ;; block, on the main line (docs/decisions/stacked-structure-geometry.md). + (define (member-advance row rank) + (define at (list-ref row rank)) + (define own (item-advance (vector-ref held at))) + (if (= (add1 rank) (length row)) + own + (chk+ own + (total-of (boundary-contributions (vector-ref held at) + (vector-ref held (list-ref row (add1 rank))) + (paragraph-writing-mode para) + style))))) + (define steps (for/list ([row (in-list rows)]) - (for/fold ([sum 0]) ([at (in-list row)]) - (chk+ sum (cluster-advance (list-ref clusters at)))))) + (for/list ([rank (in-range (length row))]) (member-advance row rank)))) + (define widths + (for/list ([row (in-list steps)]) + (for/fold ([sum 0]) ([each (in-list row)]) (chk+ sum each)))) (define heights (for/list ([row (in-list rows)]) (for/fold ([most 0]) ([at (in-list row)]) @@ -337,23 +368,21 @@ (define vertical? (eq? (paragraph-writing-mode para) 'vertical-rl)) (define blocks (row-block-offsets heights gap (extent-block (paragraph-size para)) vertical?)) (define along (for/fold ([most 0]) ([one (in-list widths)]) (max most one))) - (define across - (chk+ (for/fold ([sum 0]) ([one (in-list heights)]) (chk+ sum one)) - (chk* gap (max 0 (sub1 (length heights)))))) (define members (append* - (for/list ([row (in-list rows)] [block (in-list blocks)]) - (let walk ([rest row] [at 0] [out '()]) + (for/list ([row (in-list rows)] [row-steps (in-list steps)] [block (in-list blocks)]) + (let walk ([rest row] [ahead row-steps] [at 0] [out '()]) (cond [(null? rest) (reverse out)] [else - (define piece (list-ref clusters (car rest))) + (define each (list-ref clusters (car rest))) (walk (cdr rest) - (chk+ at (cluster-advance piece)) - (cons (piece-of para piece (list-ref indices (car rest)) - (transform-of para piece) + (cdr ahead) + (chk+ at (car ahead)) + (cons (piece-of para each (list-ref indices (car rest)) + (transform-of para each) (paragraph-writing-mode para) - at block) + at block (car ahead)) out))]))))) (item index (cluster-start (car clusters)) @@ -366,11 +395,16 @@ (cluster-size-of para (car clusters)) (cluster-frame-of para (car clusters)) #f - (classify-cluster para (car clusters) style) + (item-class (vector-ref held 0)) 'identity 'furawake members - #f #f 0 0 '() #f)) + #f #f 0 0 '() #f + ;; The block's far edge. A warichu's members stay items of the line and each + ;; is its own edge; a furawake is ONE item, so the character a boundary after + ;; the block is read against has to be carried here rather than found in the + ;; item vector. + (vector-ref held (sub1 (vector-length held))))) ;; ---------------------------------------------------------------------------- ;; Break opportunities @@ -596,7 +630,8 @@ (for/vector ([index (in-range (add1 count))]) (cond [(= index 0) (head-contributions (vector-ref items first) style first-line? writing-mode)] - [(= index count) (end-contributions (vector-ref items last) style writing-mode next)] + [(= index count) + (end-contributions (trailing-edge (vector-ref items last)) style writing-mode next)] [(after-head-space? items first count index) '()] ;; Inside one warichu block the boundaries are the block's own business: ;; the note is one position on the main line and the space between two of @@ -604,7 +639,7 @@ ;; boundary where the block BEGINS is on the line, and Table 1 states it. [(inside-one-block? index) '()] [else - (boundary-contributions (vector-ref items (+ first index -1)) + (boundary-contributions (trailing-edge (vector-ref items (+ first index -1))) (vector-ref items (+ first index)) writing-mode style)]))) @@ -624,7 +659,7 @@ ;; block's own interior on the line's arrears. [(inside-one-block? index) '()] [else - (boundary-contributions (vector-ref items (+ first index -1)) + (boundary-contributions (trailing-edge (vector-ref items (+ first index -1))) (vector-ref items (+ first index)) writing-mode style @@ -850,7 +885,9 @@ (for/list ([index (in-range 1 (add1 count))]) (define terms (vector-ref raw-gap-terms index)) (define before-class - (if (= index count) (item-class (vector-ref items last)) (item-class (vector-ref items (+ first index -1))))) + (if (= index count) + (item-class (trailing-edge (vector-ref items last))) + (item-class (trailing-edge (vector-ref items (+ first index -1)))))) (define after-class (if (= index count) line-edge (item-class (vector-ref items (+ first index))))) (gap-openings table items first index terms before-class after-class))))) @@ -893,7 +930,7 @@ (define (term-em items first index one) (if (eq? (contribution-owner one) 'after) (item-em (vector-ref items (+ first index))) - (item-em (vector-ref items (+ first index -1))))) + (item-em (trailing-edge (vector-ref items (+ first index -1)))))) ;; Give back `wanted`, in stage order, and report what is left. (define (reduce para style items first last advances gaps raw-advances raw-gap-terms wanted) @@ -1000,7 +1037,7 @@ (item-em (vector-ref items (+ first offset))))) (append* (for/list ([index (in-range 1 count)]) - (define before (vector-ref items (+ first index -1))) + (define before (trailing-edge (vector-ref items (+ first index -1)))) (define after (vector-ref items (+ first index))) (define em (boundary-em (vector-ref raw-gap-terms index) before after)) (define found (expansion-of (item-class before) (item-class after) em)) @@ -1811,11 +1848,11 @@ (define writing-mode (paragraph-writing-mode para)) (cond [(= offset (sub1 count)) - (total-of (end-contributions (vector-ref items last) style writing-mode + (total-of (end-contributions (trailing-edge (vector-ref items last)) style writing-mode (and (< (add1 last) (vector-length items)) (vector-ref items (add1 last)))))] [(after-head-space? items first count (add1 offset)) 0] [else - (total-of (boundary-contributions (vector-ref items (+ first offset)) + (total-of (boundary-contributions (trailing-edge (vector-ref items (+ first offset))) (vector-ref items (+ first offset 1)) writing-mode style))])) diff --git a/engines/racket/spacing.rkt b/engines/racket/spacing.rkt index 4737f36..850ba9a 100644 --- a/engines/racket/spacing.rkt +++ b/engines/racket/spacing.rkt @@ -44,7 +44,8 @@ total-of prohibited? stated-cell - item-em) + item-em + trailing-edge) ;; One occurrence on the line: the cluster it came from, and everything the layout ;; needs to know about it. @@ -65,8 +66,18 @@ ;; part of one: `(formula display?)` for §3.7.4, `(warichu index)` for the text of ;; an inline cutting note. It is #f for everything a line sets straight along ;; itself. +;; +;; `trailing` is the far half of the item's own class: the occurrence a boundary +;; AFTER this item is read against, where that is not this item itself. One +;; character is its own far edge and carries #f. A structure that gathers several +;; clusters into one item is a DIFFERENT character at each of its two edges, and a +;; structure has two edges: Table 1's answer before the block is asked at the block's +;; first character and its answer after the block at the block's last +;; (`docs/decisions/stacked-structure-geometry.md`). §3.7.2's furawake is the item +;; that needs it. A tate-chu-yoko run does not: §3.2.5 makes it cl-30 at both of its +;; edges however many characters it holds. (struct item (index start end advance size frame role class transform kind members - complex run separation tail attachments structure) + complex run separation tail attachments structure trailing) #:transparent) ;; One cluster of a grouped item. @@ -103,6 +114,18 @@ (define (item-em one) (extent-inline (item-size one))) +;; The occurrence a boundary AFTER `one` is read against. +;; +;; Every reading of a boundary asks Table 1 about the character before it and the +;; character after it. For an item that is one character those are the item itself +;; on one side and its neighbor on the other; for a block that gathers several +;; clusters into one item the character before the boundary after it is the LAST of +;; them, not the first (`docs/decisions/stacked-structure-geometry.md`). The +;; boundary BEFORE such an item is unchanged: the item's own class is the first +;; character's already. +(define (trailing-edge one) + (or (item-trailing one) one)) + ;; `amount` is in 1/720 em; bring it into the caller's unit against `em`. ;; ;; Every amount the six matrices state is an exact multiple of 1/720 em and every em diff --git a/engines/racket/tests/test-compose.rkt b/engines/racket/tests/test-compose.rkt index 176221d..9a8932c 100644 --- a/engines/racket/tests/test-compose.rkt +++ b/engines/racket/tests/test-compose.rkt @@ -457,6 +457,77 @@ ((0 0 1000) (1 1000 750) (2 1750 500) (3 1000 500) (4 2250 1000))))) (check-equal? (notes-of (note-with-a-gap 3000)) '(("layout.overfull" 0 15))) + ;; ------------------------------------------------------------------ + ;; §3.7.2: the same two readings inside a furawake's own columns + ;; ------------------------------------------------------------------ + + ;; A furawake is the other structure docs/decisions/stacked-structure-geometry.md is + ;; addressed to, and both halves of the reading above hold in it. Its columns are + ;; ordinary text of the structure, so a boundary between two members of one column + ;; is Table 1's ordinary answer and the member before it reports the step that + ;; reaches the one beside it; and the structure has two edges, so what Table 1 + ;; states after the block is asked at the block's LAST character rather than at its + ;; first. + ;; + ;; `※〉〈日※` in a sixteen-em measure, the furawake over `〉〈日` in two columns with + ;; a one-fifth-em line gap, and the caller's one opportunity before `日` -- which is + ;; where §3.7.2 divides the columns, so `〉〈` is the first and `日` the second. + ;; Table 1 states 1/2 be between `cl-02` and `cl-01`, half of the closing bracket's + ;; own full em, so `〉` reports 1000 + 500 = 1500 and `〈` stands 1500 past it at + ;; 2500. `〈` ends its column and reports its body alone. The first column is 2500 + ;; wide and the block with it. The boundary after the block is `日`→`※`, which + ;; Table 1 leaves blank, so the `※` after the structure stands at 3500 and the line + ;; is 4500. Reading that boundary against `〉`, the block's first character, would + ;; put the same 500 units there instead and give the same 4500 by two mistakes. + (check-equal? (layout-of (request "※〉〈日※" + (list (cluster* 0 3 1000) + (cluster* 3 6 1000) + (cluster* 6 9 1000) + (cluster* 9 12 1000) + (cluster* 12 15 1000)) + 16000 + #:alignment "start" + #:breaks (list (break* 9 "allowed")) + #:constructs (list (hasheq 'kind "furawake" 'range '(3 12) + 'columns 2 'line_gap 200)))) + '(((0 15) 0 4500 + ((0 0 1000) (1 1000 1500) (2 2500 1000) (3 1000 1000) (4 3500 1000))))) + + ;; The pair that separates the two mistakes. `※・・日※` is the same shape with a + ;; middle dot on either side of the in-column boundary: Table 1 states 1/4 be + 1/4 + ;; af between two `cl-05` occurrences, 500 units inside the column, against the 250 + ;; a `cl-05` takes before the `※` that opens the line. The first column is 2500 and + ;; the line 1250 + 2500 + 1000 = 4750, where reading the block's far edge as its + ;; near one answers 4500. + (check-equal? (layout-of (request "※・・日※" + (list (cluster* 0 3 1000) + (cluster* 3 6 1000) + (cluster* 6 9 1000) + (cluster* 9 12 1000) + (cluster* 12 15 1000)) + 16000 + #:alignment "start" + #:breaks (list (break* 9 "allowed")) + #:constructs (list (hasheq 'kind "furawake" 'range '(3 12) + 'columns 2 'line_gap 200)))) + '(((0 15) 0 4750 + ((0 0 1250) (1 1250 1500) (2 2750 1000) (3 1250 1000) (4 3750 1000))))) + + ;; The block's own two extents are unchanged by either reading: two rows one em deep + ;; with a one-fifth-em gap between them is 2200, centered on the paragraph's own em. + (check-equal? (blocks-of (request "※〉〈日※" + (list (cluster* 0 3 1000) + (cluster* 3 6 1000) + (cluster* 6 9 1000) + (cluster* 9 12 1000) + (cluster* 12 15 1000)) + 16000 + #:alignment "start" + #:breaks (list (break* 9 "allowed")) + #:constructs (list (hasheq 'kind "furawake" 'range '(3 12) + 'columns 2 'line_gap 200)))) + '((0 2200 (0 -600 -600 600 0)))) + ;; ------------------------------------------------------------------ ;; Appendix C: what may be broken ;; ------------------------------------------------------------------ From aa8969745a55e99e1822b7d484ce81b39a66c8e7 Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:57:42 +0900 Subject: [PATCH 3/3] docs(decisions): publish the seam and the two edges, and reach both at every class pair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `engines/ocaml/probe/census.ml` gains three `constructs` variants. The first is the sixth shape PR #25 deliberately withheld: `warichu-pair-inside-reduced` stands the pair under test inside a note on a line that has to give space back, which is where the two answers to "does §3.8.3's ladder reach the seam" come apart in the line's own width. The other two are `furawake-pair-inside` and its vertical mirror, which stand the pair inside one of a furawake's columns and put an ordinary character in the other, so one request asks both what Table 1 does inside a column and which of the block's characters the boundary after it is read against. The `constructs` census is 20,102 requests and all ten are at zero differences across the three engines over 122,199. Against the previous engines the same census reports 15 differing responses for the Rust one and 434 for the Racket one. `docs/decisions/stacked-structure-geometry.md` states both readings, why they follow from the sentences already published there rather than from anything new, and what a revision would have to say to settle either one the other way. The seam is the only boundary of a block that is neither strictly inside a subline nor at the block's edge, which is how an engine can reach it by asking Table 1 about two adjacent characters without asking which object they are adjacent on. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01N8KNy2ejPqy81P2hVcRUjs --- CHANGELOG.md | 39 +++++++++++ ROADMAP.md | 2 +- data/manifest.toml | 2 +- docs/decisions/README.md | 2 +- .../jidori-room-and-solid-boundaries.md | 2 +- docs/decisions/ornamented-complex-geometry.md | 2 +- docs/decisions/ruby-overhang-permission.md | 2 +- docs/decisions/stacked-structure-geometry.md | 68 ++++++++++++++++++- docs/decisions/warichu-bracket-listing.md | 2 +- engines/ocaml/README.md | 6 +- engines/ocaml/probe/census.ml | 48 +++++++++++-- engines/racket/README.md | 6 +- 12 files changed, 159 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06f6f33..0d03e79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,45 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed +- §3.8.3's ladder no longer reaches the seam where a warichu's two sublines meet, which + closes [#26](https://github.com/P4suta/jlreq/issues/26) — the first coordinate at which + two reference engines disagreed with *each other* rather than one lagging the other two, + and the one the entry below left out of the census on purpose. + `crates/jlreq/src/pipeline.rs` offered `reduction_sites` every interior boundary of a + stacked structure, the seam included, so a line carrying `※〈〉あ※` at a three-em measure + divided its 250 units of arrears over the one boundary it was composed from and one + inside the block that carries nothing: it gave back 125, ended 125 units over its + measure, and reported `layout.overfull` about a line it could have made fit. The + expansion ladder already stopped at the block, and the reduction ladder now asks the same + question. `docs/decisions/stacked-structure-geometry.md` publishes the reading the OCaml + and Racket engines already had: the seam is the block's like every other boundary inside + it, because what makes a boundary the line's is that the line was composed from it and at + the seam no engine places anything — the character that ends a subline reports its body + alone. No public API changed, and no other engine did. +- The Racket reference engine now lays a furawake's columns out from the advances the + structure was composed from and reads the boundary after the block against the block's + own last character, which closes + [#27](https://github.com/P4suta/jlreq/issues/27). `engines/racket/compose.rkt` made one + item of a furawake and measured each of its columns from bare cluster advances, so a + member standing before a Table 1 amount inside its column — `cl-02` then `cl-01`, half of + the member's own em — reported 1000 where the same engine placed the next member 1000 + units on and charged the line 1500 for the step; and it gave that one item the class of + its *first* cluster, so the boundary after the block was answered at the wrong end of it + and the amount missing from the column reappeared beside the structure. The two mistakes + cancel in the line's own width wherever the two amounts are equal, which is why the shape + had to be swept over every class pair to be seen. A furawake's members are now items of + their own for the purpose Table 1 is asked at, and the `item` struct carries a `trailing` + edge — the occurrence a boundary *after* the item is read against — which is the item + itself for everything one character wide and the block's last character for a furawake. + A tate-chu-yoko run carries none: §3.2.5 makes it cl-30 at both of its edges. No public + API and no other engine changed. +- Both readings above are reached at every class pair now. + `engines/ocaml/probe/census.ml` adds three `constructs` variants — + `warichu-pair-inside-reduced`, the sixth shape the entry below deliberately withheld, and + `furawake-pair-inside` with its vertical mirror — so the `constructs` census is 20,102 + requests and all ten censuses are at zero differences across the three engines over + 122,199. Against the previous engines the same census reports 15 differing responses for + the Rust one and 434 for the Racket one. - The Racket reference engine now answers the two coordinates inside a warichu block that the other two engines already agreed on, which closes [#23](https://github.com/P4suta/jlreq/issues/23) and diff --git a/ROADMAP.md b/ROADMAP.md index 725f28b..f4f3b4e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -44,7 +44,7 @@ conformance protocol, gated on a milestone sequence of its own advanced toward the built-in suite one disjoint milestone per pull request and now claims milestone 9, the last one: all eighty-nine cases answer bit for bit, so `just ocaml-gate` and `just conform-ocaml` are the same run and the required CI job holds the engine to the -whole suite. Ten synthetic censuses agree with the Rust engine across 120,612 further +whole suite. Ten synthetic censuses agree with the Rust engine across 122,199 further requests, and the twenty-six observable policies the exercise turned up — rules two engines must share to pass the same case, stated in no sentence of JLReq and no file under `docs/` — are listed in `engines/ocaml/README.md` and are candidates for `docs/decisions/`. diff --git a/data/manifest.toml b/data/manifest.toml index d37c07f..6200361 100644 --- a/data/manifest.toml +++ b/data/manifest.toml @@ -11,7 +11,7 @@ [[file]] path = "ROADMAP.md" -sha256 = "d804757056cc7509c09721c86427db5405febca50853971416a14c63035dc47d" +sha256 = "a6b10efed08e330003e172ee7fd9fcf1a8b3b81e5df62c731833df3e685020b6" [[file]] path = "crates/jlreq-conformance/protocol.schema.json" diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 5ab171e..e62f3f4 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -35,7 +35,7 @@ reaches it, where any does — so that a reader can reproduce the observation ra the claim. A *third* engine ([engines/racket/](../../engines/racket/README.md)) has since been brought -to zero differences against both of the others on all ten censuses, 120,612 requests, and +to zero differences against both of the others on all ten censuses, 122,199 requests, and that convergence turned up nine more policies of the same kind. Two of them are subjects nothing here covered and have files of their own in the table below; the other seven belong to subjects already published and were written into those files, one of them into a reading diff --git a/docs/decisions/jidori-room-and-solid-boundaries.md b/docs/decisions/jidori-room-and-solid-boundaries.md index 5417876..4efb9e8 100644 --- a/docs/decisions/jidori-room-and-solid-boundaries.md +++ b/docs/decisions/jidori-room-and-solid-boundaries.md @@ -11,7 +11,7 @@ SPDX-License-Identifier: MIT OR Apache-2.0 [`engines/racket/structure.rkt`](../../engines/racket/structure.rkt) - Standing: `Unstated` - JLReq: §3.7.3, §B.1, §C.3, §3.8.3 -- Observed by: `just census tabs` (31,211 requests) and `just census constructs` (18,515 +- Observed by: `just census tabs` (31,211 requests) and `just census constructs` (20,102 requests), the jidori variants; no built-in case reaches a jidori with no open boundary ## The silence diff --git a/docs/decisions/ornamented-complex-geometry.md b/docs/decisions/ornamented-complex-geometry.md index f385921..623955a 100644 --- a/docs/decisions/ornamented-complex-geometry.md +++ b/docs/decisions/ornamented-complex-geometry.md @@ -11,7 +11,7 @@ SPDX-License-Identifier: MIT OR Apache-2.0 [`engines/ocaml/lib/pipeline.ml`](../../engines/ocaml/lib/pipeline.ml) - Standing: `Unstated` - JLReq: §3.3.9, §3.7.1, §B.2#9, §C.2#6, §E.2#5 -- Observed by: `just census constructs` (18,515 requests), the emphasis and script variants +- Observed by: `just census constructs` (20,102 requests), the emphasis and script variants ## The silence diff --git a/docs/decisions/ruby-overhang-permission.md b/docs/decisions/ruby-overhang-permission.md index 0f360a2..cfdaf9c 100644 --- a/docs/decisions/ruby-overhang-permission.md +++ b/docs/decisions/ruby-overhang-permission.md @@ -150,7 +150,7 @@ is the one under which the sentence is about something. **Nothing before M6 could see this.** cl-09 and cl-10 are the same row and the same column in all six matrices, at every coordinate except those four ruby cells. An engine could carry -either reading through 120,612 census requests and every one of the eighty-nine conformance +either reading through 122,199 census requests and every one of the eighty-nine conformance cases without the difference surfacing anywhere but here. That is the reason to publish it rather than leave it in a comment: it is invisible until it is not, and the next independent engine has no way to derive it. diff --git a/docs/decisions/stacked-structure-geometry.md b/docs/decisions/stacked-structure-geometry.md index 53eb424..d9088c6 100644 --- a/docs/decisions/stacked-structure-geometry.md +++ b/docs/decisions/stacked-structure-geometry.md @@ -11,10 +11,12 @@ SPDX-License-Identifier: MIT OR Apache-2.0 [`engines/ocaml/lib/pipeline.ml`](../../engines/ocaml/lib/pipeline.ml) - Standing: `Unstated` - JLReq: §3.4.2, §3.4.3, §3.7.2, §B.2#13, §C.3 -- Observed by: `just census constructs` (18,515 requests), the warichu and furawake +- Observed by: `just census constructs` (20,102 requests), the warichu and furawake variants; the last two readings below by the third engine's convergence on the same - census, and the two block-axis and in-block readings by its `warichu-full-size` and - `warichu-pair-inside` variants + census, and the block-axis and in-block readings by its `warichu-full-size`, + `warichu-pair-inside`, `warichu-pair-inside-reduced` and `furawake-pair-inside` + variants ([#26](https://github.com/P4suta/jlreq/issues/26), + [#27](https://github.com/P4suta/jlreq/issues/27)) ## The silence @@ -100,6 +102,26 @@ because nothing of that subline stands after it. Neither ladder reaches such a b §3.8.3 and §3.8.4 adjust the spacing of the line, and space inside a block was never the line's to give back or to open. +**The *seam* where two sublines meet is one of those boundaries, and neither ladder reaches +it either.** The character that ends the first subline and the character that begins the +second are adjacent in the text, so Table 1 states an amount between them like anywhere +else — and that amount stands nowhere at all: the one before it reports its body alone and +the one after it begins its own row at the row's own origin. A ladder offered that boundary +divides the line's arrears over a place with nothing to give and recovers only the share it +charged elsewhere, so a line that needed 250 units back gives back 125 and reports itself +overfull. The seam is the block's, by the same sentence the block's interior is: what makes +a boundary the line's is that the line was composed from it, and the line was composed from +neither. + +**A structure's two edges are two different characters, and Table 1 is asked of each at its +own end.** That a structure's own space belongs to the structure says whose the amount is; +it does not say which character the amount is looked up under. The amount before the block +is Table 1's answer at the block's *first* character and the amount after it is Table 1's +answer at its *last*. A note that opens with a closing bracket and ends with an ideograph is +a closing bracket to what precedes it and an ideograph to what follows it, and an +implementation that gathers a structure's clusters into one object of the line has to carry +both classes, because one class cannot answer at both ends. + **§3.4.3's balance is a demerit in the paragraph's own objective, ranked below an overrun and above a short line.** A break inside a warichu carries a cost proportional to how unevenly it divides the note — one 1,000,000-unit demerit per cluster of imbalance, in the @@ -172,6 +194,34 @@ the defect [#24](https://github.com/P4suta/jlreq/issues/24) records. The ladders other half of it: a boundary the line was not composed from is not a boundary the line can adjust, which is why the two of them read the *line's* geometry and stop at the block. +**The seam is that same sentence at the one place two engines read it differently.** It is +the only boundary of a block that is neither strictly interior to a subline nor at the +block's edge, so an implementation can reach it by asking Table 1 about two adjacent +characters without ever asking which object they are adjacent *on* — which is exactly what +[#26](https://github.com/P4suta/jlreq/issues/26) records two of the three reference engines +doing differently. The reading above settles it against the ladder for the reason the +interior boundaries were settled: the test is not whether Table 1 states an amount but +whether the line put one there, and at the seam no engine puts one anywhere. A ladder whose +sites are boundaries Table 1 names rather than boundaries the line was composed from will +divide arrears over sites that cannot pay them, and the line it hands back is over its own +measure by exactly the share those sites were charged. That is not a second reading of +§3.8.3; it is an answer that contradicts its own diagnostic, and the census variant +`warichu-pair-inside-reduced` is what holds it. + +**One object on the line, two characters at its ends.** The reason the two edges are the +same edge twice is that a structure's reported width has to be the width the line was +measured at — an argument about *ownership*, which is silent about the lookup. Table 1 is a +matrix over two occurrences, and the two occurrences at the boundary after a block are the +block's last character and whatever follows it; nothing in §3.4.2 or §3.7.2 suggests the +first character stands in for the last. A warichu never had to state this, because its +members remain occurrences of the line and each is asked at its own position; a furawake +composed as one object does, and an engine that gave that object one class answered the +boundary after it with the class from the other end +([#27](https://github.com/P4suta/jlreq/issues/27)). The two mistakes that follow — an in-row +amount left out of the block and the block's far boundary read at its near class — cancel +in the line's own width at any coordinate where the two amounts happen to be equal, which +is why the shape had to be swept over every class pair before it could be seen at all. + **A figure is an example, and an example cannot outrank the measure.** §3.4.3 shows what a straddling note looks like when it comes out well. Reading its balance as a *requirement* would put a rule about a note's own two lines above the rule that a line of base text fits @@ -211,6 +261,18 @@ to, and both were reached by measurement before anything was written down here ([#23](https://github.com/P4suta/jlreq/issues/23), [#24](https://github.com/P4suta/jlreq/issues/24)). +The same legend settles the seam and the two edges' classes, and would settle them in +either direction. A sentence in §3.8.3 saying what a boundary of a *line* is — or an +Appendix D note saying whether a reduction stage may be offered a coordinate no amount +stands at — decides whether the ladder reaches the place two sublines meet; today the only +argument against it is that the line was not composed from it, which is this project's own +and not the specification's. A note in §3.4.2 or §3.7.2 saying which of a structure's own +characters the amount beside the structure is looked up under decides the other; an +implementation that read both edges at the first character would answer a warichu one way +and a furawake another unless it also stopped composing the furawake as one object, which +is how the disagreement was found ([#26](https://github.com/P4suta/jlreq/issues/26), +[#27](https://github.com/P4suta/jlreq/issues/27)). + For the fifth, a sentence in §3.4.3 that states what its figures show as a rule — how much of a note goes on the first base-text line, and what yields to what when a division that balances the note costs the line something — settles it. Any weighting is publishable, and diff --git a/docs/decisions/warichu-bracket-listing.md b/docs/decisions/warichu-bracket-listing.md index 923058d..2643c3d 100644 --- a/docs/decisions/warichu-bracket-listing.md +++ b/docs/decisions/warichu-bracket-listing.md @@ -11,7 +11,7 @@ SPDX-License-Identifier: MIT OR Apache-2.0 [`engines/racket/classes.rkt`](../../engines/racket/classes.rkt) - Standing: `Unstated` - JLReq: §3.9.2, §3.4.2, §A.01, §A.02, §A.28, §A.29 -- Observed by: `just census constructs` (18,515 requests), the warichu variants +- Observed by: `just census constructs` (20,102 requests), the warichu variants ## The silence diff --git a/engines/ocaml/README.md b/engines/ocaml/README.md index 13918e5..56d631e 100644 --- a/engines/ocaml/README.md +++ b/engines/ocaml/README.md @@ -160,8 +160,8 @@ measuring a coordinate it cannot name. | `vertical` | 5,290 | 529 pairs × upright, rotated, quasi-Japanese, and §3.1.3's two roles, each on a wide line and on a line with room for one cluster: the same tables asked in the other writing mode, and §3.2's orientation of every placement | | `tate-chu-yoko` | 4,761 | 529 pairs standing before and after a run of one, two or three members, two runs side by side, and the same line reduced, justified, justified with a neighbor at half the em, and broken: §3.2.5's geometry and every cl-30 coordinate of Tables 1 through 6 | | `ruby` | 37,030 | 529 pairs on either side of a ruby construct, in seventy variants: mono, group and jukugo; both alignments, both distributions, both jukugo layouts and all four overhang answers; a reading shorter than its base and longer; readings of unequal advance, of a second size and at an em that tiles nothing; the paragraph indent; vertical composition; a justified line, a reduced one and a broken one — §3.3.5 through §3.3.8, §B.2 notes 1, 7, 8, 10 and 11, §C.2 notes 7 and 8, §E.2 notes 6 and 7, and §F | -| `constructs` | 15,870 | 529 pairs beside the other five structures, in thirty variants: emphasis dots of two sizes, a superscript shorter than its complex and longer, a reference mark, a warichu with brackets and without and one that straddles two lines, a furawake of two columns and of three, and a jidori of two characters in four cells and of three in five — vertical, justified and reduced throughout. The cl-20, cl-21, cl-28 and cl-29 rows and columns of all six matrices, §3.3.9, §3.4.2, §3.7.2, §3.7.3, §B.2 notes 9 and 13, §C.2 note 6 and §E.2 note 5 | -| `tabs` | 30,153 | 529 pairs across a tab sign, in fifty-seven variants: §3.6.2's four kinds of stop; stops the line reaches and stops it has gone past; one sign and two; stops listed ascending, descending and in surplus; the sign at the line head, at the line end and with nothing after it; a measure too tight for the stop and one wide enough for two lines; the caller's breaks stated and unstated; every alignment and none; both writing modes; the paragraph indent; the sign beside a construct, inside an emphasis run, a superscript, a jidori and a tate-chu-yoko run, and standing at the first character of a run; and the same two coordinates in the two structures that stack their text off the line — a sign inside a warichu and inside a furawake, a sign that opens either, and a sign of the line standing after one — §3.6.1 through §3.6.3, §3.4.2 and §3.7.2 on what such a structure is on the line, and §3.8.1 on the line a cut leaves short | +| `constructs` | 20,102 | 529 pairs beside the other five structures, in thirty-eight variants: emphasis dots of two sizes, a superscript shorter than its complex and longer, a reference mark, a warichu with brackets and without and one that straddles two lines, a note set at the paragraph's own em rather than at §3.4.2's half one and the same in vertical composition, the pair set *inside* the note — heading one of its rows, standing inside one, on a justified line and on a line that has to give space back — a furawake of two columns and of three and the pair inside one of its columns in both writing modes, and a jidori of two characters in four cells and of three in five — vertical, justified and reduced throughout. The cl-20, cl-21, cl-28 and cl-29 rows and columns of all six matrices, §3.3.9, §3.4.2, §3.7.2, §3.7.3, both ladders at a boundary the line was not composed from, §B.2 notes 9 and 13, §C.2 note 6 and §E.2 note 5 | +| `tabs` | 31,211 | 529 pairs across a tab sign, in fifty-nine variants: §3.6.2's four kinds of stop; stops the line reaches and stops it has gone past; one sign and two; stops listed ascending, descending and in surplus; the sign at the line head, at the line end and with nothing after it; a measure too tight for the stop and one wide enough for two lines; the caller's breaks stated and unstated; every alignment and none; both writing modes; the paragraph indent; the sign beside a construct, inside an emphasis run, a superscript, a jidori and a tate-chu-yoko run, and standing at the first character of a run; and the same two coordinates in the two structures that stack their text off the line — a sign inside a warichu and inside a furawake, a sign inside a note set at the paragraph's own em, a sign that opens either structure, and a sign of the line standing after one — §3.6.1 through §3.6.3, §3.4.2 and §3.7.2 on what such a structure is on the line, and §3.8.1 on the line a cut leaves short | | `widow` | 13,225 | 529 pairs on a paragraph whose last line is one cluster short of the minimum, in twenty-five variants: minima the paragraph can meet and cannot; the pair on the line that gives a cluster up and on the line that gains one; break sets that leave the search one choice and none; both remainder and both preference settings; the indent; vertical composition; and every alignment and none — §3.5.3, §3.5.4 and §3.8.1 | The registry that names them is `kinds` in `census.ml`, and nothing else in the @@ -470,7 +470,7 @@ is what reaches the cl-20, cl-21, cl-28 and cl-29 rows and columns — the last coordinates of the six matrices that no Appendix A key can name. Then a fifth and sixth time across a tab sign and on a paragraph whose last line is a widow, where what varies is not a cell of a matrix but which line the pair ends up on and how far -the line it is on then has to be opened. 116,909 requests in all, and no answer +the line it is on then has to be opened. 122,199 requests in all, and no answer differs by one unit. The two vertical censuses found three things the eighty-nine cases do not reach: diff --git a/engines/ocaml/probe/census.ml b/engines/ocaml/probe/census.ml index da9acef..25c2053 100644 --- a/engines/ocaml/probe/census.ml +++ b/engines/ocaml/probe/census.ml @@ -1741,6 +1741,21 @@ let construct_variants : construct_variant list = let columned body columns gap = around body [ furawake 1 (1 + List.length body) ~columns ~gap ] in + (* The pair set inside one COLUMN of a furawake rather than at the structure's two + edges, which is the §3.7.2 half of what [pair_in_note] asks of §3.4.2. A + furawake's columns are ordinary text of the structure the way a note's rows are, + so the boundary between two members of one column carries Table 1's ordinary + amount and the member before it reports the step that reaches the one beside it. + It reaches the structure's far edge in the same request: the pair shares the + first column and an ordinary character has the second, so the boundary the line + reads after the block is the one at the block's LAST character rather than at + its first (#27, docs/decisions/stacked-structure-geometry.md). §3.7.2 divides at + the caller's own boundary, so the variant states the break that makes the two + columns. *) + let pair_in_column before after = + ( [ plain filler; plain before; plain after; List.nth three_bases 2; plain filler ], + [ furawake 1 4 ~columns:2 ~gap:(em / 5) ] ) + in let celled body cells = around body [ jidori 1 (1 + List.length body) ~cells ] in [ (* §3.3.9. The mark is half its base character, so a base of a second size is the @@ -1888,12 +1903,14 @@ let construct_variants : construct_variant list = cs_name = "warichu-pair-inside-row"; cs_shape = (fun before after -> pair_in_note ~head:1 ~tail:2 before after); }; - (* §3.8.4's ladder at that same boundary: what stands inside a block is no part - of what the line was composed from, so a justified line cannot open it there. - §3.8.3's ladder is the same reading and is deliberately not asked here -- a - line that has to give space back beside a note whose rows meet at a Table 1 - amount is a coordinate the Rust and OCaml engines do not yet agree on, and a - census asks the settled question. *) + (* Both ladders at that same boundary: what stands inside a block is no part of + what the line was composed from, so neither a line with room to spare nor one + that has to give space back reaches it. The reduced shape is where the two + readings come apart in the line's own width. The seam where the note's two + rows meet carries a Table 1 amount that no engine puts anywhere -- the + character ending a row reports its body alone -- so an engine that offered the + seam to §3.8.3 would divide the line's arrears over a boundary with nothing to + give, and hand back half of what it needs (#26). *) { construct_default with cs_name = "warichu-pair-inside-justified"; @@ -1902,6 +1919,12 @@ let construct_variants : construct_variant list = cs_breaks = Mandatory_after 5; cs_alignment = "justify"; }; + { + construct_default with + cs_name = "warichu-pair-inside-reduced"; + cs_shape = (fun before after -> pair_in_note ~head:0 ~tail:1 before after); + cs_extent = 3 * em; + }; (* §3.7.2. The block is centered across the line and its own height is the line's, which a gap and an odd column count make visible at once. *) { @@ -1929,6 +1952,19 @@ let construct_variants : construct_variant list = cs_shape = (fun before after -> columned three_bases 2 0 before after); cs_breaks = Boundaries_before [ 3 ]; }; + { + construct_default with + cs_name = "furawake-pair-inside"; + cs_shape = pair_in_column; + cs_breaks = Boundaries_before [ 3 ]; + }; + { + construct_default with + cs_name = "furawake-pair-inside-vertical"; + cs_shape = pair_in_column; + cs_breaks = Boundaries_before [ 3 ]; + cs_writing_mode = "vertical-rl"; + }; (* §3.7.3. Two characters in four cells, three in five: the first divides its surplus over one boundary and the second over two, and a boundary the specification calls unbreakable takes none of it. *) diff --git a/engines/racket/README.md b/engines/racket/README.md index d0ee629..ff833df 100644 --- a/engines/racket/README.md +++ b/engines/racket/README.md @@ -336,7 +336,7 @@ just census-racket expansion → 3174 request(s), 0 differing response(s) just census-racket vertical → 5290 request(s), 0 differing response(s) just census-racket tate-chu-yoko → 4761 request(s), 0 differing response(s) just census-racket ruby → 37030 request(s), 0 differing response(s) -just census-racket constructs → 18515 request(s), 0 differing response(s) +just census-racket constructs → 20102 request(s), 0 differing response(s) just census-racket tabs → 31211 request(s), 0 differing response(s) just census-racket widow → 13225 request(s), 0 differing response(s) ``` @@ -350,10 +350,10 @@ composition; again with a tate-chu-yoko run standing between them, which is the way to reach the cl-30 row and column of all six matrices at all; again beside a ruby construct in all three kinds, an ornamented complex, an inline cutting note, a furawake and a jidori; again across a tab sign at stops the line reaches and stops it -has passed; and again on a paragraph whose last line is a widow. **120,612 requests, +has passed; and again on a paragraph whose last line is a widow. **122,199 requests, and every one of them the same answer.** -The same 120,612 requests were also diffed against the **OCaml** engine, which reads +The same 122,199 requests were also diffed against the **OCaml** engine, which reads the Japanese transcription and is itself at zero against Rust: three implementations, three readings of §3.9.2 and Appendix B taken from the specification rather than from each other's layout code, and one answer.