From 5ed5cb048433eb0a57b9d9fab7a5b07408bbaa61 Mon Sep 17 00:00:00 2001 From: Oleg Kamlowski Date: Mon, 19 Oct 2026 19:46:39 +0200 Subject: [PATCH] docs: clarify comments for readers new to the codebase Rewrite JSDoc that only made sense with knowledge of the earlier slice-based design (parser, memoize, many, run, label, grammar, choice), fix the wrong associativity note on chainLeft and the circular one on inner, and drop a stale output-format comment in grammar.test.ts. README.md and doc/api are regenerated from the JSDoc. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- doc/api/combinators.md | 8 ++++---- doc/api/core.md | 8 ++++---- src/combinators/chainLeft.ts | 2 +- src/combinators/choice.ts | 6 +++--- src/combinators/inner.ts | 2 +- src/combinators/many.ts | 5 +++-- src/core/grammar.test.ts | 2 +- src/core/grammar.ts | 5 +++-- src/core/label.ts | 8 ++++---- src/core/memoize.ts | 8 ++++---- src/core/parser.ts | 7 ++++--- src/core/run.ts | 8 ++++---- 13 files changed, 38 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index fea4479..bf0b1c9 100644 --- a/README.md +++ b/README.md @@ -474,7 +474,7 @@ Take parsers, return a new parser. **64** exports — [full reference →][api-c | [`bind`][combinators-bind] | Chain parsers where the second parser depends on the first result. | | [`braced`][combinators-braced] | Parse content surrounded by braces. | | [`bracketed`][combinators-bracketed] | Parse content surrounded by brackets. | -| [`chainLeft`][combinators-chainleft] | Chain left-associative operations (right-to-left for same precedence). | +| [`chainLeft`][combinators-chainleft] | Chain left-associative operations: `1-2-3` is evaluated as `(1-2)-3`. | | [`chainLeft1`][combinators-chainleft1] | Chain left-associative operations (fails on empty input). | | [`chainRight`][combinators-chainright] | Chain right-associative operations. | | [`chainRight1`][combinators-chainright1] | Chain right-associative operations (fails on empty input). | @@ -492,7 +492,7 @@ Take parsers, return a new parser. **64** exports — [full reference →][api-c | [`foldRight1`][combinators-foldright1] | Fold one or more occurrences from the right into a single value. | | [`fuse`][combinators-fuse] | Fuse multiple string parsers into a single one. | | [`guard`][combinators-guard] | Conditionally apply parser based on a condition. | -| [`inner`][combinators-inner] | Extract inner value from surrounded content (like inner of braced). | +| [`inner`][combinators-inner] | Parse three parsers in sequence and keep only the middle result. | | [`interleaved`][combinators-interleaved] | Parse items separated by separators, keeping both in the result. | | [`last`][combinators-last] | Extract the last element from a parser result array. | | [`left`][combinators-left] | Keep only the left result from a sequence. | diff --git a/doc/api/combinators.md b/doc/api/combinators.md index 3f73864..c00fecb 100644 --- a/doc/api/combinators.md +++ b/doc/api/combinators.md @@ -42,7 +42,7 @@ bracketed(string('hi'))('[hi]'); // { ok: true, value: 'hi', index: 4 } ### `chainLeft` -Chain left-associative operations (right-to-left for same precedence). +Chain left-associative operations: `1-2-3` is evaluated as `(1-2)-3`. ```typescript chainLeft(digits, operation)('1+2+3'); // { ok: true, value: 6, index: 5 } @@ -75,7 +75,7 @@ chainRight1(digits, operation)('1+2+3'); // { ok: true, value: 6, index: 5 } ### `choice` -Try each parser in order, return first success. Alternatives all start from the same offset, so the expectations of those that failed there are unioned into the result. Anything they recorded deeper in the input is already in the parse context. +Try each parser in order, return first success. If all alternatives fail, the failure lists what each one expected at the starting offset. Failures further into the input are already recorded in the parse context. ```typescript choice(string('hello'), string('world'))('hello'); // { ok: true, value: 'hello', index: 5 } @@ -191,7 +191,7 @@ guard(false, string('hello'))('hello'); // { ok: false, index: 0, expected: [] } ### `inner` -Extract inner value from surrounded content (like inner of braced). +Parse three parsers in sequence and keep only the middle result. ```typescript inner(char('('), string('hi'), char(')'))('(hi)'); // { ok: true, value: 'hi', index: 4 } @@ -231,7 +231,7 @@ lexeme(string('hello'))('hello world'); // { ok: true, value: 'hello', index: ### `many` -Zero or more occurrences (never fails). The failure that stopped the loop is kept as a trace, so a later error can still report what the repetition was expecting next. +Zero or more occurrences (never fails). The failure that ends the repetition is still recorded in the parse context, so if parsing fails later, the error message can list what would have continued the repetition. ```typescript many(char('a'))('aaa'); // { ok: true, value: ['a', 'a', 'a'], index: 3 } diff --git a/doc/api/core.md b/doc/api/core.md index d5eebce..df23a95 100644 --- a/doc/api/core.md +++ b/doc/api/core.md @@ -26,7 +26,7 @@ context(); // { furthest: -1, expected: [] } ### `create` -Creates a parser from a parser function. A parser reads `input` from `index` and never slices it. The third argument is the parse context — pass it on to any parser you call, so expectations recorded inside your parser reach the final error message. +Creates a parser from a parser function. A parser receives the full `input` and the `index` to start reading at, and returns the index where it stopped. The third argument is the parse context — pass it on to any parser you call, so expectations recorded inside your parser reach the final error message. ```typescript create((input, index = 0) => success('parsed', index + 6))('hello world'); // { ok: true, value: 'parsed', index: 6 } @@ -50,7 +50,7 @@ format('a=1,b=x', 6, ['digit']); // "1:7 expected digit, found 'x'\n 1 | a=1,b= ### `grammar` -Creates a recursive grammar where rules can reference each other. Rules are resolved lazily through a shared record, so they may refer to one another by name without forward declarations. Left-recursive rules are reported by name rather than overflowing the stack. +Creates a recursive grammar where rules can reference each other. Each rule receives an object holding all rules of the grammar and looks them up only when it runs, so rules may refer to one another by name without forward declarations. Left-recursive rules are reported by name rather than overflowing the stack. ```typescript type Math = { @@ -78,7 +78,7 @@ run(g.expr, '(1+2)'); // 3 ### `label` -Replaces the expectations of a parser with a single description. Only applies when the parser failed without consuming input: once a branch has committed, its own deeper error is more useful than the label. The labelled parser runs against an isolated context so that its internals can be suppressed rather than leaking into the message. +Replaces the expectations of a parser with a single description. Only applies when the parser failed without consuming input: once it has consumed input, its own error further along is more useful than the label. The labelled parser runs against a separate context, so the expectations of the parsers inside it do not leak into the message. ```typescript label(char('x'), 'letter x')(''); // { ok: false, index: 0, expected: ['letter x'] } @@ -110,7 +110,7 @@ match(success('hello', 5), { success: (v) => v, failure: () => 'failed' }); // ' ### `memoize` -Memoizes a parser so each offset is parsed at most once (packrat caching). Keyed by offset rather than by the remaining input, so entries are cheap and the cache is dropped as soon as a different input is parsed. Each entry also remembers what the parse expected, so a cache hit still contributes to the error message rather than silently weakening it. +Memoizes a parser so each offset is parsed at most once (packrat caching). The cache is keyed by offset and cleared whenever the parser is called with a different input string. Each entry also stores what the parse expected, so a cache hit contributes the same expectations to the error message as a fresh parse would. ```typescript const memoDigits = memoize(digits); diff --git a/src/combinators/chainLeft.ts b/src/combinators/chainLeft.ts index 08cdda7..2317fb2 100644 --- a/src/combinators/chainLeft.ts +++ b/src/combinators/chainLeft.ts @@ -6,7 +6,7 @@ import { chainLeft1 } from './chainLeft1'; import { optional } from './optional'; /** - * Chain left-associative operations (right-to-left for same precedence). + * Chain left-associative operations: `1-2-3` is evaluated as `(1-2)-3`. * * @example * chainLeft(digits, operation)('1+2+3') // { ok: true, value: 6, index: 5 } diff --git a/src/combinators/choice.ts b/src/combinators/choice.ts index 24822b9..5ef0224 100644 --- a/src/combinators/choice.ts +++ b/src/combinators/choice.ts @@ -11,9 +11,9 @@ const NONE: readonly string[] = []; /** * Try each parser in order, return first success. * - * Alternatives all start from the same offset, so the expectations of those - * that failed there are unioned into the result. Anything they recorded deeper - * in the input is already in the parse context. + * If all alternatives fail, the failure lists what each one expected at the + * starting offset. Failures further into the input are already recorded in the + * parse context. * * @example * choice(string('hello'), string('world'))('hello') // { ok: true, value: 'hello', index: 5 } diff --git a/src/combinators/inner.ts b/src/combinators/inner.ts index 2a7bed8..ede55f1 100644 --- a/src/combinators/inner.ts +++ b/src/combinators/inner.ts @@ -5,7 +5,7 @@ import { map } from './map'; import { sequence } from './sequence'; /** - * Extract inner value from surrounded content (like inner of braced). + * Parse three parsers in sequence and keep only the middle result. * * @example * inner(char('('), string('hi'), char(')'))('(hi)') // { ok: true, value: 'hi', index: 4 } diff --git a/src/combinators/many.ts b/src/combinators/many.ts index 525680b..c239f8e 100644 --- a/src/combinators/many.ts +++ b/src/combinators/many.ts @@ -5,8 +5,9 @@ import { success, type Success } from '../core/success'; /** * Zero or more occurrences (never fails). * - * The failure that stopped the loop is kept as a trace, so a later error can - * still report what the repetition was expecting next. + * The failure that ends the repetition is still recorded in the parse context, + * so if parsing fails later, the error message can list what would have + * continued the repetition. * * @example * many(char('a'))('aaa') // { ok: true, value: ['a', 'a', 'a'], index: 3 } diff --git a/src/core/grammar.test.ts b/src/core/grammar.test.ts index 04c0959..eb3b35d 100644 --- a/src/core/grammar.test.ts +++ b/src/core/grammar.test.ts @@ -88,7 +88,7 @@ describe('grammar', () => { } { - const result = expr('1+(2+3)'); // [6, ''] + const result = expr('1+(2+3)'); assertSuccess(result, 6, 7); } }); diff --git a/src/core/grammar.ts b/src/core/grammar.ts index 8be3801..a564719 100644 --- a/src/core/grammar.ts +++ b/src/core/grammar.ts @@ -35,8 +35,9 @@ const guardLeftRecursion = ( /** * Creates a recursive grammar where rules can reference each other. * - * Rules are resolved lazily through a shared record, so they may refer to one - * another by name without forward declarations. Left-recursive rules are + * Each rule receives an object holding all rules of the grammar and looks them + * up only when it runs, so rules may refer to one another by name without + * forward declarations. Left-recursive rules are * reported by name rather than overflowing the stack. * * @example diff --git a/src/core/label.ts b/src/core/label.ts index 5f38299..41f1442 100644 --- a/src/core/label.ts +++ b/src/core/label.ts @@ -6,10 +6,10 @@ import { create } from './parser'; /** * Replaces the expectations of a parser with a single description. * - * Only applies when the parser failed without consuming input: once a branch - * has committed, its own deeper error is more useful than the label. The - * labelled parser runs against an isolated context so that its internals can - * be suppressed rather than leaking into the message. + * Only applies when the parser failed without consuming input: once it has + * consumed input, its own error further along is more useful than the label. + * The labelled parser runs against a separate context, so the expectations of + * the parsers inside it do not leak into the message. * * @example * label(char('x'), 'letter x')('') // { ok: false, index: 0, expected: ['letter x'] } diff --git a/src/core/memoize.ts b/src/core/memoize.ts index 6590fb9..3de5b9e 100644 --- a/src/core/memoize.ts +++ b/src/core/memoize.ts @@ -11,10 +11,10 @@ type Entry = { /** * Memoizes a parser so each offset is parsed at most once (packrat caching). * - * Keyed by offset rather than by the remaining input, so entries are cheap and - * the cache is dropped as soon as a different input is parsed. Each entry also - * remembers what the parse expected, so a cache hit still contributes to the - * error message rather than silently weakening it. + * The cache is keyed by offset and cleared whenever the parser is called with a + * different input string. Each entry also stores what the parse expected, so a + * cache hit contributes the same expectations to the error message as a fresh + * parse would. * * @example * const memoDigits = memoize(digits); diff --git a/src/core/parser.ts b/src/core/parser.ts index 6104146..4e63a6f 100644 --- a/src/core/parser.ts +++ b/src/core/parser.ts @@ -10,9 +10,10 @@ export type Parser = ( /** * Creates a parser from a parser function. * - * A parser reads `input` from `index` and never slices it. The third argument - * is the parse context — pass it on to any parser you call, so expectations - * recorded inside your parser reach the final error message. + * A parser receives the full `input` and the `index` to start reading at, and + * returns the index where it stopped. The third argument is the parse + * context — pass it on to any parser you call, so expectations recorded + * inside your parser reach the final error message. * * @example * create((input, index = 0) => success('parsed', index + 6))('hello world') // { ok: true, value: 'parsed', index: 6 } diff --git a/src/core/run.ts b/src/core/run.ts index 8d4c692..b4b55d2 100644 --- a/src/core/run.ts +++ b/src/core/run.ts @@ -22,10 +22,10 @@ const merge = ( * A parser can succeed without consuming everything — the interesting position * is then the furthest offset any branch reached, not where the parse stopped. * - * A result reaching the same offset as the context carries no more authority - * than the context does, so the two expectation sets are merged. Combinators - * that fail without an expectation of their own — `chainLeft`, `guard`, a - * hand-written `failure(ctx, index)` — would otherwise report nothing at all. + * When the result and the context point at the same offset, their expectations + * are merged. Otherwise combinators that fail without an expectation of their + * own — `chainLeft`, `guard`, a hand-written `failure(ctx, index)` — would + * report nothing at all. */ export const diagnose = ( input: string,