mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer architecture round 2: 1 🔴, 1 🟡; the CommonMark ask answers yes or no, the memo's callers name themselves, the bound's policy reads at its constant, Firefox's bare out-of-memory string is engine-limit, and the floor entry no longer points at item 110
This commit is contained in:
+1
-1
@@ -114,4 +114,4 @@ with one of the four now reads, or refuses with the code the table gives.
|
||||
| `null`, `undefined` or a non-empty array, on parse | throws a `TypeError` | `not-a-string` |
|
||||
| a `String` object, on parse | the document its text spells | `not-a-string`; pass `String(value)` |
|
||||
| any other value that is not a string, such as a number or `{}`, on parse | an empty document | `not-a-string` |
|
||||
| a conversion past the JavaScript engine's longest array, `Map` or `Set`, or finding no memory for a buffer | throws a `RangeError` | `engine-limit`; retry it once |
|
||||
| a conversion past the JavaScript engine's longest array, `Map` or `Set`, or finding no memory for a buffer, and on JavaScriptCore one running its heap short or past its longest string | throws | `engine-limit`; retry it once |
|
||||
|
||||
@@ -711,3 +711,34 @@ Inherited architect, decided by:
|
||||
- Shape, Locality: `src/markdown/emit/line-escaping.ts:229` `escapeClosedRuns`: mutates its input in place and needs a two-line comment arguing it is safe; output joined in batches of 4096
|
||||
- Locality, Shape: `src/markdown/portable/adf-to-portable-markdown.ts:128` `portableSequence`: a refusal propagates with its source path, where it was swallowed
|
||||
- Shape: `src/markdown/emit/inline-line.ts:235` `emitLeaf`: one place refuses content and text, and a generator replaces the boolean-callback JSON writer
|
||||
|
||||
## 2026-10-13T15:35:32Z, PR #37 at 025ccc0, against 7a27f6b
|
||||
|
||||
Ruling: worse
|
||||
|
||||
| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall |
|
||||
|---|---|---|---|---|---|
|
||||
| Mid A | better | worse | same | same | worse |
|
||||
| Inherited architect | better | worse | same | worse | worse |
|
||||
|
||||
Mid A, decided by:
|
||||
|
||||
- Locality: `src/markdown/emit/adf-to-markdown.ts:58` `walkBlocks`: the length bound is enforced at six sites across four files, each comment giving only its own reason, where one wrapper covered the conversion
|
||||
- Locality: `src/markdown/emit/adf-to-markdown.ts:126` `readableBlock`: the memo is emptied on read and filled only when `asked`, so whether a spelling is cached depends on call order and caller
|
||||
- Navigation: `src/markdown/length-bound.ts:1` `withinEngineLimits`: a refused long document leads to a file named for the problem, holding the bound, both throw lists and both messages
|
||||
- Locality: `src/markdown/parse/markdown-to-adf.ts:266` `directiveNode`: `success(undefined)` and `success(null)` encode the ask's answer, decoded only by tracing commonMarkSpelling
|
||||
- Shape: `src/canonical-json.ts:23` `canonicalJsonPieces`: a generator replaces a boolean-continue callback
|
||||
- Shape: `src/markdown/emit/line-escaping.ts:38` `PlacedSegment`: segment placements drop the cursor arithmetic two functions repeated
|
||||
- Shape: `src/markdown/emit/line-escaping.ts:113` `backslashed`: byte flags, codes and 4096-piece joins read harder than a character loop
|
||||
- Locality: `src/markdown/portable/adf-to-portable-markdown.ts:128` `portableSequence`: a refused spelling propagates where it was silently swallowed
|
||||
|
||||
Inherited architect, decided by:
|
||||
|
||||
- Locality: `src/markdown/emit/adf-to-markdown.ts:58` `walkBlocks`: one rule, no markdown past longestMarkdown, is checked at about seven places across four files
|
||||
- Locality, Shape: `src/markdown/emit/adf-to-markdown.ts:126` `readableBlock`: a read deletes the entry and a bare positional `asked` boolean decides what is stored, so results depend on call order
|
||||
- Locality, Self-sufficiency: `src/markdown/emit/line-escaping.ts:113` `backslashed`: flags, byte codes, a lookup closure and batched joins take more passes to read as putting a backslash before characters
|
||||
- Self-sufficiency: `src/markdown/emit/adf-to-markdown.ts:65` `walkBlocks`: the refusal's comment makes sense only once you know V8's limit applies on every engine, which lives in docs/decisions.md; tooLong carries path [] though the walk knows the path
|
||||
- Shape: `src/markdown/length-bound.ts:17` `tooLong`: the length refusal keeps unsupported-node-shape while engine-limit got a name of its own; directiveNode folds three outcomes into one Result to fit the wrapper
|
||||
- Navigation: `src/markdown/length-bound.ts:37` `withinEngineLimits`: the engine guard moves out of the Result module, and engine-limit leads from the symptom to it
|
||||
- Shape, Locality: `src/markdown/portable/adf-to-portable-markdown.ts:128` `portableSequence`: a refusal propagates with its block's path, where it was silently swallowed
|
||||
- Shape: `src/canonical-json.ts:23` `canonicalJsonPieces`: a generator, emitLeaf's one refusal and PlacedSegment each read simpler than before
|
||||
|
||||
+1
-1
@@ -624,7 +624,7 @@ after three rounds under a worse ruling, the maintainer's call: both paired seat
|
||||
as the same, but Locality fell to a mean of 5.13, laid on the spelling memo the chunk splits between
|
||||
the CommonMark ask, which fills it, and the walk, which drops each entry as it reads it. The overall
|
||||
scores at 1f4551a were Junior 5.5, Mid 5.5, Senior 6 and Architect 5.5, mean 5.63.
|
||||
`todo.md` item 110 is to fill and read the memo in one place.
|
||||
Item 108's chunk, 2026-10-13, fills and reads the memo in `readableBlock` alone.
|
||||
|
||||
## Properties on a fixed seed
|
||||
|
||||
|
||||
@@ -108,22 +108,23 @@ function interruptsParagraph(node: AdfNode): boolean {
|
||||
function emitBlock(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<EmittedBlock> {
|
||||
const model = blockNodeModel(node.type)
|
||||
if (model === undefined) return carriedFence(carriedBlock(node, path, depth))
|
||||
const readable = readableBlock(node, path, depth, writing, false)
|
||||
const readable = readableBlock(node, path, depth, writing, 'walked')
|
||||
if (readable !== undefined) return readable
|
||||
return emitDirectiveBlock(node, model, path, depth, () => walkBlocks(nodeContent(node), path, depth + 1, writing))
|
||||
}
|
||||
|
||||
export function commonMarkSpelling(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing & { memo: SpellingMemo }): Result<null> | undefined {
|
||||
const readable = readableBlock(node, path, depth, writing, true)
|
||||
if (readable === undefined) return undefined
|
||||
// Whether the node takes a CommonMark spelling, rather than its directive.
|
||||
export function commonMarkSpelling(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing & { memo: SpellingMemo }): Result<boolean> {
|
||||
const readable = readableBlock(node, path, depth, writing, 'asked')
|
||||
if (readable === undefined) return success(false)
|
||||
if (!readable.ok) return readable
|
||||
const fits = withinLongest(readable.value.text)
|
||||
if (!fits.ok) return fits
|
||||
return readable.value.spelling === 'directive' ? undefined : success(null)
|
||||
return success(readable.value.spelling !== 'directive')
|
||||
}
|
||||
|
||||
// Only an asked node's spelling is kept, and reading it drops it: the walk above spells the node into its own text, so no level of a subtree holds that text twice.
|
||||
function readableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing, asked: boolean): Result<EmittedBlock> | undefined {
|
||||
function readableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing, caller: 'asked' | 'walked'): Result<EmittedBlock> | undefined {
|
||||
const { memo } = writing
|
||||
const kept = memo?.get(node)
|
||||
memo?.delete(node)
|
||||
@@ -133,8 +134,8 @@ function readableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, wri
|
||||
if (depth <= kept.depth) return success({ ...kept.block, headroom: kept.block.headroom + kept.depth - depth })
|
||||
}
|
||||
const spelled = spellReadableBlock(node, path, depth, writing)
|
||||
if (asked && spelled === undefined) memo?.set(node, { block: undefined, depth })
|
||||
if (asked && spelled?.ok === true) memo?.set(node, { block: spelled.value, depth })
|
||||
if (caller === 'asked' && spelled === undefined) memo?.set(node, { block: undefined, depth })
|
||||
if (caller === 'asked' && spelled?.ok === true) memo?.set(node, { block: spelled.value, depth })
|
||||
return spelled
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,12 @@ test('refuses a string past the bound as too long on every engine, and any other
|
||||
assert.equal(code('Map maximum size exceeded'), 'engine-limit')
|
||||
assert.equal(code('Set maximum size exceeded'), 'engine-limit')
|
||||
assert.equal(code('invalid array length'), 'engine-limit')
|
||||
assert.deepEqual(
|
||||
withinEngineLimits(() => {
|
||||
throw 'out of memory'
|
||||
}),
|
||||
{ error: { code: 'engine-limit', message: 'the conversion passed this JavaScript engine\'s longest string or array, or ran it out of memory (out of memory)', path: [] }, ok: false },
|
||||
)
|
||||
assert.deepEqual(
|
||||
withinEngineLimits(() => success('x')),
|
||||
{ ok: true, value: 'x' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { failure, success, type Result } from '../result.ts'
|
||||
|
||||
// V8's longest string (docs/decisions.md §Markdown past V8's longest string is refused on every engine).
|
||||
// V8's longest string, refused on every engine so a document converts alike everywhere (docs/decisions.md §Markdown past V8's longest string is refused on every engine).
|
||||
export const longestMarkdown = 2 ** 29 - 24
|
||||
|
||||
// Measured on Firefox 155, Node 24 and Bun 1.4, and SpiderMonkey's array length from its js.msg. SpiderMonkey's overflow and V8's string overflow both mean a string past longestMarkdown; the others name an engine's own limit or its memory running short.
|
||||
@@ -38,11 +38,17 @@ export function withinEngineLimits<T>(convert: () => Result<T>): Result<T> {
|
||||
try {
|
||||
return convert()
|
||||
} catch (error) {
|
||||
// SpiderMonkey throws a bare string where it finds no memory for a buffer (measured on Firefox 155).
|
||||
if (error === 'out of memory') return engineLimit(error)
|
||||
if (!(error instanceof Error)) throw error
|
||||
const thrown = `${error.name}: ${error.message}`
|
||||
if (pastLongestString.includes(thrown)) return tooLong()
|
||||
// A stack overflow is a bug (docs/decisions.md §Nothing recurses unbounded), so it stays a throw.
|
||||
if (!pastEngineLimit.includes(thrown)) throw error
|
||||
return failure('engine-limit', `the conversion passed this JavaScript engine's longest string or array, or ran it out of memory (${error.message})`, [])
|
||||
return engineLimit(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
function engineLimit<T>(thrown: string): Result<T> {
|
||||
return failure('engine-limit', `the conversion passed this JavaScript engine's longest string or array, or ran it out of memory (${thrown})`, [])
|
||||
}
|
||||
|
||||
@@ -263,9 +263,9 @@ function directiveNode(block: DirectiveBlock, reading: Reading, path: ConvertErr
|
||||
const built = directiveBody(read.value, block.blocks, reading, path, depth)
|
||||
if (!built.ok) return built
|
||||
// Refused here so V8's throw past the bound is placed at this directive, like the bound check on other engines.
|
||||
const readable = withinEngineLimits<null | undefined>(() => commonMarkSpelling(built.value, path, depth, { flavour: 'lossless', memo: reading.memo }) ?? success(undefined))
|
||||
const readable = withinEngineLimits(() => commonMarkSpelling(built.value, path, depth, { flavour: 'lossless', memo: reading.memo }))
|
||||
if (!readable.ok) return readable
|
||||
if (readable.value === undefined) return built
|
||||
if (!readable.value) return built
|
||||
return failure('unsupported-node-shape', `${built.value.type} takes the CommonMark spelling, not the directive form`, path)
|
||||
}
|
||||
|
||||
|
||||
@@ -131,9 +131,9 @@ function portableSequence(blocks: readonly Sourced[], reduction: Reduction): Res
|
||||
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
||||
const block = numberedPastMarkers(next.block)
|
||||
const listed = block.type === 'bulletList' || block.type === 'orderedList'
|
||||
const spelled = listed && block === next.block ? commonMarkSpelling(block, next.path, reduction.depth, { flavour: 'portable', memo: reduction.memo }) : undefined
|
||||
if (spelled !== undefined && !spelled.ok) return spelled
|
||||
if (!listed || spelled !== undefined) {
|
||||
const spelled = listed && block === next.block ? commonMarkSpelling(block, next.path, reduction.depth, { flavour: 'portable', memo: reduction.memo }) : success(!listed)
|
||||
if (!spelled.ok) return spelled
|
||||
if (spelled.value) {
|
||||
sequence.push(block)
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user