3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +01:00

Answer prose round 1: 0 🔴, 7 🟡 (0 misread); state the sweep's rule as line equality with the reference, and reword its comments

This commit is contained in:
2026-10-31 01:18:45 +01:00
committed by Lillem4n
parent 42fc6d97fb
commit 085ad7f508
5 changed files with 53 additions and 45 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ In `docs/decisions.md`:
- Nothing spreads an unbounded array
- A retry loop checks its own termination
- After 32 passes, a line carries or drops every em, strike and strong mark
- Beside the first misread emphasis run, a pass takes back only runs one run per pass would take back too
- With the first misread emphasis run, `unspellableRuns` returns only runs that, taken back early, leave the line as the one-run-per-pass reference writes it
- Readers scan by index
- The spelling memo
- Cost fixes are measured, never timed
+40 -32
View File
@@ -920,47 +920,55 @@ and a line repeating such a pattern 1000 times took 33 seconds lossless and 39 p
passes, both writers fall back on every em, strike and strong run at once. Such a line keeps fewer
marks than one strong per pass would leave, and takes time linear in its length. 32 clears the
most passes any line in the suite needs, 22 portable and 5 lossless, with room to spare. The
properties' reference, which takes back one run per pass, takes no cap.
properties' reference, which falls back on only the first misread emphasis run each pass, takes no
cap.
## Beside the first misread emphasis run, a pass takes back only runs one run per pass would take back too
## With the first misread emphasis run, `unspellableRuns` returns only runs that, taken back early, leave the line as the one-run-per-pass reference writes it
2026-10-29, the maintainer. Goals 9 and 6. Valid while a writer finds a misread run only by reading
the line back.
Taking back one misread run per pass costs a pass per run. So with the first misread run, a pass
takes back every run `unspellableRuns` proves one run per pass would take back later. The proof is
sound, not complete: a run it misses costs one more pass, but a run it wrongly takes back loses a
spelling the line could keep. The properties beside `inline-line.ts` and `inline-reduction.ts`
check that every line comes out as one run per pass writes it.
A writer takes a run back by carrying its nodes, lossless, or by dropping its mark, portable. The
one-run-per-pass reference takes back only the first misread emphasis run each read-back pass, so
it spends a pass per run. To spend fewer, `unspellableRuns` returns, with the first misread run,
every run it proves can go back early and still leave the line as the reference writes it. A run
the proof misses costs one more pass. The properties beside `inline-line.ts` and
`inline-reduction.ts` define the reference, and check that every line the 32-pass cap (§After 32
passes) doesn't reach comes out as the reference writes it.
The sweep reads each run as a pair, its opener and its closer, and orders the pairs by where they
close. A pair falls with the first misread pair where both hold:
`unspellableRuns` reads each emphasis run as a pair, its opener and its closer, and orders the
pairs by where they close. Its sweep judges the pairs closing after the first misread pair, nearest
first, then those closing before it, nearest first, and each verdict reads the verdicts made before
it. A judged pair is settled to fall, and goes back with the first, where both hold:
- **It falls cleanly.** Under a drop, no run merges at its ends. Under a carry, it holds no pair
closing before it over fewer nodes, whose carry would split it.
- **It cannot read, however it is written.** CommonMark judges a delimiter by the characters either
side of its whole delimiter run. Either the opener cannot open, or the closer cannot close and the
pair holds no pair, other than those settled to fall, that may be written with its character. An
opener that may open can pair with such a delimiter inside, and one run per pass would take that
inner pair back first.
- **It falls cleanly.** Under a drop, no drop may merge runs at either of its ends, so no delimiter
there moves. Under a carry, it holds no pair closing before it over fewer nodes, which the
reference may carry first, splitting it.
- **It cannot read, whichever character it is written with.** Either its opener cannot open, or
both of these hold: its closer cannot close, and it holds no pair, other than those settled to
fall, that may be written with the same character. An opener that may open can pair with such an
inner pair's closer. The reference may then take the inner pair back first, while taking the
judged pair back early may let the inner pair read as written.
Em has no fixed character: `resolveEmphasis` writes `_`, and `*` beside a word character. So a
verdict tries every character the pairs beside it leave possible. It reads each other pair as that
pair stands when one run per pass reaches the judged one:
Em has no fixed character: `resolveEmphasis` writes `_`, and `*` beside a word character.
CommonMark judges a delimiter by the characters either side of its whole delimiter run. So whether
a pair can read depends on the pairs whose delimiters touch its own, and a verdict tries every
character they leave possible. A verdict reads each such pair as that pair stands when the
reference reaches the judged pair. `pairStanding` returns the first of these cases that fits:
- **A pair whose ends a drop may merge proves nothing**, since the merge may move its delimiters.
- **Past a judged pair closing after the first, every pair stands.** One run per pass takes back
the first misread pair in closing order. While a pair settled to fall before the judged one
stands, it or one before it is misread, and once they are gone the judged pair is. So no pair
closing after the judged one goes first.
- **Under a carry, a pair over the judged pair's nodes stands**, carried with it. Any other proves
nothing, since a carry may split it.
- **Under a drop, a pair settled to fall is gone.** One run per pass takes it back, so the judged
pair need only fail to read once every such pair is gone. Any other pair may stand or be gone,
and the verdict must hold both ways.
Each verdict reads the verdicts settled before it, so the sweep settles forward from the first
pair, then back.
- **`stands`: the judged pair itself.**
- **`moves`: a pair at whose ends a drop may merge runs.** The merge may move its delimiters, so
the verdict can't rely on it.
- **`stands`: a pair closing after the judged one, where the judged one closes after the first.**
The reference takes back the first misread pair in closing order. Each pair settled to fall that
closes before the judged one is misread once those settled before it are gone, and the judged
pair is misread once all of them are gone. So the reference reaches the judged pair before any
pair closing after it.
- **Under a carry, `stands` for a pair over exactly the judged pair's nodes, else `moves`.** One
carry takes both back at once, and any other may be carried or split first.
- **Under a drop, `fallen` for a pair settled to fall, else `unsettled`.** The reference takes back
every pair settled to fall, so the judged pair need only fail to read once they are gone. An
`unsettled` pair may stand or have fallen, so the verdict must hold both ways.
## Readers scan by index
+1 -1
View File
@@ -31,7 +31,7 @@ test('spells a line alike taking every fallback a pass finds or the first alone'
)
})
// Takes the first fallback a pass finds alone, and no cap.
// Takes only the first fallback each pass finds, with no cap.
function firstFallbackLine(nodes: readonly AdfNode[], container: LineContainer): Result<string> {
const fallbacks = withheldMarkFallbacks(nodes, 'lossless')
for (;;) {
+10 -10
View File
@@ -17,7 +17,7 @@ type PairSweep = {
// 1 where the pair is settled to fall.
readonly fallen: Uint8Array
readonly first: number
// The node index at each end of a run whose drop is not alone.
// The bounds of every run whose drop may merge runs: its first node's index and the index past its last.
readonly mergingEnds: ReadonlySet<number>
readonly output: string
// The pair each placed segment delimits, -1 where it delimits none.
@@ -26,10 +26,10 @@ type PairSweep = {
readonly placed: readonly PlacedSegment[]
}
// How another pair stands when the judged one falls: `moves` where a carry or a merge may move its delimiters, `unsettled` where it may stand or be gone.
// How another pair stands when the judged one falls: `moves` where a carry or a merge may move its delimiters, `unsettled` where it may stand or have fallen.
type Standing = 'fallen' | 'moves' | 'stands' | 'unsettled'
// The runs this pass takes back: the first emphasis run the line misreads and every run that falls with it, else every misflanked highlight run. docs/decisions.md §Beside the first misread emphasis run, a pass takes back only runs one run per pass would take back too.
// The runs this pass takes back: the first emphasis run the line misreads and every run that falls with it, else every misflanked highlight run. docs/decisions.md §With the first misread emphasis run, `unspellableRuns` returns only runs that, taken back early, leave the line as the one-run-per-pass reference writes it.
export function unspellableRuns(placed: readonly PlacedSegment[], reading: InlineReading, output: string, fallback: RunFallback): MarkRun[] {
const { pairAt, pairs } = emphasisPairs(placed)
const first = pairs.findIndex((pair) => !readAsWritten(reading, placed, pair))
@@ -37,7 +37,7 @@ export function unspellableRuns(placed: readonly PlacedSegment[], reading: Inlin
if (firstPair === undefined) return misflankedHighlights(placed, output)
const sweep: PairSweep = { fallback, fallen: new Uint8Array(pairs.length), first, mergingEnds: mergingEnds(pairs, fallback), output, pairAt, pairs, placed }
sweep.fallen[first] = 1
// Each verdict reads the verdicts settled before it, so the sweep settles outward from the first pair.
// Each verdict reads the verdicts settled before it, so the sweep settles forward from the first pair, then back.
for (let judged = first + 1; judged < pairs.length; judged += 1) if (fallsWithFirst(sweep, judged)) sweep.fallen[judged] = 1
for (let judged = first - 1; judged >= 0; judged -= 1) if (fallsWithFirst(sweep, judged)) sweep.fallen[judged] = 1
const runs = [firstPair.nodes]
@@ -98,14 +98,14 @@ function endsAlone(edge: AdfNode | undefined, beside: AdfNode | undefined, depth
.every((mark) => !besideTypes.has(mark.type))
}
// A pair falls with the first only where one run per pass would take it back too: it falls cleanly, and cannot read however it is written.
// A pair falls with the first only where that leaves the line as the one-run-per-pass reference writes it: it falls cleanly, and cannot read whichever character it is written with.
function fallsWithFirst(sweep: PairSweep, judged: number): boolean {
if (!fallsCleanly(sweep, judged)) return false
const spellings = pairSpellings(sweep, judged, judged)
return spellings !== undefined && spellings.every((character) => cannotRead(sweep, judged, character))
}
// A carry splits every pair holding it, so a pair falls cleanly under a carry only where it holds no pair spanning fewer nodes, and under a drop only where no run merges at its ends.
// A carry splits every pair holding it, so a pair falls cleanly under a carry only where it holds no pair spanning fewer nodes, and under a drop only where no drop may merge runs at its ends.
function fallsCleanly({ fallback, mergingEnds, pairs }: PairSweep, judged: number): boolean {
const pair = pairs[judged]
if (pair === undefined) return false
@@ -116,7 +116,7 @@ function mergesAt(mergingEnds: ReadonlySet<number>, { nodes }: EmphasisPair): bo
return mergingEnds.has(nodes.first) || mergingEnds.has(nodes.last + 1)
}
// An opener that may open can pair with a delimiter of its own character inside, so one run per pass may take back a pair standing there first.
// An opener that may open can pair with a delimiter of its own character inside, so the one-run-per-pass reference may take back a pair standing there first.
function cannotRead(sweep: PairSweep, judged: number, character: string): boolean {
if (!mayFlank(sweep, judged, character, 'opener')) return true
return !mayFlank(sweep, judged, character, 'closer') && !holdsAlike(sweep, judged, character)
@@ -187,7 +187,7 @@ function wordBeside(sweep: PairSweep, judged: number, from: number, step: -1 | 1
return found
}
// The delimiters touching the one at `from`, nearest first, and the segment holding text past them; an empty segment touches both sides.
// The delimiters touching the one at `from`, nearest first, and the segment holding text past them; an empty segment between two delimiters leaves them touching.
function touching({ pairAt, placed }: PairSweep, from: number, step: -1 | 1): { delimiters: number[]; next: number } {
const delimiters: number[] = []
let next = from + step
@@ -205,7 +205,7 @@ function delimiterEdge(placed: readonly PlacedSegment[], at: number, step: -1 |
return step === 1 ? delimiter.start + delimiter.segment.text.length : delimiter.start
}
// How the other pair stands when the judged one falls; docs/decisions.md §Beside the first misread emphasis run, a pass takes back only runs one run per pass would take back too derives each case.
// Derived case by case in docs/decisions.md §With the first misread emphasis run, `unspellableRuns` returns only runs that, taken back early, leave the line as the one-run-per-pass reference writes it.
function pairStanding({ fallback, fallen, first, mergingEnds, pairs }: PairSweep, judged: number, other: number): Standing {
const pair = pairs[other]
if (other === judged) return 'stands'
@@ -220,7 +220,7 @@ function pairSpellings(sweep: PairSweep, judged: number, other: number): string[
const pair = sweep.pairs[other]
if (pair === undefined) return undefined
if (pair.delimiter.length > 1) return [pair.delimiter.charAt(0)]
// The carry's punctuation stands where the split pair meets it.
// A carry of a pair it holds may split it first, and a part meeting the carry's punctuation may take either character.
if (sweep.fallback.kind === 'carry' && other !== judged && holdsPairBefore(sweep.pairs, judged, pair)) return ['*', '_']
const before = wordBeside(sweep, judged, pair.opener, -1)
const after = wordBeside(sweep, judged, pair.closer, 1)
@@ -36,7 +36,7 @@ test('reduces a line alike dropping every run a pass may or the first alone', ()
)
})
// Drops the first run a pass finds alone, with every misflanked highlight, and takes no cap.
// Drops only the first run each pass finds, with every misflanked highlight, and takes no cap.
function firstRunLine(nodes: readonly AdfNode[], container: LineContainer): Result<AdfNode[]> {
const leaves = portableLeaves(nodes, container, [], 0)
if (!leaves.ok) return leaves