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:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 (;;) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user