Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`.
Expand Down
27 changes: 26 additions & 1 deletion crates/jlreq/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Range<usize>> {
let cluster = paragraph.text.clusters().get(ordinal)?.range();
let range = paragraph.constructs.iter().find_map(|construct| {
Expand Down Expand Up @@ -949,7 +972,9 @@ fn reduction_sites(
) -> Vec<ReductionSite> {
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) {
Expand Down
94 changes: 94 additions & 0 deletions crates/jlreq/tests/public_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(&paragraph, &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<_>>(),
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<_>>(),
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] {
Expand Down
2 changes: 1 addition & 1 deletion data/manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

[[file]]
path = "ROADMAP.md"
sha256 = "d804757056cc7509c09721c86427db5405febca50853971416a14c63035dc47d"
sha256 = "a6b10efed08e330003e172ee7fd9fcf1a8b3b81e5df62c731833df3e685020b6"

[[file]]
path = "crates/jlreq-conformance/protocol.schema.json"
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/jidori-room-and-solid-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/ornamented-complex-geometry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/ruby-overhang-permission.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
68 changes: 65 additions & 3 deletions docs/decisions/stacked-structure-geometry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading