diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ddc926..06f6f33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,35 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed +- 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 + [#24](https://github.com/P4suta/jlreq/issues/24). `engines/racket/compose.rkt` reported a + line's `block_extent` as the sum of the note's own subline heights where that was larger + than the paragraph's own block size, so a note set at the paragraph's em rather than at + §3.4.2's half one made the line twice as deep as the paragraph set it; and it reported a + member's advance as its bare body, so a member standing before a Table 1 amount inside + its own subline — `cl-02` then `cl-01`, half of the note's own em — reported 500 where the + same engine placed the next member 750 units on. Both are one reading: + `docs/decisions/stacked-structure-geometry.md` now states what a block does on the block + axis (its sublines run *beside* the line, so their depth is not a depth the line reports) + and what Table 1 does inside one (a note's text is ordinary text, so the boundary between + two characters of one subline carries the ordinary amount, and a member's advance is the + step that reaches the member after it). The same reading closes §3.8.3's half of it: a + boundary the line was not composed from is not one the line may take space back from, so + the reduction ladder no longer reaches inside a block — the expansion ladder already did + not. No public API and no other engine changed. `engines/ocaml/probe/census.ml` reaches + both shapes at every class pair now: five further `constructs` variants + (`warichu-full-size` and its vertical mirror, `warichu-pair-inside`, + `warichu-pair-inside-row` and `warichu-pair-inside-justified`) and two further `tabs` + variants (a sign inside a note set at the paragraph's own em, at a stop the line reaches + and at one it has passed) — so the `constructs` census is 18,515 requests, `tabs` is + 31,211, and all ten censuses are at zero differences across the three engines over + 120,612. A sixth `constructs` variant is deliberately absent: a line that has to *give + space back* beside a note whose two sublines meet at a Table 1 amount is a coordinate the + Rust and OCaml engines do not yet agree on themselves — the Rust engine divides the + line's arrears over that in-block boundary and lays nothing down there, the OCaml engine + does not offer it at all — and a census asks the settled question. - The Racket reference engine now reads §3.6.3's tab round the way `docs/decisions/tab-line-correspondence.md` publishes it, which closes [#19](https://github.com/P4suta/jlreq/issues/19) and lifts the census exclusion the entry diff --git a/ROADMAP.md b/ROADMAP.md index b395411..725f28b 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 116,909 further +whole suite. Ten synthetic censuses agree with the Rust engine across 120,612 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 a1b8f8b..d37c07f 100644 --- a/data/manifest.toml +++ b/data/manifest.toml @@ -11,7 +11,7 @@ [[file]] path = "ROADMAP.md" -sha256 = "e47ffe6ac5c189fa125b463981404643086f40bced7d6713fc85cc773bad3b5e" +sha256 = "d804757056cc7509c09721c86427db5405febca50853971416a14c63035dc47d" [[file]] path = "crates/jlreq-conformance/protocol.schema.json" diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 4a483d5..5ab171e 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, 116,909 requests, and +to zero differences against both of the others on all ten censuses, 120,612 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 30dc315..5417876 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` (30,153 requests) and `just census constructs` (15,870 +- Observed by: `just census tabs` (31,211 requests) and `just census constructs` (18,515 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 a55957d..f385921 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` (15,870 requests), the emphasis and script variants +- Observed by: `just census constructs` (18,515 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 cf360ce..0f360a2 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 116,909 census requests and every one of the eighty-nine conformance +either reading through 120,612 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 8874da8..53eb424 100644 --- a/docs/decisions/stacked-structure-geometry.md +++ b/docs/decisions/stacked-structure-geometry.md @@ -11,9 +11,10 @@ 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` (15,870 requests), the warichu and furawake +- Observed by: `just census constructs` (18,515 requests), the warichu and furawake variants; the last two readings below by the third engine's convergence on the same - census + census, and the two block-axis and in-block readings by its `warichu-full-size` and + `warichu-pair-inside` variants ## The silence @@ -84,6 +85,21 @@ stands before the whole block and is no part of the bracket's reported advance, amount stated in the *neighbor's* em is the neighbor's and stands where the neighbor puts it. A structure has two edges and they are the same edge twice. +**The block a structure makes is not a depth the line reports.** A warichu's sublines run +*beside* the line, so however deep they come to, the line's own block extent is the one the +paragraph set — a note whose members are set at the paragraph's own em rather than at the +half em §3.4.2's own sizing comes to makes a block two ems deep and leaves the line one em +deep. The sublines' own extents are the block's business and the line reports its own. + +**A character *inside* the block is spaced like any other, and its advance is the step that +reaches the next one.** §3.4.2's note is ordinary text of the note, so the boundary between +two characters of one subline carries Table 1's ordinary amount, and the advance the +character before it reports is its own body plus that amount — exactly what it would report +standing beside the line. The character that *ends* a subline reports its body alone, +because nothing of that subline stands after it. Neither ladder reaches such a boundary: +§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. + **§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 @@ -134,6 +150,28 @@ taken from — the same distinction [ruby-overhang-permission](ruby-overhang-per draws for a Table 1 `hang` term — and it is drawn once for the structure rather than twice for its brackets. +**The block axis is the same sentence read across the line instead of along it.** "One +position" is a statement about how much of the line a structure is, and a line has two +extents. If the sublines' own heights summed into the line's, a caller could make a line +taller by writing a longer note into it — and §3.4.2's own subject is a note set *inside* a +line rather than one that pushes the lines apart. The sublines are not lines of their own, +so what they cover is not a height the line has to make room for, exactly as their combined +width is not a width the line advances by. It is the same argument that keeps the closing +bracket's trailing space off the bracket, applied to the axis the section never spells out +separately. + +**Inside the block, there is no structure to be at the edge of.** The two readings above +withdraw a space because the character stands where the *line* stops and the block begins; +between two characters of one subline there is no such edge, and the note is text being set. +Table 1 is what sets text, so it answers there like anywhere else, and an engine that +withheld it would be composing the note by a rule the specification states for nothing. The +advance follows from the same place the line's own advances do: the number an implementation +reports as a character's advance is the number that reaches the character after it, and an +answer whose advances did not add up to its own positions would be two geometries at once — +the defect [#24](https://github.com/P4suta/jlreq/issues/24) records. The ladders are the +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. + **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 @@ -166,6 +204,13 @@ where it belongs, because it is a fact about how the matrix is addressed rather what a warichu is. That same legend settles the fourth reading in the same stroke: the two edges are one question and a note about either of them answers both. +A sentence in §3.4.2 stating how deep a line carrying a note is, or an Appendix B legend +that says whether the matrix is asked between two characters of one subline, settles the two +readings above in the same stroke: both are questions about which object a rule is addressed +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)). + 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/tab-line-correspondence.md b/docs/decisions/tab-line-correspondence.md index fc43dc3..9ddaed7 100644 --- a/docs/decisions/tab-line-correspondence.md +++ b/docs/decisions/tab-line-correspondence.md @@ -13,7 +13,7 @@ SPDX-License-Identifier: MIT OR Apache-2.0 [`engines/racket/compose.rkt`](../../engines/racket/compose.rkt) - Standing: `Unstated` - JLReq: §3.6.1, §3.6.2, §3.6.3, §3.1 (silence), ADR-0018 -- Observed by: `just census tabs` (30,153 requests), and by probing the reference engine +- Observed by: `just census tabs` (31,211 requests), and by probing the reference engine with stops the eighty-nine built-in cases never state; the reading about which constructs hold a coordinate a stop can name by the third engine's convergence on the same census. The two coordinates this file once excluded — a sign that opens a tate-chu-yoko run @@ -215,7 +215,7 @@ tate-chu-yoko run; a sign inside a warichu and inside a furawake; a sign that op those; and a sign of the line standing *after* such a structure, whose own stop has to be measured from a walk in which the whole block is one step. Every one of them at both a stop the line has gone past and one it has not, and all three reference engines answer all of them -alike at every class pair (30,153 requests, no difference). The last four shapes went in when +alike at every class pair (31,211 requests, no difference). The last four shapes went in when the Racket engine reached the reading published here ([#19](https://github.com/P4suta/jlreq/issues/19)); until then they were deliberately left out, because a census is a gate and a gate that is red is not one. diff --git a/docs/decisions/tate-chu-yoko-spacing-sources.md b/docs/decisions/tate-chu-yoko-spacing-sources.md index 6d01686..621f81a 100644 --- a/docs/decisions/tate-chu-yoko-spacing-sources.md +++ b/docs/decisions/tate-chu-yoko-spacing-sources.md @@ -15,7 +15,7 @@ SPDX-License-Identifier: MIT OR Apache-2.0 sentence reaches at all. - JLReq: §3.2.5, §3.6, §B.1, §B.2, §3.8.3, §3.8.4 - Observed by: `just census tate-chu-yoko` (4,761 requests) for the cl-30 coordinates, and - `just census tabs` (30,153 requests) for the cell after a tab sign + `just census tabs` (31,211 requests) for the cell after a tab sign ## The silence diff --git a/docs/decisions/warichu-bracket-listing.md b/docs/decisions/warichu-bracket-listing.md index cefd7e7..923058d 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` (15,870 requests), the warichu variants +- Observed by: `just census constructs` (18,515 requests), the warichu variants ## The silence diff --git a/engines/ocaml/probe/census.ml b/engines/ocaml/probe/census.ml index 72abd9e..da9acef 100644 --- a/engines/ocaml/probe/census.ml +++ b/engines/ocaml/probe/census.ml @@ -1635,6 +1635,17 @@ let note (count : int) : piece list = piece_advance = Some (em / 2); }) +(** The same note set at the paragraph's own em instead. + + §3.4.2's own sizing is what {!note} halves for, and nothing in the protocol makes + it the only note a caller can state. A note set at the paragraph's own size is the + one shape where a block's two axes come apart: the rows sum to twice the depth of + the line that holds them, and the line stays the depth the paragraph set it, + because a block runs beside the line rather than being lines of its own + (docs/decisions/stacked-structure-geometry.md). *) +let full_size_note (count : int) : piece list = + List.map (fun one -> { one with piece_size = None; piece_advance = None }) (note count) + type construct_variant = { cs_name : string; cs_shape : string -> string -> piece list * span list; @@ -1709,6 +1720,24 @@ let construct_variants : construct_variant list = around body [ { span_default with span_kind = "warichu"; span_first = 1; span_last = 1 + List.length body } ] in + (* The pair set INSIDE the note rather than at the structure's two edges, which is + where every variant above stands it. A pair inside the block is asking the other + question §3.4.2 answers: a note's text is ordinary text of the note, so the + boundary between two of its characters carries Table 1's ordinary amount, and the + advance the character before it reports is the step that reaches the one beside + it (docs/decisions/stacked-structure-geometry.md). [head] note characters stand + before the pair and [tail] after it, which is what decides which row the pair + lands on and whether it heads that row; [trailing] adds one more ordinary + character past the structure, so that a justified variant's line under test is + not the paragraph's last. *) + let pair_in_note ?(trailing = false) ~head ~tail before after = + let letters = note (head + tail) in + let ahead = List.filteri (fun index _ -> index < head) letters in + let behind = List.filteri (fun index _ -> index >= head) letters in + let body = ahead @ [ halved (plain before); halved (plain after) ] @ behind in + ( (plain filler :: (body @ [ plain filler ])) @ (if trailing then [ plain filler ] else []), + [ warichu 1 (1 + List.length body) ] ) + in let columned body columns gap = around body [ furawake 1 (1 + List.length body) ~columns ~gap ] in @@ -1832,6 +1861,47 @@ let construct_variants : construct_variant list = cs_extent = 3 * em; cs_breaks = Boundaries_before [ 3; 4; 5; 6 ]; }; + (* A note set at the paragraph's own em rather than at §3.4.2's half one. The + rows then come to twice the depth of the line that holds them, and the line + is as deep as the paragraph set it: a block is one position on the line and + not two lines of it. The two writing modes count the same stack from opposite + ends, so both are asked. *) + { construct_default with cs_name = "warichu-full-size"; + cs_shape = bracketed (full_size_note 4) }; + { + construct_default with + cs_name = "warichu-full-size-vertical"; + cs_shape = bracketed (full_size_note 4); + cs_writing_mode = "vertical-rl"; + }; + (* The pair inside the block instead of beside it: Table 1 between two characters + of one row, and the advance the one before the boundary reports. The pair heads + its row in the first and stands inside one in the second, where the note is five + characters and the balance divides it three and two. *) + { + construct_default with + cs_name = "warichu-pair-inside"; + cs_shape = (fun before after -> pair_in_note ~head:0 ~tail:1 before after); + }; + { + construct_default with + 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. *) + { + construct_default with + cs_name = "warichu-pair-inside-justified"; + cs_shape = (fun before after -> pair_in_note ~trailing:true ~head:0 ~tail:1 before after); + cs_extent = 5 * em; + cs_breaks = Mandatory_after 5; + cs_alignment = "justify"; + }; (* §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. *) { @@ -1975,6 +2045,16 @@ let tab_variants : tab_variant list = let sign_in_note_then_sign before after = [ plain before; note_head; halved tab; note_tail; plain after; tab; plain filler ] in + (* The same structure with its note set at the paragraph's own em instead of at + §3.4.2's half one, where the sign is full-size for the same reason it is halved + above: a character of a note is set at the note's size, whatever else it is. What + that reaches is the sign standing in a block deeper than the line that holds it, + which is the one place where a block's own depth and the line's come apart. *) + let full_head = List.nth (full_size_note 2) 0 in + let full_tail = List.nth (full_size_note 2) 1 in + let sign_in_full_note before after = + [ plain before; full_head; tab; full_tail; plain after ] + in [ (* Found stops, one per §3.6.2 alignment. The stop is past where the text before the sign ends, so the sign moves the rest of the line forward. *) @@ -2218,6 +2298,12 @@ let tab_variants : tab_variant list = { tab_default with tb_name = "warichu-then-a-sign-of-the-line"; tb_pieces = sign_in_note_then_sign; tb_spans = [ warichu 1 4 ]; tb_stops = (fun _ _ -> [ stop (2 * em); stop (5 * em) ]); tb_extent = 7 * em }; + { tab_default with tb_name = "inside-full-size-warichu"; tb_pieces = sign_in_full_note; + tb_spans = [ warichu 1 4 ]; tb_stops = (fun _ _ -> [ stop (em / 2) ]); + tb_extent = 6 * em }; + { tab_default with tb_name = "inside-full-size-warichu-found"; + tb_pieces = sign_in_full_note; tb_spans = [ warichu 1 4 ]; + tb_stops = (fun _ _ -> [ stop (3 * em) ]); tb_extent = 6 * em }; (* §3.7.2 divides a furawake at the caller's own boundaries rather than at one it finds, so a two-column structure states the one break that makes its columns. *) { tab_default with tb_name = "inside-furawake"; tb_pieces = sign_in_note; diff --git a/engines/racket/README.md b/engines/racket/README.md index 5f874cf..d0ee629 100644 --- a/engines/racket/README.md +++ b/engines/racket/README.md @@ -237,6 +237,19 @@ characters stand along the line at positions of their own. Reaching that reading closed [#19](https://github.com/P4suta/jlreq/issues/19), and the `tabs` census now holds all three engines to every one of those shapes. +Two more coordinates were this engine's own, both about the *inside* of a warichu block +and both filed off measurements rather than argument: +[#23](https://github.com/P4suta/jlreq/issues/23), where a note set at the paragraph's own +em made the line report the block's depth instead of its own, and +[#24](https://github.com/P4suta/jlreq/issues/24), where a member standing before a Table 1 +amount inside its own subline reported a bare advance that did not reach the member the +same engine placed next. Both are settled by +[stacked-structure-geometry](../../docs/decisions/stacked-structure-geometry.md), which now +says what a block does on the block axis and what Table 1 does inside one, and the +`constructs` census reaches both shapes at every class pair — `warichu-full-size` and +`warichu-pair-inside`. With them the three engines are at zero differences everywhere any +census asks. + ## Milestones `milestones/M1.ids` through `M9.ids` partition the eighty-nine built-in cases into @@ -310,7 +323,7 @@ has run. `inline_extent` is the second of the two. just racket-milestone 9 → exit 0 (89 cases) just conform-racket → exit 0 (0 DIFF, 0 protocol errors) just racket-gate → exit 0 (CURRENT = 9, so the same run as conform-racket) -just test-engine-racket → 10185 tests passed +just test-engine-racket → 10193 tests passed ``` All ten censuses agree with the Rust engine at every request: @@ -323,8 +336,8 @@ 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 → 15870 request(s), 0 differing response(s) -just census-racket tabs → 30153 request(s), 0 differing response(s) +just census-racket constructs → 18515 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) ``` @@ -337,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. **116,909 requests, +has passed; and again on a paragraph whose last line is a widow. **120,612 requests, and every one of them the same answer.** -The same 116,909 requests were also diffed against the **OCaml** engine, which reads +The same 120,612 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. diff --git a/engines/racket/compose.rkt b/engines/racket/compose.rkt index 109ea59..9ff6ccc 100644 --- a/engines/racket/compose.rkt +++ b/engines/racket/compose.rkt @@ -402,6 +402,11 @@ ;; ---------------------------------------------------------------------------- ;; Where one warichu's own characters stand inside the block they make. +;; +;; `advance` is the advance that character reports: its own body and the space +;; Table 1 states after it inside its own row, which is the step that reaches the +;; character beside it. The one that ends a row reports its body alone, because +;; nothing of that row stands after it (docs/decisions/stacked-structure-geometry.md). (struct spot (run last inline block advance) #:transparent) ;; The runs of warichu text on one line, as `(first . last)` pairs of item indices. @@ -433,16 +438,25 @@ (caddr found) (list 'warichu (cadr found))))) +;; Whether two items stand on the rows of the SAME note, which is what makes the +;; boundary between them the block's own business rather than the line's: the note +;; is one position on the line and the space between two of its characters is +;; inside that position (docs/decisions/stacked-structure-geometry.md). The line is +;; not composed from such a boundary and neither ladder reaches one -- space inside +;; a block is not space the line put there to give back or to open. +(define (same-block? before after) + (and (warichu-of before) (equal? (warichu-of before) (warichu-of after)) #t)) + ;; The layout of every warichu run on one line. ;; -;; Answers a hash from item index to its own place inside the block, and two more -;; from the run's first item to how much of the line the block takes and how much -;; of the block axis it needs. +;; Answers a hash from item index to its own place inside the block, and one more +;; from the run's first item to how much of the line the block takes. How deep the +;; block is is nobody's answer but the block's: the rows run BESIDE the line, so +;; the line is not measured across them (docs/decisions/stacked-structure-geometry.md). (define (warichu-layout para style items first last) (define vertical? (eq? (paragraph-writing-mode para) 'vertical-rl)) (define spots (make-hasheqv)) (define widths (make-hasheqv)) - (define heights (make-hasheqv)) (for ([run (in-list (warichu-runs items first last))]) (define indices (for/list ([index (in-range (car run) (add1 (cdr run)))]) index)) ;; §3.4.2 divides the note "at a position where line breaking is permitted", and @@ -468,20 +482,29 @@ (if (and (word-space? (vector-ref items index)) (or (= rank 0) (= rank (sub1 size)))) 0 (item-advance (vector-ref items index)))) + ;; §3.4.2: a note's own text is ordinary text of the note, set and spaced the + ;; way any run of text is -- so the boundary between two characters of one row + ;; carries Table 1's ordinary answer, exactly as it would beside the line. A + ;; row's last character has nothing of the row after it, and a boundary beside + ;; a collapsed word space is §B.2 note 13's, which took the space out of the + ;; row along with its own advance. + (define (gap-after row rank size) + (define index (list-ref row rank)) + (cond + [(>= (add1 rank) size) 0] + [(or (zero? (own index rank size)) + (zero? (own (list-ref row (add1 rank)) (add1 rank) size))) + 0] + [else + (total-of (boundary-contributions (vector-ref items index) + (vector-ref items (list-ref row (add1 rank))) + (paragraph-writing-mode para) + style))])) (define row-widths (for/list ([row (in-list rows)]) (define size (length row)) - (for/fold ([sum 0]) ([index (in-list row)] [rank (in-naturals)]) - (chk+ (chk+ sum (own index rank size)) - (if (< (add1 rank) size) - (if (or (zero? (own index rank size)) - (zero? (own (list-ref row (add1 rank)) (add1 rank) size))) - 0 - (total-of (boundary-contributions (vector-ref items index) - (vector-ref items (list-ref row (add1 rank))) - (paragraph-writing-mode para) - style))) - 0))))) + (for/fold ([sum 0]) ([rank (in-range size)]) + (chk+ (chk+ sum (own (list-ref row rank) rank size)) (gap-after row rank size))))) (define row-heights (for/list ([row (in-list rows)]) (for/fold ([most 0]) ([index (in-list row)]) @@ -493,25 +516,15 @@ (let walk ([rest row] [rank 0] [at 0]) (unless (null? rest) (define index (car rest)) - (define advance (own index rank size)) + ;; The advance a character of the note reports is the step that reaches + ;; the next one, which is its own body and the space inside the row after + ;; it: an answer whose advances did not add up to its own positions would + ;; be two geometries at once. + (define advance (chk+ (own index rank size) (gap-after row rank size))) (hash-set! spots index (spot (car run) (cdr run) at block advance)) - (walk (cdr rest) - (add1 rank) - (chk+ (chk+ at advance) - (if (< (add1 rank) size) - (if (or (zero? advance) - (zero? (own (list-ref row (add1 rank)) (add1 rank) size))) - 0 - (total-of (boundary-contributions - (vector-ref items index) - (vector-ref items (list-ref row (add1 rank))) - (paragraph-writing-mode para) - style))) - 0)))))) - (hash-set! widths (car run) (for/fold ([most 0]) ([one (in-list row-widths)]) (max most one))) - (hash-set! heights (car run) - (for/fold ([sum 0]) ([one (in-list row-heights)]) (chk+ sum one)))) - (values spots widths heights)) + (walk (cdr rest) (add1 rank) (chk+ at advance))))) + (hash-set! widths (car run) (for/fold ([most 0]) ([one (in-list row-widths)]) (max most one)))) + (values spots widths)) ;; ---------------------------------------------------------------------------- ;; One line, measured @@ -557,7 +570,7 @@ ;; it both gone. The line is composed from the second and the ladders measure ;; their room in the first, which is what lets a collapsed space still give back ;; the width it would have had. - (define-values (spots stack-widths stack-heights) (warichu-layout para style items first last)) + (define-values (spots stack-widths) (warichu-layout para style items first last)) ;; §3.4: a run of warichu text is ONE position on the line however many characters ;; it holds. Its first item carries the block's whole width along the line and the ;; rest carry nothing, which is also what keeps the boundaries inside it out of @@ -575,6 +588,10 @@ (or (stacked offset) (if (collapses-at-edge? one (or (= offset 0) (= offset (sub1 count)))) 0 (item-advance one))))) (define next (and (< (add1 last) (vector-length items)) (vector-ref items (add1 last)))) + ;; Whether the boundary before the item at `index` stands inside one note's own + ;; rows. Both geometries below ask it, because such a boundary is on neither. + (define (inside-one-block? index) + (same-block? (vector-ref items (+ first index -1)) (vector-ref items (+ first index)))) (define gap-terms (for/vector ([index (in-range (add1 count))]) (cond @@ -585,10 +602,7 @@ ;; the note is one position on the main line and the space between two of ;; its characters is inside that position rather than beside it. The ;; boundary where the block BEGINS is on the line, and Table 1 states it. - [(let ([before (hash-ref spots (+ first index -1) #f)] - [after (hash-ref spots (+ first index) #f)]) - (and before after (= (spot-run before) (spot-run after)))) - '()] + [(inside-one-block? index) '()] [else (boundary-contributions (vector-ref items (+ first index -1)) (vector-ref items (+ first index)) @@ -604,6 +618,11 @@ (for/vector ([index (in-range (add1 count))]) (cond [(or (= index 0) (= index count)) (vector-ref gap-terms index)] + ;; A boundary inside a block is the one exception the ladders do not read at + ;; face value: there is no withdrawal here to see past, the space simply + ;; never stood on this line. A ladder that took it back would be spending a + ;; block's own interior on the line's arrears. + [(inside-one-block? index) '()] [else (boundary-contributions (vector-ref items (+ first index -1)) (vector-ref items (+ first index)) @@ -1068,7 +1087,7 @@ ;; The boundaries inside an inline cutting note are inside the block and not ;; on the line, so neither ladder reaches them: the note is one position on ;; the line and the line adjusts around it. - (and (warichu-of before) (equal? (warichu-of before) (warichu-of after))))) + (same-block? before after))) ;; Whether the boundary before offset `index` touches a word space the line edge ;; collapsed. §3.2.2's space is restored the moment the same text sits elsewhere on @@ -1549,7 +1568,7 @@ [else 0])) ;; What the line has to make room for across itself: every item's own block ;; size, and every annotation standing beside one. - (define-values (spots stack-widths stack-heights) + (define-values (spots stack-widths) (warichu-layout para style items first last)) (define block-extent (for/fold ([most 0]) ([offset (in-range (add1 (- last first)))]) @@ -1557,10 +1576,16 @@ (define found (hash-ref spots (+ first offset) #f)) (max most (cond + ;; §3.4.2: the note's rows run BESIDE the line rather than being + ;; lines of their own, and the block they make is one position on + ;; this line -- so however deep the rows come to, the line is as + ;; deep as the paragraph set it and no deeper + ;; (docs/decisions/stacked-structure-geometry.md). A block that + ;; raised the line's own extent would be a structure participating + ;; in the line as more than the one thing it is. [found (if (= (+ first offset) (spot-run found)) - (max (extent-block (paragraph-size para)) - (hash-ref stack-heights (spot-run found))) + (extent-block (paragraph-size para)) 0)] [else (chk+ (block-room one) @@ -1612,14 +1637,15 @@ (define indent (if (= first 0) (paragraph-first-line-indent para) 0)) ;; The designed geometry: the advance a placement reports is the character's own ;; plus the space Table 1 states after it, before either ladder ran. - (define-values (spots stack-widths stack-heights) (warichu-layout para style items first last)) + (define-values (spots stack-widths) (warichu-layout para style items first last)) (define designed (for/vector ([offset (in-range count)]) (define one (vector-ref items (+ first offset))) (define found (hash-ref spots (+ first offset) #f)) (cond - ;; A character of an inline cutting note reports its own advance, which is - ;; what it was set at inside the block; the block's own width is the line's. + ;; A character of an inline cutting note reports what it takes up inside the + ;; block: its own advance and the space Table 1 states after it in its own + ;; row. The block's own width is the line's. [found (spot-advance found)] [else ;; §3.6: a tab sign's advance is what the line gave it and not what it was diff --git a/engines/racket/tests/test-compose.rkt b/engines/racket/tests/test-compose.rkt index 964c31e..176221d 100644 --- a/engines/racket/tests/test-compose.rkt +++ b/engines/racket/tests/test-compose.rkt @@ -387,6 +387,76 @@ ((0 0 1000) (1 1000 500) (2 1500 500) (3 1000 500) (4 2000 500) (5 2500 1000))))) + ;; ------------------------------------------------------------------ + ;; §3.4.2: the block a note makes, and what stands inside it + ;; ------------------------------------------------------------------ + + ;; docs/decisions/stacked-structure-geometry.md: a note's rows run BESIDE the line + ;; rather than being lines of their own, and the block they make is ONE position on + ;; the line. So a note whose members are set at the paragraph's own em rather than + ;; at the half em §3.4.2's own sizing comes to makes a block two ems deep and + ;; leaves the line as deep as the paragraph set it. + ;; + ;; `(abcd)` in an eight-em measure, every character at the paragraph's own 1000, + ;; the warichu over the whole of it, and an opportunity at each of the note's own + ;; boundaries. The note divides where it balances -- `ab` over `cd`, 2000 apiece -- + ;; so the block is 2000 wide and the line is 1000 + 2000 + 1000 = 4000. The two + ;; rows are 1000 deep each and the block they make is centered on the paragraph's + ;; own em: 500 - 1000 puts the first row at -500 and the second at 500. The line's + ;; own block extent is the paragraph's 1000 and not the 2000 the rows sum to. + (define full-size-note + (request "(abcd)" + (list (cluster* 0 1 1000 #:role "warichu-bracket") + (cluster* 1 2 1000) + (cluster* 2 3 1000) + (cluster* 3 4 1000) + (cluster* 4 5 1000) + (cluster* 5 6 1000 #:role "warichu-bracket")) + 8000 + #:breaks (list (break* 2 "allowed") (break* 3 "allowed") (break* 4 "allowed")) + #:constructs (list (hasheq 'kind "warichu" 'range '(0 6))))) + (check-equal? (blocks-of full-size-note) '((0 1000 (0 -500 -500 500 500 0)))) + (check-equal? (layout-of full-size-note) + '(((0 6) 0 4000 + ((0 0 1000) (1 1000 1000) (2 2000 1000) (3 1000 1000) (4 2000 1000) + (5 3000 1000))))) + + ;; The same reading on the inline axis: a boundary INSIDE the block is Table 1's + ;; ordinary answer, and the advance a member reports is the step that reaches the + ;; member beside it. `※〉〈あ※` in a six-em measure, the note's three characters at + ;; the half em §3.4.2 comes to and no opportunity stated, so the balance divides it + ;; after two: `〉〈` is the first row and `あ` the second. Table 1 states 1/2 be + ;; between `cl-02` and `cl-01`, half of the closing bracket's own em, which is 250 + ;; of the note's half em -- so `〉` reports 500 + 250 = 750 and `〈` stands 750 past + ;; it at 1750. `〈` ends its row and reports its body alone. The first row is 1250 + ;; wide and the block with it, which puts the `※` after the note at 2250 and makes + ;; the line 3250. + (define (note-with-a-gap extent) + (request "※〉〈あ※" + (list (cluster* 0 3 1000) + (cluster* 3 6 500 #:size 500) + (cluster* 6 9 500 #:size 500) + (cluster* 9 12 500 #:size 500) + (cluster* 12 15 1000)) + extent + #:alignment "start" + #:constructs (list (hasheq 'kind "warichu" 'range '(3 12))))) + (check-equal? (layout-of (note-with-a-gap 6000)) + '(((0 15) 0 3250 + ((0 0 1000) (1 1000 750) (2 1750 500) (3 1000 500) (4 2250 1000))))) + + ;; And §3.8.3's ladder at that same boundary. A line gives space back at the + ;; boundaries it was composed from, and one inside the block is not among them: the + ;; note is one position on the line, and the 250 units between `〉` and `〈` are + ;; inside that position. The same paragraph in a three-em measure is 250 units too + ;; wide and has nowhere else to find them -- every other boundary of it is solid -- + ;; so the line overruns with its geometry unchanged rather than squeezing the note's + ;; own interior, which is a width the block already reported to the line. + (check-equal? (layout-of (note-with-a-gap 3000)) + '(((0 15) 0 3250 + ((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))) + ;; ------------------------------------------------------------------ ;; Appendix C: what may be broken ;; ------------------------------------------------------------------