diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62a3a58ec..e76127682 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -498,6 +498,9 @@ { "run": "git add -A && git diff --cached --exit-code" }, + { + "run": "! grep -rnE '^(/\\*\\*.*@typedef|\\s\\* *@typedef)' --include='*.mjs' --exclude-dir=node_modules ." + }, { "run": "npx tsc" }, diff --git a/AGENTS.md b/AGENTS.md index 119fabd00..2ac2a2693 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,7 +80,10 @@ Every new `.f.mjs` module ships a co-located `proof.f.mjs` with **100% proof coverage** — every export called, every line executed, every branch taken. Values are immutable (no in-place mutation, no `.push`/`Map#set`/index assignment), there is no `try`/`catch` and no regular expressions, and types are -written in JSDoc with a sibling `types.ts` for a type-level API. +written in JSDoc with a sibling `types.ts` for a type-level API. No authored +`.mjs` anywhere in the repository — `fjs/` or not — may contain a **file-scope** +JSDoc `@typedef`; function-local typedefs are allowed. Named types live in +`types.ts` (the public declaration closure) or an optional `private.ts`. Testing, documentation, and the full coding style: [fjs/AGENTS.md](./fjs/AGENTS.md). diff --git a/changelog/unreleased/1750.md b/changelog/unreleased/1750.md new file mode 100644 index 000000000..9bc53641c --- /dev/null +++ b/changelog/unreleased/1750.md @@ -0,0 +1,5 @@ +- **BREAKING CHANGES:** type import paths changed: `Grammar` → + `fjs/fsm/types.ts`; `MemoryOperationMap`, `MemoryRun`, `Uuid` → + `fjs/effects/node/memory/types.ts`; `BrowserTestReport` → + `fjs/emergent_testing/types.ts`; the JSON-Schema `Unknown` alias is gone — + spell it `Ts`. diff --git a/fjs/AGENTS.md b/fjs/AGENTS.md index e979e8fdc..e43afc9fe 100644 --- a/fjs/AGENTS.md +++ b/fjs/AGENTS.md @@ -247,15 +247,42 @@ changes. A separately useful type-level API may live in an authored sibling `types.ts`; that file remains TypeScript type source and holds no runtime implementation. -Name implementation-only JSDoc typedefs with a leading `_` -(`/** @typedef {number} _Type */`). Declaration emit cannot strip them yet, so -the underscore — not the emitted `.d.ts` — is what marks a name private, -and renaming or removing a `_`-prefixed alias is not by itself a breaking -change. The public contract still governs transitive effects. See -[Private JSDoc typedefs](./fsc/README.md#private-jsdoc-typedefs) for the -full rule and examples. - -Use `@typedef` for a named type and `@template` for its type parameters. A +No authored `.mjs` may contain a **file-scope** JSDoc `@typedef` — anywhere in +the repository, whatever the directory or basename. Function-local typedefs are +allowed, and are the normal home for compile-time proof types (see the +`consistency` and `signatures` entries in `fjs/edag/proof.f.mjs` and +`fjs/effects/proof.f.mjs`). A named file-scope type goes to one of: + +- the sibling `types.ts` when it is part of the **public declaration closure** — + public types, plus any private `_` helper a shipped public declaration + reaches transitively (e.g. `_Byte` in `fjs/types/byte_set/types.ts`) — or the + type is inlined into the annotation instead; +- an optional sibling `private.ts` for implementation-private types outside the + public closure, when separating them reads cleaner than inlining (e.g. + `fjs/common/monoid/private.ts`, `fjs/rtti/data/private.ts`); do not create it + mechanically for every `_` name; +- nowhere: a short type used once or twice is simply inlined. + +Name private types and private runtime constants with a leading `_`, even when +module linkage requires an export: exportability is linkage, not API status, so +renaming or removing a `_`-prefixed name is not by itself a breaking change. +The public contract still governs transitive effects. See +[Private types](./fsc/README.md#private-types) for the full rule. + +The intra-directory dependency direction is +`types.ts <- private.ts <- module.f.mjs <- proof.f.mjs <- module.mjs <- proof.mjs` +(dependency to dependent; a layering guide, not a requirement that every file +exists). `types.ts` must not depend on `private.ts`, and verification moves +downstream: an assertion that checks the implementation belongs in a proof +function, not in `types.ts`. Recursive RTTI whose annotation needs a named +public type may stay in `module.f.mjs` (e.g. `exp` in `fjs/edag/module.f.mjs`), +and declarative compile-time/runtime constants shared between TypeScript and +runtime code may be split into a normal subordinate metaprogramming module such +as `meta/module.f.mjs` when that helps — it is an ordinary module, discovered +and covered like any other `module.f.mjs`, never a requirement. + +Use `@typedef` (function-local in `.mjs`, or `export type` in `types.ts` / +`private.ts`) for a named type and `@template` for its type parameters. A constraint goes in braces before the parameter name: ```js @@ -435,10 +462,15 @@ inference with an `Assert>` in the proof, per literal, since a primitive would pass with or without it: ```js -const v = validate({ a: 42, b: 'hello' }) -/** @typedef {Assert>>} _ConstParameter */ +constParameter: () => { + const v = validate({ a: 42, b: 'hello' }) + /** @typedef {Assert>>} _ConstParameter */ +}, ``` +The typedef sits inside the proof entry because an authored `.mjs` carries no +file-scope typedef (§3.2). + #### Avoid `as` type assertions Avoid `as` type assertions (except `as const`). Treat them like `unsafe` in Rust @@ -536,10 +568,10 @@ that context on its own: `ToAsyncOperationMap` is a mapped type keyed on back out of the argument. Left to argument inference `O` falls back to its `Operation` constraint — payloads and outputs `never` — which no real map is assignable to, and the call site reaches for exactly the cast this section warns -about. **Annotate the result instead**: pin the runner's own type -(`/** @type {_EffectToPromise} */`, `/** @type {MemoryRun} */`) and `O` is -inferred from the return type, giving the call a real `O` to check its argument -against. Both Node runners are written that way — +about. **Annotate the result instead**: pin the runner's own type — an inline +generic annotation, or a `types.ts` name such as `/** @type {MemoryRun} */` — +and `O` is inferred from the return type, giving the call a real `O` to check +its argument against. Both Node runners are written that way — `fjs/effects/node/module.mjs`'s `runNodeEffect` and `fjs/effects/node/memory/module.mjs`'s `memoryRun`. diff --git a/fjs/asn.1/module.f.mjs b/fjs/asn.1/module.f.mjs index 35b997354..efe4c8c1a 100644 --- a/fjs/asn.1/module.f.mjs +++ b/fjs/asn.1/module.f.mjs @@ -6,6 +6,7 @@ * * @import { Unpacked, Vec } from '../types/bit_vec/types.ts' * @import { ObjectIdentifier, Raw, Record, Sequence, SupportedRecord, _Tag } from './types.ts' + * @import { _ClassPc, _ParsedTag } from './private.ts' */ import { bitLength, divUp8 } from '../types/bigint/module.f.mjs' @@ -32,29 +33,10 @@ const pop8 = pop(8n) // tag -/** - * @typedef {| - * 0b000_00000n | - * 0b001_00000n | - * 0b010_00000n | - * 0b011_00000n | - * 0b100_00000n | - * 0b101_00000n | - * 0b110_00000n | - * 0b111_00000n - * } _ClassPc - */ - const classPcMask = 0b111_00000n const tagNumberMask = 0b000_11111n -/** - * Note: the tag number (the second parameter) can be arbitrarily large, - * so we can't just use a single byte to represent it. - * @typedef {readonly[_ClassPc, bigint]} _ParsedTag - */ - /** @type {([classPc, number]: _ParsedTag) => Vec} */ const parsedTagEncode = ([classPc, number]) => { const [firstByteNumber, rest] = number < tagNumberMask @@ -140,14 +122,7 @@ export const constructedSet = 0x31n // constructed | set // -/** - * @typedef {{ - * readonly byteLen: bigint - * readonly v: Vec - * }} _Round8 - */ - -/** @type {(_: Unpacked) => _Round8} */ +/** @type {(_: Unpacked) => { readonly byteLen: bigint, readonly v: Vec }} */ const round8 = ({ length, uint }) => { const byteLen = divUp8(length) return { byteLen, v: vec(byteLen << 3n)(uint) } diff --git a/fjs/asn.1/private.ts b/fjs/asn.1/private.ts new file mode 100644 index 000000000..fd64e8dda --- /dev/null +++ b/fjs/asn.1/private.ts @@ -0,0 +1,20 @@ +/** + * Implementation-private types for ASN.1 tag encoding. + */ + +/** The top three bits of a tag's first byte: class and constructed flag. */ +export type _ClassPc = + | 0b000_00000n + | 0b001_00000n + | 0b010_00000n + | 0b011_00000n + | 0b100_00000n + | 0b101_00000n + | 0b110_00000n + | 0b111_00000n + +/** + * Note: the tag number (the second element) can be arbitrarily large, + * so we can't just use a single byte to represent it. + */ +export type _ParsedTag = readonly [_ClassPc, bigint] diff --git a/fjs/bnf/data/module.f.mjs b/fjs/bnf/data/module.f.mjs index ffb97fc3f..68eeff4b7 100644 --- a/fjs/bnf/data/module.f.mjs +++ b/fjs/bnf/data/module.f.mjs @@ -14,9 +14,9 @@ * @module * * @import { DataRule, Rule as FRule, Sequence as FSequence } from '../types.ts' - * @import { StringMap } from '../../types/object/types.ts' * @import { StringSet } from '../../types/string_set/types.ts' - * @import { EmptyTag, Repeat, Rule, RuleSet, Sequence, Variant } from './types.ts' + * @import { EmptyTag, Repeat, Rule, RuleSet, Sequence, Variant, _EmptyTagMap } from './types.ts' + * @import { _FRuleMap, _NewRule } from './private.ts' */ import { stringToCodePointList } from '../../text/utf16/module.f.mjs' @@ -37,8 +37,6 @@ import { contains, set } from '../../types/string_set/module.f.mjs' */ export const isRepeat = rule => typeof rule === 'string' -/** @typedef {StringMap} _EmptyTagMap */ - /** @type {(map: _EmptyTagMap) => (rule: Rule) => EmptyTag} */ const emptyTagOf = map => rule => { if (typeof rule === 'number') { @@ -102,8 +100,6 @@ export const emptyTagMap = ruleSet => { // -/** @typedef {StringMap} _FRuleMap */ - const { entries } = Object /** @type {(map: _FRuleMap) => (fr: FRule) => string | undefined} */ @@ -127,8 +123,6 @@ const newName = (map, name) => { return result } -/** @typedef {(m: _FRuleMap) => readonly [_FRuleMap, RuleSet, Rule]} _NewRule */ - /** @type {(list: FSequence) => _NewRule} */ const sequence = list => map => { /** @type {Sequence} */ diff --git a/fjs/bnf/data/private.ts b/fjs/bnf/data/private.ts new file mode 100644 index 000000000..144a29e11 --- /dev/null +++ b/fjs/bnf/data/private.ts @@ -0,0 +1,19 @@ +/** + * Implementation-private types for the `toData` conversion. + */ + +import type { Rule as FRule } from '../types.ts' +import type { StringMap } from '../../types/object/types.ts' +import type { Rule, RuleSet } from './types.ts' + +/** + * Functional rules already converted, keyed by the generated rule name — the + * memo that keeps a shared functional rule one named data rule. + */ +export type _FRuleMap = StringMap + +/** + * One conversion step: given the memo so far, produces the extended memo, the + * rules the step generated, and the converted rule itself. + */ +export type _NewRule = (m: _FRuleMap) => readonly [_FRuleMap, RuleSet, Rule] diff --git a/fjs/bnf/data/types.ts b/fjs/bnf/data/types.ts index 5f1104640..9615b5b33 100644 --- a/fjs/bnf/data/types.ts +++ b/fjs/bnf/data/types.ts @@ -59,3 +59,8 @@ export type RuleSet = Readonly> * variant branch. */ export type EmptyTag = string | true | undefined + +/** + * The {@link EmptyTag} of every rule in a {@link RuleSet}, keyed by rule name. + */ +export type _EmptyTagMap = StringMap diff --git a/fjs/bnf/descent/module.f.mjs b/fjs/bnf/descent/module.f.mjs index 70bd90998..468923ddc 100644 --- a/fjs/bnf/descent/module.f.mjs +++ b/fjs/bnf/descent/module.f.mjs @@ -29,8 +29,9 @@ * @import { Rule as DataRule, RuleSet, Sequence } from '../data/types.ts' * @import { Rule as FRule } from '../types.ts' * @import { List } from '../../types/list/types.ts' - * @import { Ast, AstResult, AstSequence, AstTag, Cursor } from '../matcher/types.ts' + * @import { Ast, AstSequence, AstTag, Cursor } from '../matcher/types.ts' * @import { CodePointMeta, DescentFailure, DescentMatch, DescentMatchResult, DescentMatchRule } from './types.ts' + * @import { _Failure, _Result } from './private.ts' */ import { rangeDecode } from '../module.f.mjs' @@ -40,27 +41,6 @@ import { definedEntries } from '../../types/object/module.f.mjs' import { emptyTagMap, isRepeat, toData } from '../data/module.f.mjs' import { leafAt, mrFail, mrSuccess, physicalIdx, symbolAt } from '../matcher/module.f.mjs' -/** - * The furthest-failure record while matching, positioned by the complete - * {@link Cursor}. {@link DescentFailure} is its public, physically-positioned - * form. - * - * @typedef {{ - * readonly pos: Cursor - * readonly expected: readonly TerminalRange[] - * }} _Failure - */ - -/** - * The machine's own result: a {@link DescentMatchResult} positioned by the - * complete cursor, and with no failure record — that one is tracked per match - * rather than per frame. This backend always has a position, so it needs no - * `null` case. - * - * @template T - * @typedef {AstResult, Cursor>} _Result - */ - /** * A leaf here is a code point with its metadata, so its symbol is the first * half. This is the only thing {@link symbolAt} needs to know about a leaf. diff --git a/fjs/bnf/descent/private.ts b/fjs/bnf/descent/private.ts new file mode 100644 index 000000000..9d4e0c34d --- /dev/null +++ b/fjs/bnf/descent/private.ts @@ -0,0 +1,25 @@ +/** + * Implementation-private types for the recursive descent matcher backend. + */ + +import type { TerminalRange } from '../types.ts' +import type { AstResult, Cursor } from '../matcher/types.ts' +import type { CodePointMeta, DescentFailure } from './types.ts' + +/** + * The furthest-failure record while matching, positioned by the complete + * {@link Cursor}. {@link DescentFailure} is its public, physically-positioned + * form. + */ +export type _Failure = { + readonly pos: Cursor + readonly expected: readonly TerminalRange[] +} + +/** + * The machine's own result: a `DescentMatchResult` positioned by the complete + * cursor, and with no failure record — that one is tracked per match rather + * than per frame. This backend always has a position, so it needs no `null` + * case. + */ +export type _Result = AstResult, Cursor> diff --git a/fjs/bnf/ll1/module.f.mjs b/fjs/bnf/ll1/module.f.mjs index d44b82c5e..378c10e8e 100644 --- a/fjs/bnf/ll1/module.f.mjs +++ b/fjs/bnf/ll1/module.f.mjs @@ -28,11 +28,10 @@ * @import { CodePoint } from '../../text/utf16/types.ts' * @import { Properties } from '../../types/range_map/types.ts' * @import { StringSet } from '../../types/string_set/types.ts' - * @import { List } from '../../types/list/types.ts' - * @import { RuleSet, Sequence } from '../data/types.ts' - * @import { Ast, AstResult, AstSequence, AstTag, Cursor } from '../matcher/types.ts' + * @import { RuleSet } from '../data/types.ts' * @import { Rule as FRule } from '../types.ts' * @import { Match, MatchResult, Remainder, _Dispatch, _DispatchBranch, _DispatchMap, _DispatchResult, _DispatchRule } from './types.ts' + * @import { _Position, _Result, _Stack, _Task } from './private.ts' */ import { strictEqual } from '../../types/function/operator/module.f.mjs' @@ -163,90 +162,6 @@ export const parser = fr => { return parserRuleSet(data[0]) } -/** - * Where a match stopped: a {@link Cursor}, or `null` when it ran out of input — - * the `null` {@link Remainder} this backend reports for that. - * - * @typedef {Cursor|null} _Position - */ - -/** - * The machine's own result: a {@link MatchResult} positioned by a cursor - * instead of by a materialized remainder. - * - * @typedef {AstResult} _Result - */ - -/** - * A suspended sequence match: `items[itemIndex]` is being matched by the - * current task, and `seq` holds the ASTs of the items already matched. - * - * @typedef {{ - * readonly kind: 'seq' - * readonly tag: AstTag - * readonly items: Sequence - * readonly itemIndex: number - * readonly seq: AstSequence - * }} _SeqFrame - */ - -/** - * A suspended repetition: the item is being matched by the current task for - * one more round, and `items` holds the ASTs of the rounds that already - * completed. They accumulate as a list rather than an array because a - * repetition is as long as its input: appending to an array per round would - * copy the whole prefix each time and make one repetition quadratic in the - * number of items it matched. - * - * @typedef {{ - * readonly kind: 'repeat' - * readonly tag: AstTag - * readonly item: string - * readonly items: _Items - * }} _RepeatFrame - */ - -/** @typedef {List>} _Items */ - -/** @typedef {_SeqFrame | _RepeatFrame} _Frame */ - -/** - * Immutable cons-cell stack: O(1) push/pop, no array copying per step. - * - * @typedef {null | { - * readonly top: _Frame - * readonly rest: _Stack - * }} _Stack - */ - -/** - * The rule invocation about to be evaluated, or `null` when a result is ready - * to resume the innermost frame instead. - * - * @typedef {{ - * readonly kind: 'rule' - * readonly name: string - * readonly tag: AstTag - * readonly pos: Cursor - * }} _RuleTask - */ - -/** - * The next round of a repetition, about to be decided by lookahead. Both the - * rule that introduces a repetition and the frame that finishes one of its - * rounds go through this, so a round is set up in exactly one place. - * - * @typedef {{ - * readonly kind: 'repeat' - * readonly tag: AstTag - * readonly item: string - * readonly items: _Items - * readonly pos: Cursor - * }} _RepeatTask - */ - -/** @typedef {_RuleTask | _RepeatTask} _Task */ - /** * A leaf here is the code point itself, so it *is* its own symbol. The * annotation pins `identity`'s type parameter, which `symbolAt`'s own cannot diff --git a/fjs/bnf/ll1/private.ts b/fjs/bnf/ll1/private.ts new file mode 100644 index 000000000..d8442edba --- /dev/null +++ b/fjs/bnf/ll1/private.ts @@ -0,0 +1,85 @@ +/** + * Implementation-private types for the LL(1) matcher machine. + */ + +import type { CodePoint } from '../../text/utf16/types.ts' +import type { List } from '../../types/list/types.ts' +import type { Sequence } from '../data/types.ts' +import type { Ast, AstResult, AstSequence, AstTag, Cursor } from '../matcher/types.ts' + +/** + * Where a match stopped: a {@link Cursor}, or `null` when it ran out of input — + * the `null` `Remainder` this backend reports for that. + */ +export type _Position = Cursor | null + +/** + * The machine's own result: a `MatchResult` positioned by a cursor instead of + * by a materialized remainder. + */ +export type _Result = AstResult + +/** + * A suspended sequence match: `items[itemIndex]` is being matched by the + * current task, and `seq` holds the ASTs of the items already matched. + */ +export type _SeqFrame = { + readonly kind: 'seq' + readonly tag: AstTag + readonly items: Sequence + readonly itemIndex: number + readonly seq: AstSequence +} + +/** + * A suspended repetition: the item is being matched by the current task for + * one more round, and `items` holds the ASTs of the rounds that already + * completed. They accumulate as a list rather than an array because a + * repetition is as long as its input: appending to an array per round would + * copy the whole prefix each time and make one repetition quadratic in the + * number of items it matched. + */ +export type _RepeatFrame = { + readonly kind: 'repeat' + readonly tag: AstTag + readonly item: string + readonly items: _Items +} + +export type _Items = List> + +export type _Frame = _SeqFrame | _RepeatFrame + +/** + * Immutable cons-cell stack: O(1) push/pop, no array copying per step. + */ +export type _Stack = null | { + readonly top: _Frame + readonly rest: _Stack +} + +/** + * The rule invocation about to be evaluated, or `null` when a result is ready + * to resume the innermost frame instead. + */ +export type _RuleTask = { + readonly kind: 'rule' + readonly name: string + readonly tag: AstTag + readonly pos: Cursor +} + +/** + * The next round of a repetition, about to be decided by lookahead. Both the + * rule that introduces a repetition and the frame that finishes one of its + * rounds go through this, so a round is set up in exactly one place. + */ +export type _RepeatTask = { + readonly kind: 'repeat' + readonly tag: AstTag + readonly item: string + readonly items: _Items + readonly pos: Cursor +} + +export type _Task = _RuleTask | _RepeatTask diff --git a/fjs/bnf/matcher/module.f.mjs b/fjs/bnf/matcher/module.f.mjs index 03bc447fe..871d81811 100644 --- a/fjs/bnf/matcher/module.f.mjs +++ b/fjs/bnf/matcher/module.f.mjs @@ -56,13 +56,7 @@ export const symbolAt = symbolOf => (input, pos) => */ export const physicalIdx = length => pos => Math.min(pos, length) -/** - * @template L - * @template P - * @typedef {(tag: AstTag, sequence: AstSequence, pos: P) => AstResult} _Mr - */ - -/** @type {(success: boolean) => _Mr} */ +/** @type {(success: boolean) => (tag: AstTag, sequence: AstSequence, pos: P) => AstResult} */ const mr = success => (tag, sequence, pos) => ({ ast: { tag, sequence }, success, pos }) /** diff --git a/fjs/bnf/module.f.mjs b/fjs/bnf/module.f.mjs index 37c2311ae..9dd292053 100644 --- a/fjs/bnf/module.f.mjs +++ b/fjs/bnf/module.f.mjs @@ -167,20 +167,18 @@ export const range = ab => { return rangeEncode(...a) } -/** @typedef {readonly TerminalRange[]} _RangeList */ - /** @type {(r: TerminalRange) => readonly [string, TerminalRange]} */ const rangeToEntry = r => ['0x' + r.toString(16), r] -/** @type {(r: _RangeList) => RangeVariant} */ +/** @type {(r: readonly TerminalRange[]) => RangeVariant} */ const toVariantRangeSet = r => fromEntries(r.map(rangeToEntry)) -/** @type {(list: _RangeList, ab: number) => _RangeList} */ +/** @type {(list: readonly TerminalRange[], ab: number) => readonly TerminalRange[]} */ const removeOne = (list, ab) => { const [a, b] = rangeDecode(ab) - /** @type {_RangeList} */ + /** @type {readonly TerminalRange[]} */ let result = [] for (const ab0 of list) { const [a0, b0] = rangeDecode(ab0) @@ -200,7 +198,7 @@ const removeOne = (list, ab) => { /** @type {(range: TerminalRange, v: RangeVariant) => RangeVariant} */ export const remove = (range, v) => { - /** @type {_RangeList} */ + /** @type {readonly TerminalRange[]} */ let result = [range] for (const r of definedValues(v)) { result = removeOne(result, r) diff --git a/fjs/bnf/private.ts b/fjs/bnf/private.ts new file mode 100644 index 000000000..1a0246564 --- /dev/null +++ b/fjs/bnf/private.ts @@ -0,0 +1,28 @@ +/** + * Implementation-private types for the AST renderer in `./testlib.f.mjs`. + */ + +import type { Ast } from './matcher/types.ts' + +/** + * The leaf of either backend's AST: `bnf/ll1` keeps the code point alone and + * `bnf/descent` pairs it with metadata, so a renderer that takes both is + * generic over exactly this. + * + * `showAst`'s exported declaration writes this union inline so the public + * declaration does not depend on this private module. + */ +export type _Leaf = number | readonly [number, unknown] + +export type _AstNode = Ast<_Leaf> + +export type _AstChild = _AstNode | _Leaf + +/** + * The renderer's accumulator: the parts already rendered, and the run of + * consumed code points being accumulated as one quoted string. + */ +export type _Parts = { + readonly parts: readonly string[] + readonly text: string +} diff --git a/fjs/bnf/testlib.f.mjs b/fjs/bnf/testlib.f.mjs index 7bb384d60..08594c06c 100644 --- a/fjs/bnf/testlib.f.mjs +++ b/fjs/bnf/testlib.f.mjs @@ -3,6 +3,7 @@ * * @import { Ast, AstTag } from './matcher/types.ts' * @import { Rule } from './types.ts' + * @import { _AstChild, _AstNode, _Leaf, _Parts } from './private.ts' */ import { codePointToString } from '../text/utf16/module.f.mjs' @@ -199,18 +200,6 @@ export const deterministic = () => { // -/** - * The leaf of either backend's AST: `bnf/ll1` keeps the code point alone and - * `bnf/descent` pairs it with metadata, so a renderer that takes both is - * generic over exactly this. - * - * @typedef {number | readonly [number, unknown]} _Leaf - */ - -/** @typedef {Ast<_Leaf>} _AstNode */ - -/** @typedef {_AstNode | _Leaf} _AstChild */ - /** * @param {_AstChild} child * @returns {child is _AstNode} @@ -230,8 +219,6 @@ const codePointOf = child => typeof child === 'number' ? child : child[0] const showTag = tag => tag === undefined ? '' : tag === true ? '*' : JSON.stringify(tag) -/** @typedef {{ readonly parts: readonly string[], readonly text: string }} _Parts */ - /** * Ends the run of consumed code points being accumulated, if there is one, so * that a node's text appears as one quoted string rather than one part per @@ -262,7 +249,11 @@ const noParts = { parts: [], text: '' } * tags survive. Repeated items are siblings under one node, whereas the * right-recursive encoding puts each item one level deeper than the last. * - * @type {(node: _AstNode) => string} + * The leaf union — a bare code point, or a code point with metadata — is + * `_Leaf` in `./private.ts`, written inline here so the exported declaration + * does not depend on the private module. + * + * @type {(node: Ast) => string} * * @example * diff --git a/fjs/cas/proof.f.mjs b/fjs/cas/proof.f.mjs index 097ef7a98..4eb24407c 100644 --- a/fjs/cas/proof.f.mjs +++ b/fjs/cas/proof.f.mjs @@ -22,8 +22,6 @@ import { assert, assertEq, assertNotNullish } from '../asserts/module.f.mjs' const testDir = './test-cas-cli' -/** @typedef {FileCasOperation | WriteFile | ReadFile | Mkdir} _TestOp */ - // Names the command a `FileCasOperation` effect stops at, so a proof can assert // on it and resume the continuation without reading the `Do` layout. The map // has to list every operation the CAS can perform — that is what makes it total, @@ -152,7 +150,7 @@ const createBigFileContent = () => { // and the virtual filesystem cannot remove a *non-empty* directory, so that // `rm` had been failing on every run without anything noticing. There is also // nothing to clean: each run interprets against a fresh `emptyState`. -/** @type {() => Effect<_TestOp, void, IoChannel>} */ +/** @type {() => Effect} */ const testAddBigFile = () => { const bigFilePath = `${testDir}/big-file.bin` const cas = fileCas(sha256)(testDir) @@ -170,7 +168,7 @@ const testAddBigFile = () => { } // Test adding and retrieving a big file -/** @type {() => Effect<_TestOp, void, IoChannel>} */ +/** @type {() => Effect} */ const testAddAndGetBigFile = () => { const bigContent = createBigFileContent() const bigFilePath = `${testDir}/big-file.bin` diff --git a/fjs/ci/node/module.f.mjs b/fjs/ci/node/module.f.mjs index ecc355138..57c6aaf58 100644 --- a/fjs/ci/node/module.f.mjs +++ b/fjs/ci/node/module.f.mjs @@ -64,6 +64,10 @@ const node26Steps = [ ...nodeInstall(node.default), test({ run: 'npm run ci-update' }), test({ run: 'git add -A && git diff --cached --exit-code' }), + // No authored `.mjs` may contain a file-scope JSDoc `@typedef` (root + // `AGENTS.md`); `tsc` accepts one silently, so the prohibition needs its + // own gate. + test({ run: "! grep -rnE '^(/\\*\\*.*@typedef|\\s\\* *@typedef)' --include='*.mjs' --exclude-dir=node_modules ." }), test({ run: 'npx tsc' }), test({ run: 'npm run cov' }), test({ run: 'npm pack' }), diff --git a/fjs/ci/todo/f-mjs-package-support.md b/fjs/ci/todo/f-mjs-package-support.md index eea1a0991..23e886d16 100644 --- a/fjs/ci/todo/f-mjs-package-support.md +++ b/fjs/ci/todo/f-mjs-package-support.md @@ -160,22 +160,15 @@ Update `AGENTS.md` with that runtime source-migration policy and the stable `types.ts` companion convention. Compiler compatibility is a later `.f.mjs -> .f.js` migration and is not part of this package prerequisite. -JSDoc declaration emit currently exposes every top-level `@typedef` as an -exported type alias. During the migration, implementation-only typedefs that stay -inside `.mjs` use the repository's leading-`_` convention, for example `_Node`; -see [`todo/migrate-typescript-to-mjs.md`](../../../todo/migrate-typescript-to-mjs.md). -An emitted `export type _Node = ...` is therefore package-private by contract, -not public API. Clean-consumer tests must exercise documented public types and -must not turn `_`-prefixed declaration artifacts into supported API merely -because TypeScript emitted them. - -Types intentionally moved to `types.ts` use ordinary TypeScript syntax and do -not need the JSDoc-emission workaround merely to remain expressible. The eventual -replacement for private JSDoc typedefs is still `@internal` plus `stripInternal`, -blocked on -[microsoft/TypeScript#46407](https://github.com/microsoft/TypeScript/issues/46407) -and tracked in -[`todo/blocked/jsdoc-typedef-strip-internal.md`](../../../todo/blocked/jsdoc-typedef-strip-internal.md). +Authored `.mjs` files carry no file-scope JSDoc `@typedef` (root `AGENTS.md`); +named types live in `types.ts` or an optional `private.ts`, so declaration emit +exposes private types as `_`-prefixed names in `types.d.ts` and as generated +`private.d.ts` files. Both are package-private by contract, not public API: +clean-consumer tests must exercise documented public types and must not turn +`_`-prefixed declaration artifacts into supported API merely because TypeScript +emitted them. Deleting generated `private.d.ts` before packaging is the second +stage of +[`fjs/todo/separate-private-types.md`](../../todo/separate-private-types.md). Package selection does not need to distinguish every authored `.mjs` by public API status during this transition. Incidental authored files such as @@ -248,9 +241,10 @@ emission, `npm pack`, and a clean consumer. required for portable resolution. Done in [#1520](https://github.com/functionalscript/functionalscript/pull/1520): only `types.d.ts` is required; `types.js` is no longer generated. -- [ ] Include an implementation-only `_`-prefixed JSDoc typedef in the `.mjs` - fixture; tolerate its current exported declaration form without treating it - as clean-consumer public API. +- [ ] Include an implementation-only `_`-prefixed type (in the fixture's + `types.ts` or `private.ts`, per the file-scope-typedef prohibition) whose + name reaches the emitted declarations; tolerate that declaration form + without treating it as clean-consumer public API. - [ ] Test the allowed `.ts` -> `.mjs` runtime dependency direction in a clean checkout and CI-built package archive. - [ ] Reject authored `.mjs` runtime imports to remaining relative implementation @@ -327,9 +321,9 @@ not, and the pipeline is simplified accordingly. two-pass `prepack`. - [`todo/migrate-typescript-to-mjs.md`](../../../todo/migrate-typescript-to-mjs.md) — repository-wide stage-1 implementation source migration. -- [`todo/blocked/jsdoc-typedef-strip-internal.md`](../../../todo/blocked/jsdoc-typedef-strip-internal.md) - — replace the temporary `_` convention with `@internal` when declaration emit - supports it. +- [`fjs/todo/separate-private-types.md`](../../todo/separate-private-types.md) + — private-type placement rules and the packaging stage that unships + generated private declarations. - [microsoft/TypeScript#46407](https://github.com/microsoft/TypeScript/issues/46407) — upstream blocker for stripping private JSDoc typedefs. - [`publishing-packages.md`](./publishing-packages.md) — broader package roadmap. diff --git a/fjs/common/monoid/module.f.mjs b/fjs/common/monoid/module.f.mjs index aed4fa9d9..2bdf6ab9f 100644 --- a/fjs/common/monoid/module.f.mjs +++ b/fjs/common/monoid/module.f.mjs @@ -9,6 +9,7 @@ * @import { Fold, Reduce } from '../../types/function/operator/types.ts' * @import { Accumulator, List } from '../../types/list/types.ts' * @import { Absorbing, Monoid } from './types.ts' + * @import { _Run, _Stack } from './private.ts' */ import { fold as listFold, tryFold } from '../../types/list/module.f.mjs' @@ -59,26 +60,6 @@ export const repeat = ({ identity, operation }) => n => a => { } } -/** - * A run of `size` already-combined elements. Runs live on a stack whose top is - * the most recent — and smallest — run, so `rest` holds everything to the left - * of `value`. - * - * @template T - * @typedef {{ - * readonly size: number - * readonly value: T - * readonly rest: _Stack - * }} _Run - */ - -/** - * A stack of runs, `null` when empty. - * - * @template T - * @typedef {_Run | null} _Stack - */ - /** * Pushes a run of `size` combined elements onto the stack, merging while the * top run has the same size — exactly the carry of incrementing a binary diff --git a/fjs/common/monoid/private.ts b/fjs/common/monoid/private.ts new file mode 100644 index 000000000..a86d8a890 --- /dev/null +++ b/fjs/common/monoid/private.ts @@ -0,0 +1,17 @@ +/** + * Implementation-private types for the monoid fold. + */ + +/** + * A run of `size` already-combined elements. Runs live on a stack whose top is + * the most recent — and smallest — run, so `rest` holds everything to the left + * of `value`. + */ +export type _Run = { + readonly size: number + readonly value: T + readonly rest: _Stack +} + +/** A stack of runs, `null` when empty. */ +export type _Stack = _Run | null diff --git a/fjs/crypto/sha2/module.f.mjs b/fjs/crypto/sha2/module.f.mjs index e08474495..2094d2569 100644 --- a/fjs/crypto/sha2/module.f.mjs +++ b/fjs/crypto/sha2/module.f.mjs @@ -28,25 +28,17 @@ const { concat, front } = msb // across every `base(...)` config (32-bit and 64-bit SHA-2 variants). const chunkListMsb = chunkList(msb) -/** @typedef {Tuple<3, bigint>} _V3 */ - -/** @typedef {Tuple<4, bigint>} _V4 */ - -/** - * @typedef {{ - * readonly logBitLen: bigint, - * readonly k: readonly V16[], - * readonly bs0: _V3, - * readonly bs1: _V3, - * readonly ss0: _V3, - * readonly ss1: _V3, - * }} _BaseInit - */ - /** @type {Vec} */ const lastOne = vec(1n)(1n) -/** @type {(init: _BaseInit) => Base} */ +/** @type {(init: { + * readonly logBitLen: bigint, + * readonly k: readonly V16[], + * readonly bs0: Tuple<3, bigint>, + * readonly bs1: Tuple<3, bigint>, + * readonly ss0: Tuple<3, bigint>, + * readonly ss1: Tuple<3, bigint>, + * }) => Base} */ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }) => { const bitLength = 1n << logBitLen @@ -57,7 +49,7 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }) => { return n => n >> d | n << r } - /** @type {(third: Reduce) => (..._: _V3) => (x: bigint) => bigint} */ + /** @type {(third: Reduce) => (..._: Tuple<3, bigint>) => (x: bigint) => bigint} */ const sigma = third => (a, b, c) => { const ra = rotr(a) const rb = rotr(b) @@ -85,7 +77,7 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }) => { const m = mask(bitLength) - /** @type {(..._: _V4) => bigint} */ + /** @type {(..._: Tuple<4, bigint>) => bigint} */ const wi = (a0, a1, a2, a3) => (smallSigma1(a0) + a1 + smallSigma0(a2) + a3) & m diff --git a/fjs/crypto/sign/module.f.mjs b/fjs/crypto/sign/module.f.mjs index b0afe15c1..6384dec46 100644 --- a/fjs/crypto/sign/module.f.mjs +++ b/fjs/crypto/sign/module.f.mjs @@ -8,7 +8,7 @@ * @import { Vec } from '../../types/bit_vec/types.ts' * @import { Curve } from '../secp/types.ts' * @import { Sha2 } from '../sha2/types.ts' - * @import { All } from './types.ts' + * @import { All, _Signature } from './types.ts' */ import { assertNotNullish } from '../../asserts/module.f.mjs' @@ -133,8 +133,6 @@ export const computeK = } } -/** @typedef {Tuple<2, bigint>} _Signature */ - /** * Signs a message bit vector and returns an ECDSA `(r, s)` signature pair. * diff --git a/fjs/crypto/sign/types.ts b/fjs/crypto/sign/types.ts index 9825e3666..22f23da2c 100644 --- a/fjs/crypto/sign/types.ts +++ b/fjs/crypto/sign/types.ts @@ -4,6 +4,7 @@ * @module */ +import type { Tuple } from '../../types/array/types.ts' import type { Vec } from '../../types/bit_vec/types.ts' export type All = { @@ -13,3 +14,6 @@ export type All = { readonly int2octets: (x: bigint) => Vec readonly bits2octets: (b: Vec) => Vec } + +/** An ECDSA signature: the `(r, s)` pair. */ +export type _Signature = Tuple<2, bigint> diff --git a/fjs/djs/ast/module.f.mjs b/fjs/djs/ast/module.f.mjs index 04e87c945..f6953cbf0 100644 --- a/fjs/djs/ast/module.f.mjs +++ b/fjs/djs/ast/module.f.mjs @@ -4,9 +4,8 @@ * @module * * @import { Array, Unknown } from '../types.ts' - * @import { List } from '../../types/list/types.ts' - * @import { Entry } from '../../types/ordered_map/types.ts' * @import { AstConst, AstBody } from './types.ts' + * @import { _FoldObjectState, _RunState } from './private.ts' */ import { concat, fold, last, map, take, toArray } from '../../types/list/module.f.mjs' @@ -14,17 +13,6 @@ import { fromEntries } from '../../types/object/module.f.mjs' const { entries } = Object -/** @typedef {{ - * readonly body: AstBody - * readonly args: Array - * readonly consts: List - * }} _RunState */ - -/** @typedef {{ - * readonly runState: _RunState, - * readonly entries: List> - * }} _FoldObjectState */ - /** @type {(ast: AstConst) => (state: _RunState) => _RunState} */ const foldOp = ast => state => { const djs = toDjs(state)(ast) diff --git a/fjs/djs/ast/private.ts b/fjs/djs/ast/private.ts new file mode 100644 index 000000000..6c5e7ce49 --- /dev/null +++ b/fjs/djs/ast/private.ts @@ -0,0 +1,21 @@ +/** + * Implementation-private types for the DJS AST evaluator. + */ + +import type { List } from '../../types/list/types.ts' +import type { Entry } from '../../types/ordered_map/types.ts' +import type { Array, Unknown } from '../types.ts' +import type { AstBody } from './types.ts' + +/** An evaluation in progress: the body, its arguments, and the values so far. */ +export type _RunState = { + readonly body: AstBody + readonly args: Array + readonly consts: List +} + +/** The state of folding an AST object's entries into evaluated entries. */ +export type _FoldObjectState = { + readonly runState: _RunState, + readonly entries: List> +} diff --git a/fjs/djs/module.f.mjs b/fjs/djs/module.f.mjs index 6716d28e3..6256873a5 100644 --- a/fjs/djs/module.f.mjs +++ b/fjs/djs/module.f.mjs @@ -3,9 +3,8 @@ * * @module * - * @import { WriteFile, ReadFile, Write } from '../effects/node/types.ts' * @import { Result } from '../types/result/types.ts' - * @import { Unknown } from './types.ts' + * @import { Unknown, _CompileOp } from './types.ts' * @import { ParseError } from './parser/types.ts' * @import { Effect } from '../effects/types.ts' */ @@ -16,8 +15,6 @@ import { sort } from '../types/object/module.f.mjs' import { resultStep } from '../effects/module.f.mjs' import { errorExit, exitStep, writeUtf8File } from '../effects/node/module.f.mjs' -/** @typedef {ReadFile | WriteFile | Write} _CompileOp */ - /** * Where an error happened, as much of it as is known: the token's * `path:line:column` when the reader tracks positions, and otherwise the name diff --git a/fjs/djs/parser/module.f.mjs b/fjs/djs/parser/module.f.mjs index 4fe10a1f0..6090981b2 100644 --- a/fjs/djs/parser/module.f.mjs +++ b/fjs/djs/parser/module.f.mjs @@ -5,18 +5,14 @@ * * @import { Result } from '../../types/result/types.ts' * @import { List } from '../../types/list/types.ts' - * @import { Fold } from '../../types/function/operator/types.ts' * @import { DjsToken, DjsTokenWithMetadata } from '../tokenizer/types.ts' - * @import { OrderedMap } from '../../types/ordered_map/types.ts' * @import { AstArray, AstConst, AstModule, AstModuleRef, AstObject } from '../ast/types.ts' - * @import { TokenMetadata } from '../../js/tokenizer/types.ts' - * @import { ParseError, _FramingKeyword, _OrdinaryTokenName, _ValueToken } from './types.ts' - * @import { Assert } from '../../asserts/types.ts' - * @import { Equal } from '../../types/ts/types.ts' + * @import { ParseError, _OrdinaryTokenName, _ValueToken } from './types.ts' * @import { CodePointMeta } from '../../bnf/descent/types.ts' - * @import { Ast, AstSequence } from '../../bnf/matcher/types.ts' + * @import { AstSequence } from '../../bnf/matcher/types.ts' * @import { Rule, TerminalRange } from '../../bnf/types.ts' * @import { DescentMatch } from '../../bnf/descent/types.ts' + * @import { _FoldFrame, _FoldState, _Node, _TokenStream } from './private.ts' */ import { error, ok } from '../../types/result/module.f.mjs' @@ -29,16 +25,6 @@ import { encoding } from '../../bnf/token_symbol/module.f.mjs' import { toData } from '../../bnf/data/module.f.mjs' import { descentParserRuleSet } from '../../bnf/descent/module.f.mjs' -/** - * The ordinary token stream a BNF parser layer consumes, with the tokenizer's - * one physical end-of-input token split off. - * - * @typedef {{ - * readonly tokens: readonly DjsTokenWithMetadata[] - * readonly eofMetadata: TokenMetadata - * }} _TokenStream - */ - /** * Splits the tokenizer's single final physical `eof` token off a token list. * @@ -96,13 +82,14 @@ const splitEof = tokenList => { * * A name is not always a kind. The framing keywords arrive as `id` tokens and * need terminals of their own, or the grammar could not tell `export default` - * from two arbitrary identifiers — see {@link framingKeywords}. + * from two arbitrary identifiers — see {@link _framingKeywords}. * - * The `_…AreComplete` assertions below check both halves against `DjsToken` and - * `_FramingKeyword` at compile time, so a kind or keyword added there breaks the - * build rather than going unrepresented. + * The `_…AreComplete` assertions in `./proof.f.mjs`'s `consistency` entry check + * both halves against `DjsToken` and `_FramingKeyword` at compile time, so a + * kind or keyword added there breaks the build rather than going unrepresented. + * Exported with a leading `_` for that linkage — the export is not API. */ -const tokenKindNames = /** @type {const} */ ([ +export const _tokenKindNames = /** @type {const} */ ([ 'true', 'false', 'null', 'undefined', '{', '}', ':', ',', '[', ']', '.', '=', 'string', 'number', 'error', 'id', 'bigint', @@ -125,29 +112,14 @@ const tokenKindNames = /** @type {const} */ ([ * Giving a word its own symbol narrows where it is *required*, never where it is * *allowed*. */ -const framingKeywords = /** @type {const} */ (['import', 'const', 'export', 'default', 'from']) +export const _framingKeywords = /** @type {const} */ (['import', 'const', 'export', 'default', 'from']) /** * The complete alphabet: one name per `DjsToken` kind except `eof`, plus one per * framing keyword. No keyword collides with a kind, so the two lists concatenate * without a name being registered twice — which `encoding` would reject anyway. */ -const ordinaryTokenNames = [...tokenKindNames, ...framingKeywords] - -/** @typedef {Assert>>} _KindsAreComplete */ - -/** @typedef {Assert>} _KeywordsAreComplete */ - -/** @typedef {Assert>} _AlphabetIsComplete */ - -/** - * `eof` is not a member of the alphabet, so a second end marker cannot be - * encoded rather than merely going unused — and `encode` would reject the name - * outright. Checked at the type level because that is where it is decidable: - * `includes('eof')` does not even compile against this element type. - * - * @typedef {Assert, never>>} _EofIsNotAName - */ +export const _ordinaryTokenNames = [..._tokenKindNames, ..._framingKeywords] /** * The alphabet's encoding, built once for the module rather than per parse. @@ -158,7 +130,7 @@ const ordinaryTokenNames = [...tokenKindNames, ...framingKeywords] * last Unicode scalar value, so a token symbol can never be mistaken for a code * point of the layer below. */ -const tokenEncoding = encoding(ordinaryTokenNames) +const tokenEncoding = encoding(_ordinaryTokenNames) /** * One ordinary token as a descent input leaf: the symbol standing for its kind, @@ -181,7 +153,7 @@ const tokenToSymbol = t => { // a set membership test because it also narrows the result to the keyword // union, which is what lets `encode` be called without a cast. const keyword = token.kind === 'id' - ? framingKeywords.find(k => k === token.value) + ? _framingKeywords.find(k => k === token.value) : undefined const name = keyword ?? token.kind assert(name !== 'eof', ['eof token reached the parser alphabet', t]) @@ -226,7 +198,7 @@ const statementEnd = () => [ * Every word that may stand where an identifier is expected: a plain `id` and * each framing keyword, since none of them is reserved. * - * This is the union {@link framingKeywords} obliges the grammar to provide. + * This is the union {@link _framingKeywords} obliges the grammar to provide. */ const identifier = { id: sym('id'), @@ -396,8 +368,6 @@ const isValueToken = token => { } // -- folding the match into an `AstModule` ---------------------------------- -/** @typedef {Ast>} _Node */ - /** * The token a slot holds. * @@ -509,22 +479,6 @@ const keyOf = node => { return [token.value, computed] } -/** - * A fold in progress: the names bound so far, the module specifiers and the - * body collected so far, and the first error if one has been met. - * - * The error rides in the state rather than wrapping every step in a `Result`, - * so a step reads as one expression instead of a nested match. Once set it is - * never replaced, which is what makes the reported error the *first* one. - * - * @typedef {{ - * readonly refs: OrderedMap - * readonly modules: readonly string[] - * readonly consts: readonly AstConst[] - * readonly error: ParseError | null - * }} _FoldState - */ - /** @type {(message: string) => (token: DjsTokenWithMetadata) => ParseError} */ const foldError = message => ({ metadata }) => ({ message, metadata }) @@ -545,24 +499,6 @@ const bind = state => node => ref => { : { ...state, refs: setReplace(token.value)(ref)(state.refs) } } -/** - * A frame of {@link foldValue}'s explicit stack: the container being built, the - * element nodes still to read, and what has been built so far. - * - * `done` is a `List` rather than an array because a frame gains one element at a - * time: appending to an array per element would copy the whole prefix each time, - * which is what makes the obvious spelling quadratic in an array's length. - * - * @typedef {{ - * readonly items: readonly _Node[] - * readonly index: number - * readonly array: List - * readonly object: OrderedMap - * readonly keys: readonly(readonly[string, boolean])[] - * readonly isArray: boolean - * }} _FoldFrame - */ - /** * The error a frame's current key earns, or `null`. * @@ -774,7 +710,7 @@ export const proof = { // matters because the token-symbol mapping this alphabet feeds has to be // injective over it — two entries for one name would break that. noDuplicates: () => { - assertEq(new Set(ordinaryTokenNames).size, ordinaryTokenNames.length) + assertEq(new Set(_ordinaryTokenNames).size, _ordinaryTokenNames.length) }, }, tokenToSymbol: { @@ -782,8 +718,8 @@ export const proof = { // code point — the three properties that let a token stream be the // alphabet of the layer above. distinctAndAboveUnicode: () => { - const symbols = ordinaryTokenNames.map(n => tokenEncoding.encode(n)) - assertEq(new Set(symbols).size, ordinaryTokenNames.length) + const symbols = _ordinaryTokenNames.map(n => tokenEncoding.encode(n)) + assertEq(new Set(symbols).size, _ordinaryTokenNames.length) const [, unicodeLast] = rangeDecode(unicodeRange) assert(symbols.every(s => s > unicodeLast), JSON.stringify(symbols)) }, @@ -796,9 +732,9 @@ export const proof = { const symbolOf = value => tokenToSymbol({ token: { kind: 'id', value }, metadata: { path: 'a.js', line: 1, column: 1 } })[0] const id = symbolOf('foo') - const keywords = framingKeywords.map(symbolOf) + const keywords = _framingKeywords.map(symbolOf) assert(keywords.every(s => s !== id), JSON.stringify([id, keywords])) - assertEq(new Set(keywords).size, framingKeywords.length) + assertEq(new Set(keywords).size, _framingKeywords.length) assertEq(tokenEncoding.decode(symbolOf('export')), 'export') assertEq(tokenEncoding.decode(id), 'id') }, diff --git a/fjs/djs/parser/private.ts b/fjs/djs/parser/private.ts new file mode 100644 index 000000000..567963e37 --- /dev/null +++ b/fjs/djs/parser/private.ts @@ -0,0 +1,56 @@ +/** + * Implementation-private types for the DJS parser. + */ + +import type { CodePointMeta } from '../../bnf/descent/types.ts' +import type { Ast } from '../../bnf/matcher/types.ts' +import type { TokenMetadata } from '../../js/tokenizer/types.ts' +import type { List } from '../../types/list/types.ts' +import type { OrderedMap } from '../../types/ordered_map/types.ts' +import type { AstConst, AstModuleRef } from '../ast/types.ts' +import type { DjsTokenWithMetadata } from '../tokenizer/types.ts' +import type { ParseError } from './types.ts' + +/** + * The ordinary token stream a BNF parser layer consumes, with the tokenizer's + * one physical end-of-input token split off. + */ +export type _TokenStream = { + readonly tokens: readonly DjsTokenWithMetadata[] + readonly eofMetadata: TokenMetadata +} + +/** A node of the matched module's AST, its leaves carrying the tokens. */ +export type _Node = Ast> + +/** + * A fold in progress: the names bound so far, the module specifiers and the + * body collected so far, and the first error if one has been met. + * + * The error rides in the state rather than wrapping every step in a `Result`, + * so a step reads as one expression instead of a nested match. Once set it is + * never replaced, which is what makes the reported error the *first* one. + */ +export type _FoldState = { + readonly refs: OrderedMap + readonly modules: readonly string[] + readonly consts: readonly AstConst[] + readonly error: ParseError | null +} + +/** + * A frame of `foldValue`'s explicit stack: the container being built, the + * element nodes still to read, and what has been built so far. + * + * `done` is a `List` rather than an array because a frame gains one element at a + * time: appending to an array per element would copy the whole prefix each time, + * which is what makes the obvious spelling quadratic in an array's length. + */ +export type _FoldFrame = { + readonly items: readonly _Node[] + readonly index: number + readonly array: List + readonly object: OrderedMap + readonly keys: readonly(readonly[string, boolean])[] + readonly isArray: boolean +} diff --git a/fjs/djs/parser/proof.f.mjs b/fjs/djs/parser/proof.f.mjs index 54e59831a..29859ff27 100644 --- a/fjs/djs/parser/proof.f.mjs +++ b/fjs/djs/parser/proof.f.mjs @@ -1,8 +1,16 @@ /** - * @import { DjsTokenWithMetadata } from '../tokenizer/types.ts' + * @import { Assert } from '../../asserts/types.ts' + * @import { Equal } from '../../types/ts/types.ts' + * @import { DjsToken, DjsTokenWithMetadata } from '../tokenizer/types.ts' + * @import { _FramingKeyword, _OrdinaryTokenName } from './types.ts' */ -import { parseFromTokens } from './module.f.mjs' +import { + parseFromTokens, + _framingKeywords, + _ordinaryTokenNames, + _tokenKindNames, +} from './module.f.mjs' import { tokenize } from '../tokenizer/module.f.mjs' import { toArray } from '../../types/list/module.f.mjs' import { sort } from '../../types/object/module.f.mjs' @@ -44,6 +52,22 @@ const proofKind = (kind, line) => ({ token: { kind }, metadata: { path: 'a.js', const proofId = (value, line) => ({ token: { kind: 'id', value }, metadata: { path: 'a.js', line, column: 1 } }) export const proof = { + /** + * The parser alphabet in `./module.f.mjs` agrees with its type-level + * description in `./types.ts`. These are compile-time checks; the function + * body only has to exist so the typedefs have a local scope. + */ + consistency: () => { + /** @typedef {Assert>>} _KindsAreComplete */ + /** @typedef {Assert>} _KeywordsAreComplete */ + /** @typedef {Assert>} _AlphabetIsComplete */ + // `eof` is not a member of the alphabet, so a second end marker cannot + // be encoded rather than merely going unused — and `encode` would + // reject the name outright. Checked at the type level because that is + // where it is decidable: `includes('eof')` does not even compile + // against this element type. + /** @typedef {Assert, never>>} _EofIsNotAName */ + }, // The corpus that proved parity against the hand-written state machine, // kept as fixed expectations now that the state machine is gone. // diff --git a/fjs/djs/serializer/module.f.mjs b/fjs/djs/serializer/module.f.mjs index 0e8e0bc71..cecf5e460 100644 --- a/fjs/djs/serializer/module.f.mjs +++ b/fjs/djs/serializer/module.f.mjs @@ -10,6 +10,8 @@ * @import { Unknown, Object, _MapEntries } from '../types.ts' * @import { Fold } from '../../types/function/operator/types.ts' * @import { List } from '../../types/list/types.ts' + * @import { _RefCounter, _Refs } from './types.ts' + * @import { _KeySerialize, _RefLookup } from './private.ts' */ import { fold } from '../../types/list/module.f.mjs' @@ -23,10 +25,6 @@ import { assertNotNullish } from '../../asserts/module.f.mjs' export const undefinedSerialize = ['undefined'] -/** @typedef {readonly [number, number]} _RefCounter */ - -/** @typedef {ReadonlyMap} _Refs */ - /** * Returns the value's `RefCounter` only if it is *shared* (referenced more * than once) — otherwise `undefined`. Names the single predicate that drives @@ -39,13 +37,12 @@ const sharedRef = refs => v => { return rc !== undefined && rc[1] > 1 ? rc : undefined } -/** @typedef {{ - * readonly added: ReadonlySet - * readonly consts: List - * }} _GetConstsState */ - /** @type {(refs: _Refs) => (djs: Unknown) => List} */ const getConstants = refs => { + /** @typedef {{ + * readonly added: ReadonlySet + * readonly consts: List + * }} _GetConstsState */ const shared = sharedRef(refs) /** @type {Fold} */ const checkSelf = djs => state => { @@ -82,24 +79,9 @@ const getConstants = refs => { /** @type {(kv: readonly [string, Unknown]) => Unknown} */ const entryValue = kv => kv[1] -/** - * A pre-hook consulted before each value's default serialization. - * Returning a non-null list short-circuits the default path; this is how - * `serializeWithConst` substitutes repeated values with `c` references. - * @typedef {(value: Unknown) => List | null} _RefLookup - */ - /** @type {_RefLookup} */ const noRef = () => null -/** - * How one output format spells a property key. The two formats disagree about - * exactly one key, `__proto__`, so the spelling is a parameter of - * `buildSerialize` rather than a property of the shared JSON helper. - * - * @typedef {(key: string) => List} _KeySerialize - */ - const protoKey = '__proto__' /** diff --git a/fjs/djs/serializer/private.ts b/fjs/djs/serializer/private.ts new file mode 100644 index 000000000..ee8cfb80e --- /dev/null +++ b/fjs/djs/serializer/private.ts @@ -0,0 +1,20 @@ +/** + * Implementation-private types for the DJS serializer. + */ + +import type { List } from '../../types/list/types.ts' +import type { Unknown } from '../types.ts' + +/** + * A pre-hook consulted before each value's default serialization. + * Returning a non-null list short-circuits the default path; this is how + * `serializeWithConst` substitutes repeated values with `c` references. + */ +export type _RefLookup = (value: Unknown) => List | null + +/** + * How one output format spells a property key. The two formats disagree about + * exactly one key, `__proto__`, so the spelling is a parameter of + * `buildSerialize` rather than a property of the shared JSON helper. + */ +export type _KeySerialize = (key: string) => List diff --git a/fjs/djs/serializer/types.ts b/fjs/djs/serializer/types.ts new file mode 100644 index 000000000..4ee485353 --- /dev/null +++ b/fjs/djs/serializer/types.ts @@ -0,0 +1,12 @@ +/** + * Type-level API for `fjs/djs/serializer/module.f.mjs`: the reference-count + * map `countRefs` produces and `stringify` hoists `const`s from. + */ + +import type { Unknown } from '../types.ts' + +/** A value's `const` index and how many times the value is referenced. */ +export type _RefCounter = readonly [number, number] + +/** Every value of a graph, mapped to its {@link _RefCounter}. */ +export type _Refs = ReadonlyMap diff --git a/fjs/djs/tokenizer/module.f.mjs b/fjs/djs/tokenizer/module.f.mjs index 1d628563f..ee9675352 100644 --- a/fjs/djs/tokenizer/module.f.mjs +++ b/fjs/djs/tokenizer/module.f.mjs @@ -11,18 +11,9 @@ * } from '../../bnf/descent/types.ts' * @import { DataRule, Rule } from '../../bnf/types.ts' * @import { - * BigIntToken, - * CommentToken, - * EofToken, - * ErrorToken, - * IdToken, * JsToken, * JsTokenWithMetadata, - * NewLineToken, - * NumberToken, - * StringToken, * TokenMetadata, - * WhitespaceToken, * } from '../../js/tokenizer/types.ts' * @import { CodePoint } from '../../text/utf16/types.ts' * @import { StateScan } from '../../types/function/operator/types.ts' @@ -30,6 +21,13 @@ * @import { DjsToken, DjsTokenWithMetadata } from './types.ts' * @import { TriviaKind } from '../../js/tokenizer/types.ts' * @import { Nullable } from '../../types/nullable/types.ts' + * @import { + * _DjsScanState, + * _FlatToken, + * _StringDecodeState, + * _Token, + * _TokenScanState, + * } from './private.ts' */ import { assert, assertEq } from '../../asserts/module.f.mjs' @@ -314,13 +312,6 @@ const metadataScan = (cp, metadata) => [[[cp, metadata]], advanceMetadata(cp)(me /** @type {(path: string) => (cp: readonly number[]) => readonly CodePointMeta[]} */ const codePointsWithMetadata = path => cp => toArray(flat(stateScan(metadataScan)({ path, line: 1, column: 1 })(cp))) -// tag, the metadata of the token's first code point, and its code points. -/** @typedef {[string, TokenMetadata, readonly number[]]} _Token */ - -/** @typedef {string | CodePointMeta} _FlatToken */ - -/** @typedef {[string, TokenMetadata | null, List]} _TokenScanState */ - /** * The grammar tag of a trivia code point, as the kind `mergeTrivia` speaks in; * `null` for every other tag. @@ -396,12 +387,6 @@ const filterFunc = tk => { */ const unwrapHexDigitValue = mapUnwrap(hexDigitValue) -/** @typedef { - * | { readonly kind: 'normal' } - * | { readonly kind: 'escape' } - * | { readonly kind: 'unicode', readonly acc: number, readonly count: number } - * } _StringDecodeState */ - /** @type {StateScan>} */ const stringDecodeScan = (cp, state) => { switch (state.kind) { @@ -592,8 +577,6 @@ export const tokenizeJs = input => path => { return withMetadata([{ token: { kind: 'eof' }, metadata: finalMetadata }]) } -/** @typedef {{ readonly kind: 'def' | '-' }} _DjsScanState */ - /** @type {(input: JsToken) => List} */ const mapDjsToken = input => { switch (input.kind) { diff --git a/fjs/djs/tokenizer/private.ts b/fjs/djs/tokenizer/private.ts new file mode 100644 index 000000000..b2d184327 --- /dev/null +++ b/fjs/djs/tokenizer/private.ts @@ -0,0 +1,25 @@ +/** + * Implementation-private types for the DJS tokenizer. + */ + +import type { CodePointMeta } from '../../bnf/descent/types.ts' +import type { TokenMetadata } from '../../js/tokenizer/types.ts' +import type { List } from '../../types/list/types.ts' + +/** A tag, the metadata of the token's first code point, and its code points. */ +export type _Token = [string, TokenMetadata, readonly number[]] + +/** One item of a flattened match: a tag, or a code point with its metadata. */ +export type _FlatToken = string | CodePointMeta + +/** A token being accumulated: its tag, start metadata, and code points so far. */ +export type _TokenScanState = [string, TokenMetadata | null, List] + +/** Where a string-literal decode is: plain text, after `\`, or inside `\uXXXX`. */ +export type _StringDecodeState = + | { readonly kind: 'normal' } + | { readonly kind: 'escape' } + | { readonly kind: 'unicode', readonly acc: number, readonly count: number } + +/** Whether the previous JS token was a bare `-` awaiting a number to negate. */ +export type _DjsScanState = { readonly kind: 'def' | '-' } diff --git a/fjs/djs/types.ts b/fjs/djs/types.ts index 1ba892335..0a7845fd1 100644 --- a/fjs/djs/types.ts +++ b/fjs/djs/types.ts @@ -12,6 +12,7 @@ import type { } from '../media/json/types.ts' import type { Assert } from '../asserts/types.ts' import type { Equal } from '../types/ts/types.ts' +import type { ReadFile, Write, WriteFile } from '../effects/node/types.ts' export type Object = { readonly[k in string]?: Unknown } @@ -35,3 +36,6 @@ type _Unknown = Assert>> * extended JSON instantiate, at DJS's leaf set. */ export type _MapEntries = TreeMapEntries + +/** The effect operations `compile` performs: file I/O and error output. */ +export type _CompileOp = ReadFile | WriteFile | Write diff --git a/fjs/edag/amnesia/module.f.mjs b/fjs/edag/amnesia/module.f.mjs index 8cac12387..4fe59c341 100644 --- a/fjs/edag/amnesia/module.f.mjs +++ b/fjs/edag/amnesia/module.f.mjs @@ -37,15 +37,11 @@ const o2 = (/**@type {(a: any, b: any) => unknown}*/o) => o2lazy((a, b) => o(a, b())) -/** @typedef {(c: Context, e: Op1) => unknown} _Func1 */ - const o1 = (/**@type {(a: any) => unknown}*/o) => - /**@type {_Func1}*/ + /**@type {(c: Context, e: Op1) => unknown}*/ (c, [, a]) => o(vm(c)(a)) -/** @typedef {(_: Exp) => unknown} _Eval */ - /** Both ways of being nullish, which is what every optional step guards. */ /** @type {(v: unknown) => boolean} */ const nullish = v => v === undefined || v === null @@ -56,7 +52,7 @@ const nullish = v => v === undefined || v === null * collects with `(...args)`. Passed as a single argument instead, the callee's * `['args']` would be `[[a, b]]`. * - * @type {(f: _Eval, e: Exp) => readonly any[]} + * @type {(f: (_: Exp) => unknown, e: Exp) => readonly any[]} */ const argsOf = (f, e) => /**@type {any}*/(f(e)) @@ -69,7 +65,7 @@ const argsOf = (f, e) => /**@type {any}*/(f(e)) * method would then silently succeed on the wrapper instead of throwing: * `((a.at)(0))(0)` returned `Array.prototype.at`. * - * @type {(f: _Eval, v: unknown, e: Exp) => unknown} + * @type {(f: (_: Exp) => unknown, v: unknown, e: Exp) => unknown} */ const callValue = (f, v, e) => /**@type {any}*/(v)(...argsOf(f, e)) @@ -90,7 +86,7 @@ const callValue = (f, v, e) => /**@type {any}*/(v)(...argsOf(f, e)) * would put every argument list ahead of the property read, and every test * here would still pass. * - * @type {(f: _Eval, obj: any, prop: any, e: Exp) => unknown} + * @type {(f: (_: Exp) => unknown, obj: any, prop: any, e: Exp) => unknown} */ const callProperty = (f, obj, prop, e) => obj[prop](...argsOf(f, e)) @@ -106,7 +102,7 @@ const callProperty = (f, obj, prop, e) => obj[prop](...argsOf(f, e)) * every step is `[tag, operand, continuation]`, and a `|!()` is reachable * through `|.` steps from either — `(a?.(...b).c)(...d)` is exactly that. * - * @type {(f: _Eval, k: OptionLambda | OptionPropertyLambda) => unknown} + * @type {(f: (_: Exp) => unknown, k: OptionLambda | OptionPropertyLambda) => unknown} */ const skip = (f, k) => { if (k === null) { return undefined } @@ -120,7 +116,7 @@ const skip = (f, k) => { * step leaves. Nothing here can short-circuit: the two productions are a call * that stays in the region and a property access that hands on a receiver. * - * @type {(f: _Eval, v: unknown, k: OptionLambda) => unknown} + * @type {(f: (_: Exp) => unknown, v: unknown, k: OptionLambda) => unknown} */ const optionLambda = (f, v, k) => { if (k === null) { return v } @@ -141,7 +137,7 @@ const optionLambda = (f, v, k) => { * `obj[prop]` is read once per step, twice only where the guard has to see * the value before the call is made. * - * @type {(f: _Eval, obj: any, prop: any, k: OptionPropertyLambda) => unknown} + * @type {(f: (_: Exp) => unknown, obj: any, prop: any, k: OptionPropertyLambda) => unknown} */ const optionPropertyLambda = (f, obj, prop, k) => { if (k === null) { return obj[prop] } @@ -164,7 +160,7 @@ const optionPropertyLambda = (f, obj, prop, k) => { * node's value, since `optionLambda` has no `|!()` of its own — but the walk * still goes through `skip`, which reaches one through a `|.`. * - * @type {(f: _Eval, obj: any, prop: any, k: PropertyLambda) => unknown} + * @type {(f: (_: Exp) => unknown, obj: any, prop: any, k: PropertyLambda) => unknown} */ const propertyLambda = (f, obj, prop, k) => { if (k === null) { return obj[prop] } diff --git a/fjs/edag/amnesia/proof.f.mjs b/fjs/edag/amnesia/proof.f.mjs index 26d625e08..9fde723ef 100644 --- a/fjs/edag/amnesia/proof.f.mjs +++ b/fjs/edag/amnesia/proof.f.mjs @@ -17,15 +17,6 @@ import { assert, assertEq, assertStructurallySame } from '../../asserts/module.f.mjs' import { vm } from './module.f.mjs' -// `TagMap` exists so a dispatcher generic over `K` sees one handler -// signature; these pin the tag -> node-tuple correlation it is built on, -// including the tags whose node kinds are not `op1`/`op2`. -/** @typedef {Assert, Op2>>} _PlusIsOp2 */ -/** @typedef {Assert, Op1>>} _NegIsOp1 */ -/** @typedef {Assert, ExpArray>>} _BracketsIsArray */ -/** @typedef {Assert, Call>>} _CallIsCall */ -/** @typedef {Assert, Dot>>} _DotIsDot */ - /** @type {Context} */ const context = { frame: { x: 1 }, args: [10, 20] } @@ -96,6 +87,16 @@ const methods = ['{}', [ const constMethods = ['=>', ['[]', []], methods] export const proof = { + // `TagMap` exists so a dispatcher generic over `K` sees one handler + // signature; these pin the tag -> node-tuple correlation it is built on, + // including the tags whose node kinds are not `op1`/`op2`. + tagMap: () => { + /** @typedef {Assert, Op2>>} _PlusIsOp2 */ + /** @typedef {Assert, Op1>>} _NegIsOp1 */ + /** @typedef {Assert, ExpArray>>} _BracketsIsArray */ + /** @typedef {Assert, Call>>} _CallIsCall */ + /** @typedef {Assert, Dot>>} _DotIsDot */ + }, // The non-`Array` side of `vm`'s only branch: a primitive is its own // value, returned without ever reaching `map`. primitive: () => { diff --git a/fjs/edag/module.f.mjs b/fjs/edag/module.f.mjs index f742bb826..114378d95 100644 --- a/fjs/edag/module.f.mjs +++ b/fjs/edag/module.f.mjs @@ -1,34 +1,7 @@ /** * @module * - * @import { Assert } from '../asserts/types.ts' - * @import { Check, Check3 } from '../rtti/ts/types.ts' - * @import { - * Array, - * Exp, - * Primitive, - * Property, - * NumberCast, - * Object, - * PropertyLambda, - * OptionLambda, - * OptionPropertyLambda, - * Call, - * Dot, - * OptionDot, - * OptionCall, - * Comma, - * Op2Id, - * Op2, - * Op1Id, - * Op1, - * Op0Id, - * Op0, - * Spread, - * Items, - * Properties, - * Exps, - * } from './types.ts' + * @import { Exp, OptionLambda, OptionPropertyLambda } from './types.ts' * @import { Phantom } from '../types/phantom/types.ts' */ @@ -79,7 +52,7 @@ import { * typeof op0, * ]} */ -const _exp = () => (['or', +export const _exp = () => (['or', primitive, array, object, @@ -96,8 +69,6 @@ const _exp = () => (['or', /** @type {Phantom} */ export const exp = _exp -/** @typedef {Assert>} _ExpAssert */ - // Primitive /** @@ -110,14 +81,10 @@ export const exp = _exp */ export const primitive = or(null, boolean, number, string, bigint) -/** @typedef {Assert>} _Primitive */ - // Exps export const exps = rttiArray(exp) -/** @typedef {Assert>} _Exps */ - // Spread /** @@ -131,15 +98,11 @@ export const exps = rttiArray(exp) */ export const spread = /** @type {const} */ (['...', exp]) -/** @typedef {Assert>} _Spread */ - // Items /** An array element: a plain `exp`, or a `spread` splicing another array in. */ export const items = or(exp, spread) -/** @typedef {Assert>} _Items */ - // Array /** @@ -150,8 +113,6 @@ export const items = or(exp, spread) */ export const array = /** @type {const} */ (['[]', rttiArray(items)]) -/** @typedef {Assert>} _Array */ - // Property /** @@ -167,15 +128,11 @@ export const array = /** @type {const} */ (['[]', rttiArray(items)]) */ export const property = /** @type {const} */ ([':', exp, exp]) -/** @typedef {Assert>} _Property */ - // Properties /** An object entry: a plain `property`, or a `spread` splicing another object in. */ export const properties = or(property, spread) -/** @typedef {Assert>} _Properties */ - // Object — same nesting as `array` above, one position further in /** @@ -203,8 +160,6 @@ export const properties = or(property, spread) */ export const object = /** @type {const} */ (['{}', rttiArray(properties)]) -/** @typedef {Assert>} _Object */ - // Number /** @@ -214,10 +169,6 @@ export const object = /** @type {const} */ (['{}', rttiArray(properties)]) */ export const numberCast = /** @type {const} */ (['Number', exp]) -/** - * @typedef {Assert>} _NumberCast - */ - // Index /** @@ -299,7 +250,7 @@ export const index = or(numberCast, string, number) * readonly['|.', typeof index, typeof optionPropertyLambda], * ]} */ -const _optionLambda = () => (['or', +export const _optionLambda = () => (['or', null, /** @type {const} */ (['|()', exp, optionLambda]), /** @type {const} */ (['|.', index, optionPropertyLambda]), @@ -308,10 +259,6 @@ const _optionLambda = () => (['or', /** @type {Phantom} */ export const optionLambda = _optionLambda -/** - * @typedef {Assert>} _OptionLambda - */ - /** * The continuation of a property step **inside** an open region — both bits * live, so this is the state with every production. @@ -339,7 +286,7 @@ export const optionLambda = _optionLambda * readonly['|!()', typeof exp, null], * ]} */ -const _optionPropertyLambda = () => (['or', +export const _optionPropertyLambda = () => (['or', null, /** @type {const} */ (['|()', exp, optionLambda]), /** @type {const} */ (['|.', index, optionPropertyLambda]), @@ -350,10 +297,6 @@ const _optionPropertyLambda = () => (['or', /** @type {Phantom} */ export const optionPropertyLambda = _optionPropertyLambda -/** - * @typedef {Assert>} _OptionPropertyLambda - */ - /** * The continuation of a `dot` — a receiver is live and no region is open. * @@ -377,10 +320,6 @@ export const propertyLambda = or( /** @type {const} */ (['|?.()', exp, optionLambda]), ) -/** - * @typedef {Assert>} _PropertyLambda - */ - // Call /** @@ -399,8 +338,6 @@ export const propertyLambda = or( */ export const call = /** @type {const} */ (['()', exp, exp]) -/** @typedef {Assert>} _Call */ - // Dot /** @@ -421,8 +358,6 @@ export const call = /** @type {const} */ (['()', exp, exp]) */ export const dot = /** @type {const} */ (['.', exp, index, propertyLambda]) -/** @typedef {Assert>} _Dot */ - // Option Dot /** @@ -447,8 +382,6 @@ export const dot = /** @type {const} */ (['.', exp, index, propertyLambda]) */ export const optionDot = /** @type {const} */ (['?.', exp, index, optionPropertyLambda]) -/** @typedef {Assert>} _OptionDot */ - // Option Call /** @@ -464,8 +397,6 @@ export const optionDot = /** @type {const} */ (['?.', exp, index, optionProperty */ export const optionCall = /** @type {const} */ (['?.()', exp, exp, optionLambda]) -/** @typedef {Assert>} _OptionCall */ - // Comma /** @@ -484,10 +415,6 @@ export const optionCall = /** @type {const} */ (['?.()', exp, exp, optionLambda] */ export const comma = /** @type {const} */ ([',', exps]) -/** - * @typedef {Assert>} _Comma - */ - // No-Args Operations /** @@ -500,12 +427,8 @@ export const comma = /** @type {const} */ ([',', exps]) */ export const op0Id = or('undefined', 'args', 'frame') -/** @typedef {Assert>} _Op0Id */ - export const op0 = /** @type {const} */ ([op0Id]) -/** @typedef {Assert>} _Op0 */ - // Unary Operations /** @@ -514,12 +437,8 @@ export const op0 = /** @type {const} */ ([op0Id]) */ export const op1Id = or('String', 'Number', 'neg', '!', '~') -/** @typedef {Assert>} _Op1Id */ - export const op1 = /** @type {const} */ ([op1Id, exp]) -/** @typedef {Assert>} _Op1 */ - // Binary Operations /** @@ -552,8 +471,4 @@ export const op2Id = or( '&&', '||', '??' ) -/** @typedef {Assert>} _Op2Id */ - export const op2 = /** @type {const} */ ([op2Id, exp, exp]) - -/** @typedef {Assert>} _Op2 */ diff --git a/fjs/edag/proof.f.mjs b/fjs/edag/proof.f.mjs index 90a92faa1..b41b23e50 100644 --- a/fjs/edag/proof.f.mjs +++ b/fjs/edag/proof.f.mjs @@ -10,9 +10,58 @@ * which `comma` is now the sole route to; it pins the operand array's * element schema, and claims nothing about what a `,` means. * + * @import { Assert } from '../asserts/types.ts' * @import { ValidationError } from '../rtti/common/types.ts' - * @import { Unknown } from '../rtti/ts/types.ts' + * @import { Check, Check3, Unknown } from '../rtti/ts/types.ts' * @import { StringMap } from '../types/object/types.ts' + * @import { + * _exp, + * _optionLambda, + * _optionPropertyLambda, + * array, + * call, + * comma, + * dot, + * exps, + * items, + * numberCast, + * object, + * op0, + * op1, + * op2, + * optionCall, + * optionDot, + * primitive, + * properties, + * property, + * spread, + * } from './module.f.mjs' + * @import { + * Array, + * Call, + * Comma, + * Dot, + * Exp, + * Exps, + * Items, + * NumberCast, + * Object, + * Op0, + * Op0Id, + * Op1, + * Op1Id, + * Op2, + * Op2Id, + * OptionCall, + * OptionDot, + * OptionLambda, + * OptionPropertyLambda, + * Primitive, + * Properties, + * Property, + * PropertyLambda, + * Spread, + * } from './types.ts' */ import { validate } from '../rtti/validate/module.f.mjs' @@ -99,6 +148,37 @@ const op2Ids = /** @type {const} */ ([ const desugarOptionalAt = o => o !== null && o !== undefined ? o.at : undefined export const proof = { + /** + * Each RTTI constant in `./module.f.mjs` matches its declared type in + * `./types.ts`. These are compile-time checks; the function body only has + * to exist so the typedefs have a local scope. + */ + consistency: () => { + /** @typedef {Assert>} _ExpAssert */ + /** @typedef {Assert>} _Primitive */ + /** @typedef {Assert>} _Exps */ + /** @typedef {Assert>} _Spread */ + /** @typedef {Assert>} _Items */ + /** @typedef {Assert>} _Array */ + /** @typedef {Assert>} _Property */ + /** @typedef {Assert>} _Properties */ + /** @typedef {Assert>} _Object */ + /** @typedef {Assert>} _NumberCast */ + /** @typedef {Assert>} _OptionLambda */ + /** @typedef {Assert>} _OptionPropertyLambda */ + /** @typedef {Assert>} _PropertyLambda */ + /** @typedef {Assert>} _Call */ + /** @typedef {Assert>} _Dot */ + /** @typedef {Assert>} _OptionDot */ + /** @typedef {Assert>} _OptionCall */ + /** @typedef {Assert>} _Comma */ + /** @typedef {Assert>} _Op0Id */ + /** @typedef {Assert>} _Op0 */ + /** @typedef {Assert>} _Op1Id */ + /** @typedef {Assert>} _Op1 */ + /** @typedef {Assert>} _Op2Id */ + /** @typedef {Assert>} _Op2 */ + }, primitive: { ok: () => { assertOk(v(null)) diff --git a/fjs/effects/memory/proof.f.mjs b/fjs/effects/memory/proof.f.mjs index 05a9aec19..9038e2dd0 100644 --- a/fjs/effects/memory/proof.f.mjs +++ b/fjs/effects/memory/proof.f.mjs @@ -13,16 +13,14 @@ import { } from './module.f.mjs' /** - * @typedef {{ + * @type {{ * readonly next: number, * readonly values: { readonly [key: string]: unknown }, - * }} _MemoryState + * }} */ - -/** @type {_MemoryState} */ const initial = { next: 0, values: {} } -/** @type {MemOperationMap} */ +/** @type {MemOperationMap} */ const mock = { memCreate: value => state => { const id = `k${state.next}` diff --git a/fjs/effects/memory/todo/sync-interpreter-owner.md b/fjs/effects/memory/todo/sync-interpreter-owner.md index 38a70c8d9..726bf1294 100644 --- a/fjs/effects/memory/todo/sync-interpreter-owner.md +++ b/fjs/effects/memory/todo/sync-interpreter-owner.md @@ -35,12 +35,20 @@ now exist in two variants for no reason. ### Proposal -`fjs/effects/memory` exports the sync interpreter next to the constructors: +`fjs/effects/memory` exports the sync interpreter next to the constructors, +with `MemoryState` in the module's `types.ts` (authored `.mjs` carries no +file-scope `@typedef`): + +```ts +// types.ts +export type MemoryState = { + readonly next: number + readonly values: { readonly [k: string]: unknown } +} +``` ```js -/** @typedef {{ readonly next: number, - * readonly values: { readonly [k: string]: unknown } }} MemoryState */ - +// module.f.mjs — `@import { MemoryState } from './types.ts'` in the header /** @type {MemoryState} */ export const memoryInitial = { next: 0, values: {} } diff --git a/fjs/effects/node/memory/module.mjs b/fjs/effects/node/memory/module.mjs index 6aa3ab29e..b4445dd10 100644 --- a/fjs/effects/node/memory/module.mjs +++ b/fjs/effects/node/memory/module.mjs @@ -3,9 +3,8 @@ * * @module * - * @import { Effect, ToAsyncOperationMap } from '../../types.ts' - * @import { Result } from '../../../types/result/types.ts' - * @import { Key, MemOp } from '../../memory/types.ts' + * @import { Key } from '../../memory/types.ts' + * @import { MemoryOperationMap, MemoryRun, Uuid } from './types.ts' */ import { randomUUID } from 'node:crypto' @@ -13,10 +12,6 @@ import { asyncRun } from '../../module.mjs' import { ok } from '../../../types/result/module.f.mjs' import { asBase, asNominal } from '../../memory/module.f.mjs' -/** @typedef {ToAsyncOperationMap} MemoryOperationMap */ - -/** @typedef {() => string} Uuid */ - /** @type {(id: string) => Error} */ const missingKey = id => new Error(`memory key not found: ${id}`) @@ -55,11 +50,6 @@ export const memoryOperationMap = (uuid = randomUUID) => { } } -/** - * An {@link asyncRun} runner over {@link MemOp}: an effect in, its `Result` out. - * @typedef {(effect: Effect) => Promise>} MemoryRun - */ - /** * Creates a runner owning a fresh memory store. Every effect passed to the * *same* runner shares that store; a new runner starts empty. diff --git a/fjs/effects/node/memory/proof.mjs b/fjs/effects/node/memory/proof.mjs index 6453027d6..2a8e2d413 100644 --- a/fjs/effects/node/memory/proof.mjs +++ b/fjs/effects/node/memory/proof.mjs @@ -4,7 +4,7 @@ * @module * * @import { Key } from '../../memory/types.ts' - * @import { Uuid } from './module.mjs' + * @import { Uuid } from './types.ts' */ import { errorSummary } from '../module.f.mjs' diff --git a/fjs/effects/node/memory/types.ts b/fjs/effects/node/memory/types.ts new file mode 100644 index 000000000..f58f1f21d --- /dev/null +++ b/fjs/effects/node/memory/types.ts @@ -0,0 +1,17 @@ +/** + * Types for the Node.js memory-effect interpreter. + */ + +import type { Effect, ToAsyncOperationMap } from '../../types.ts' +import type { Result } from '../../../types/result/types.ts' +import type { MemOp } from '../../memory/types.ts' + +export type MemoryOperationMap = ToAsyncOperationMap + +export type Uuid = () => string + +/** + * An `asyncRun` (`../../module.mjs`) runner over {@link MemOp}: an effect in, + * its `Result` out. + */ +export type MemoryRun = (effect: Effect) => Promise> diff --git a/fjs/effects/node/module.mjs b/fjs/effects/node/module.mjs index 9bd740c1a..41022718c 100644 --- a/fjs/effects/node/module.mjs +++ b/fjs/effects/node/module.mjs @@ -13,9 +13,9 @@ * @module * * @import { Effect } from '../types.ts' - * @import { IoResult, Server as EffectServer, Headers, Module, NodeOp, RequestListener as Erl, NodeProgram, NodeProgramOptions, WriteConsoles, TestContext, TestFn, } from './types.ts' + * @import { IoResult, Server as EffectServer, Module, NodeOp, RequestListener as Erl, NodeProgram, NodeProgramOptions, WriteConsoles, TestContext, TestFn, } from './types.ts' + * @import { _Readable, _RequestListener, _Server, _ServerResponse } from './private.ts' * @import { Result } from '../../types/result/types.ts' - * @import { StringMap } from '../../types/object/types.ts' * @import { Nullable } from '../../types/nullable/types.ts' */ @@ -40,40 +40,6 @@ import { asyncTryCatch, tryCatch } from '../../types/result/module.mjs' import { fromVec, listToVec, toVec } from '../../types/uint8array/module.f.mjs' import { maxLengthBytes } from '../../types/bit_vec/module.f.mjs' -/** The one thing this runner does with the socket a `connect` event hands it. - * - * @typedef {{ readonly end: (data: string) => void }} _Socket - */ - -/** - * @typedef {{ - * readonly listen: (port: number, host: string) => void, - * readonly once: (event: string, f: (e: unknown) => void) => void, - * on(event: string, f: (req: unknown, socket: _Socket) => void): void, - * readonly removeListener: (event: string, f: (e: unknown) => void) => void, - * }} _Server - */ - -/** @typedef {AsyncIterable} _Readable */ - -/** - * @typedef {_Readable & { - * readonly method: string, - * readonly url: string, - * readonly headers: Headers, - * }} _IncomingMessage - */ - -/** - * @typedef {{ - * readonly writeHead: (status: number, headers: StringMap) => _ServerResponse, - * readonly end: (body: Uint8Array) => void, - * readonly headersSent: boolean, - * }} _ServerResponse - */ - -/** @typedef {(req: _IncomingMessage, res: _ServerResponse) => Promise} _RequestListener */ - /** * Narrowed structural view of `node:http`'s `createServer`. The official types * declare `method`/`url` optional and header values as @@ -83,8 +49,6 @@ import { maxLengthBytes } from '../../types/bit_vec/module.f.mjs' */ const createServer = http.createServer -/** @typedef {(effect: Effect) => Promise>} _EffectToPromise */ - /** * Performs host IO, reporting a thrown failure as an {@link IoResult} error. * @@ -355,7 +319,7 @@ const randomMax = Number(1n << 32n) const { randomInt } = crypto -/** @type {_EffectToPromise} */ +/** @type {(effect: Effect) => Promise>} */ const runNodeEffect = asyncRun({ ...memoryOperationMap(), all: async (...effects) => ok(await Promise.all(effects.map(runNodeEffect))), @@ -553,9 +517,7 @@ const inlineTest = async (name, { expectFailure }, fn) => { /** @type {TestContext} */ const inlineContext = { test: inlineTest } -/** @typedef {(name: string, fn: () => Promise) => Promise} _FrameworkRegister */ - -/** @type {(register: _FrameworkRegister) => TestContext} */ +/** @type {(register: (name: string, fn: () => Promise) => Promise) => TestContext} */ const wrapInlineTest = register => ({ test: (name, opts, fn) => register(name, () => inlineTest(name, opts, fn)) }) diff --git a/fjs/effects/node/private.ts b/fjs/effects/node/private.ts new file mode 100644 index 000000000..bc88d25af --- /dev/null +++ b/fjs/effects/node/private.ts @@ -0,0 +1,36 @@ +/** + * Implementation-private types for the Node.js effect runner: the narrowed + * structural views of `node:http` objects the runner interprets HTTP + * operations against. + */ + +import type { StringMap } from '../../types/object/types.ts' +import type { Headers } from './types.ts' + +/** The one thing the runner does with the socket a `connect` event hands it. */ +export type _Socket = { + readonly end: (data: string) => void +} + +export type _Server = { + readonly listen: (port: number, host: string) => void + readonly once: (event: string, f: (e: unknown) => void) => void + on(event: string, f: (req: unknown, socket: _Socket) => void): void + readonly removeListener: (event: string, f: (e: unknown) => void) => void +} + +export type _Readable = AsyncIterable + +export type _IncomingMessage = _Readable & { + readonly method: string + readonly url: string + readonly headers: Headers +} + +export type _ServerResponse = { + readonly writeHead: (status: number, headers: StringMap) => _ServerResponse + readonly end: (body: Uint8Array) => void + readonly headersSent: boolean +} + +export type _RequestListener = (req: _IncomingMessage, res: _ServerResponse) => Promise diff --git a/fjs/effects/proof.f.mjs b/fjs/effects/proof.f.mjs index 102ad602c..0f095b347 100644 --- a/fjs/effects/proof.f.mjs +++ b/fjs/effects/proof.f.mjs @@ -1,6 +1,8 @@ /** - * @import { Effect, Func, Operation } from './types.ts' + * @import { Assert } from '../asserts/types.ts' + * @import { Effect, Func, NotImplemented, Operation } from './types.ts' * @import { Result } from '../types/result/types.ts' + * @import { Equal } from '../types/ts/types.ts' */ import { @@ -35,10 +37,9 @@ const assertPure = (e, expected) => { * `Operation` requires a `Result` return, so a runner always has somewhere to * answer `error(notImplemented)` — and that requirement is what lets an effect * carry its error channel in the type rather than inside an opaque payload. - * @typedef {readonly['add', (a: number, b: number) => Result]} _AddOp + * @type {(command: 'add') => (a: number, b: number) => + * Effect Result], number, string>} */ - -/** @type {(command: 'add') => (a: number, b: number) => Effect<_AddOp, number, string>} */ const doAdd = do_ const next = match({ @@ -51,10 +52,9 @@ const next = match({ * nothing stops it naming a member `map` inherits from `Object.prototype` * rather than an own handler. `match` must refuse those, and this type is how a * proof says so without an `as` cast. - * @typedef {readonly[string, (a: number) => Result]} _AnyOp + * @type {(command: string) => (a: number) => + * Effect Result], number, string>} */ - -/** @type {(command: string) => (a: number) => Effect<_AnyOp, number, string>} */ const doAny = do_ const anyNext = match({ add: (/** @type {number} */ a) => ok(a + 1) }) @@ -78,21 +78,15 @@ const anyPartial = partialMatch( * A fallible operation, spelled the way every operation is spelled: the * `Result` is in the command's declared return type, so `do_` already builds an * `Effect` and the runner's handler already answers with `ok` / `error`. - * @typedef {readonly['div', (a: number, b: number) => Result]} _DivOp + * @type {Func Result]>} */ +const div = do_('div') /** * A second operation, so a chain can join two of them and the operation sets * union. - * @typedef {readonly['neg', (a: number) => Result]} _NegOp + * @type {Func Result]>} */ - -/** @typedef {_DivOp | _NegOp} _Op */ - -/** @type {Func<_DivOp>} */ -const div = do_('div') - -/** @type {Func<_NegOp>} */ const neg = do_('neg') const nextArith = match({ @@ -104,7 +98,10 @@ const nextArith = match({ /** * Runs an effect to completion against the two operations above — `asyncRun`'s * loop without the `await`, which is all a synchronous runner is. - * @type {(e: Effect<_Op, T, E>) => Result} + * @type {(e: Effect< + * | readonly['div', (a: number, b: number) => Result] + * | readonly['neg', (a: number) => Result], + * T, E>) => Result} */ const run = e => { let current = e @@ -176,6 +173,86 @@ const checked = v => { const show = e => `${e}` export const proof = { + /** + * Every combinator's signature, pinned at a concrete instantiation. These + * verify `./module.f.mjs`, so they live here rather than in `./types.ts`; + * the widening rules the layer itself rests on stay there. + */ + signatures: () => { + /** @typedef {readonly['add', (a: number, b: number) => Result]} _AddOp */ + /** @typedef {readonly['mul', (a: number, b: number) => Result]} _MulOp */ + // `step` unions the operation sets and the errors, and replaces the + // success type with the continuation's. + /** + * @typedef {Assert>, + * Effect<_AddOp | _MulOp, string, NotImplemented | string>>>} _StepSig + */ + // `catchStep` mirrors it: the success channel is the union of the + // preserved value and the recovery's, and the error type is the + // recovery's alone — `never` when every error is handled. + /** + * @typedef {Assert>, + * Effect<_AddOp | _MulOp, string | number, never>>>} _CatchStepSig + */ + // `resultStep` consumes both branches, so it replaces both channels and + // unions only the operation sets. It is the layer's primitive — `step` + // and `catchStep` are it with a tag test in front — so this signature + // is the one the other two are derived from rather than a third + // variant beside them. + /** + * @typedef {Assert>, + * Effect<_AddOp | _MulOp, string, string>>>} _ResultStepSig + */ + // `mapStep` widens nothing: a pure projection issues no commands and + // cannot fail, so only the success type changes. + /** + * @typedef {Assert>, + * Effect<_AddOp, string, NotImplemented>>>} _MapStepSig + */ + // `resultMapStep` is the both-branches projection, so it replaces the + // error channel as well — this is the assert that says a caller may + // discard errors here, which is the whole reason the name is separate + // from `mapStep`'s. + /** + * @typedef {Assert>>, + * Effect<_AddOp, string, string>>>} _ResultMapStepSig + */ + // ...and a projection that only ever answers `ok` empties the channel + // rather than acquiring one. Reading the two halves off `f`'s concrete + // return type is what makes this line pass; matching `Result` + // directly would infer `F` from the `ok` payload. + /** + * @typedef {Assert>, + * Effect<_AddOp, string, never>>>} _ResultMapStepEmpties + */ + // `unwrapStep` panics on the error branch, so what it hands back is an + // effect whose channel is empty — `never` earned by the throw rather + // than asserted. + /** + * @typedef {Assert>, + * Effect<_AddOp, number, never>>>} _UnwrapStepSig + */ + // ...and the renderer it takes is what stops that panic from quietly + // growing. A summary written for one channel is *not* usable where a + // wider channel's summary is required — parameters are contravariant — + // so adding a failure upstream breaks the site that chose to panic + // instead of silently enlarging what it crashes on. This is the assert + // that makes the argument checkable: were it to pass, `unwrapStep` + // would be back to absorbing anything. + /** @template E @typedef {Parameters>[1]} _Summary */ + /** + * @typedef {Assert extends _Summary ? true : false, + * false>>} _UnwrapStepPinsItsChannel + */ + }, runPure: { ok: () => { assertPure(pure(ok(5)), ok(5)) @@ -321,6 +398,7 @@ export const proof = { overFailedDo: () => { // `todo` never returns, so it pins none of the continuation's type // parameters; the annotation supplies the operation set `run` needs. + /** @typedef {readonly['div', (a: number, b: number) => Result]} _DivOp */ /** @type {Effect<_DivOp, never, string>} */ const e = step(div(1, 0), todo) assertError(run(e), 'div by zero') @@ -328,7 +406,9 @@ export const proof = { // Adjacent links performing different commands: the operation sets // union, so one runner interprets the whole chain. joinsOperations: () => { - /** @type {Effect<_Op, number, string>} */ + /** @typedef {readonly['div', (a: number, b: number) => Result]} _DivOp */ + /** @typedef {readonly['neg', (a: number) => Result]} _NegOp */ + /** @type {Effect<_DivOp | _NegOp, number, string>} */ const e = step(div(6, 3), neg) assertOk(run(e), -2) }, diff --git a/fjs/effects/types.ts b/fjs/effects/types.ts index 4164e2db9..8ea1f3a5b 100644 --- a/fjs/effects/types.ts +++ b/fjs/effects/types.ts @@ -8,9 +8,6 @@ import type { Ok, Error, Result } from '../types/result/types.ts' import type { Assert } from '../asserts/types.ts' import type { Unknown as Json } from '../media/json/types.ts' import type { Equal } from '../types/ts/types.ts' -import type { - catchStep, mapStep, resultMapStep, resultStep, step, unwrapStep, -} from './module.f.mjs' /** * A command name paired with the signature a runner implements it at. @@ -280,11 +277,13 @@ export type Func = // ── The contract, checked rather than merely declared ──────────────────────── // -// Every combinator's signature is pinned below at a concrete instantiation, -// together with the widening rules the layer rests on. The union rules are the -// subtle part: a "simplification" that unified an error channel instead of -// unioning it would still compile at the definition and fail only at some -// future call site, so each rule is written down as a check here. +// The widening rules the layer rests on are pinned below at a concrete +// instantiation. The union rules are the subtle part: a "simplification" that +// unified an error channel instead of unioning it would still compile at the +// definition and fail only at some future call site, so each rule is written +// down as a check here. The combinator signatures themselves verify +// `./module.f.mjs`, so those asserts live downstream in `./proof.f.mjs`'s +// `signatures` proof rather than here. /** @see {@link _WidensOperations} — a second command to widen the op-set with. */ type _AddOp = readonly['add', (a: number, b: number) => Result] @@ -322,66 +321,6 @@ type _WidensOk = Assert<_Add extends Effect<_AddOp, number | string, NotImplemen // composing with one that requests further commands. type _WidensOperations = Assert<_Add extends Effect<_AddOp | _MulOp, number, NotImplemented> ? true : false> -// `step` unions the operation sets and the errors, and replaces the success -// type with the continuation's. -type _StepSig = Assert>, - Effect<_AddOp | _MulOp, string, NotImplemented | string>>> - -// `catchStep` mirrors it: the success channel is the union of the preserved -// value and the recovery's, and the error type is the recovery's alone — -// `never` when every error is handled. -type _CatchStepSig = Assert>, - Effect<_AddOp | _MulOp, string | number, never>>> - -// `resultStep` consumes both branches, so it replaces both channels and unions -// only the operation sets. It is the layer's primitive — `step` and `catchStep` -// are it with a tag test in front — so this signature is the one the other two -// are derived from rather than a third variant beside them. -type _ResultStepSig = Assert>, - Effect<_AddOp | _MulOp, string, string>>> - -// `mapStep` widens nothing: a pure projection issues no commands and cannot -// fail, so only the success type changes. -type _MapStepSig = Assert>, - Effect<_AddOp, string, NotImplemented>>> - -// `resultMapStep` is the both-branches projection, so it replaces the error -// channel as well — this is the assert that says a caller may discard errors -// here, which is the whole reason the name is separate from `mapStep`'s. -type _ResultMapStepSig = Assert>>, - Effect<_AddOp, string, string>>> - -// ...and a projection that only ever answers `ok` empties the channel rather -// than acquiring one. Reading the two halves off `f`'s concrete return type is -// what makes this line pass; matching `Result` directly would infer `F` -// from the `ok` payload. -type _ResultMapStepEmpties = Assert>, - Effect<_AddOp, string, never>>> - -// `unwrapStep` panics on the error branch, so what it hands back is an effect -// whose channel is empty — `never` earned by the throw rather than asserted. -type _UnwrapStepSig = Assert>, - Effect<_AddOp, number, never>>> - -// ...and the renderer it takes is what stops that panic from quietly growing. -// A summary written for one channel is *not* usable where a wider channel's -// summary is required — parameters are contravariant — so adding a failure -// upstream breaks the site that chose to panic instead of silently enlarging -// what it crashes on. This is the assert that makes the argument checkable: -// were it to pass, `unwrapStep` would be back to absorbing anything. -type _Summary = Parameters>[1] - -type _UnwrapStepPinsItsChannel = Assert extends _Summary ? true : false, - false>> - // `NotImplemented` is JSON data. This is the assert the "command name only" // rule exists to keep true: an operation's payload may hold functions, and // admitting one here would fail this line. The dependency is type-only and diff --git a/fjs/emergent_testing/browser.mjs b/fjs/emergent_testing/browser.mjs index 6a7e82f6d..1963cc7e3 100644 --- a/fjs/emergent_testing/browser.mjs +++ b/fjs/emergent_testing/browser.mjs @@ -15,7 +15,7 @@ * * @module * - * @import { TestResult, _TestAndPath } from './types.ts' + * @import { BrowserTestReport, TestResult, _BrowserImporter, _BrowserTestResult, _TestAndPath } from './types.ts' * @import { Result } from '../types/result/types.ts' */ @@ -59,21 +59,6 @@ const errorDetails = error => { return [fallback, fallback] } -/** - * A leaf's outcome as the page reports it: the shared {@link TestResult} — - * identity, status and duration, decided by `testResult` rather than here — plus - * the two fields only a browser report needs. - * - * `message` and `stack` are the browser's own part, and stay outside the shared - * record for the reason `TestResult` gives: describing a thrown value needs the - * value, a serializable report cannot carry one, and `fjs t` describes it - * differently because it is writing to a terminal rather than to a wire. - * - * @typedef {TestResult & { readonly message?: string, readonly stack?: string }} _BrowserTestResult - */ - -/** @typedef {{ readonly status: string, readonly browser: string, readonly totals: { readonly tests: number, readonly passed: number, readonly failed: number }, readonly duration: number, readonly results: readonly _BrowserTestResult[] }} BrowserTestReport */ - /** * A failure of a whole module — one that will not link, or whose `proof` export * cannot be enumerated. It does not go through `testResult`, and that is the @@ -278,11 +263,7 @@ export const runBrowserProofs = (modules, result = () => undefined) => { return completed.then(results => reportOf(performance.now() - start, results)) } -/** @typedef {(source: string) => Promise<{ readonly proof?: unknown }>} _BrowserImporter */ -/** @typedef {{ readonly status: 'loaded', readonly source: string, readonly proof: unknown } | { readonly status: 'error', readonly source: string, readonly error: unknown }} _LoadedModule */ -/** @typedef {Window & { fjsBrowserTestReport?: Promise }} _TestWindow */ - -/** @type {(root: Element) => _TestWindow | null} */ +/** @type {(root: Element) => (Window & { fjsBrowserTestReport?: Promise }) | null} */ const viewOf = root => root.ownerDocument.defaultView /** @@ -310,6 +291,7 @@ const publish = (root, report) => { * @type {(root: Element, sources: readonly string[], importer: _BrowserImporter) => Promise} */ export const startBrowserTestSources = (root, sources, importer) => { + /** @typedef {{ readonly status: 'loaded', readonly source: string, readonly proof: unknown } | { readonly status: 'error', readonly source: string, readonly error: unknown }} _LoadedModule */ const start = performance.now() setState(root, 'loading') let loaded = 0 diff --git a/fjs/emergent_testing/browser/proof.mjs b/fjs/emergent_testing/browser/proof.mjs index 887f9d150..6b542ec61 100644 --- a/fjs/emergent_testing/browser/proof.mjs +++ b/fjs/emergent_testing/browser/proof.mjs @@ -14,90 +14,101 @@ import { renderBrowserReport, runBrowserProofs, startBrowserTests, startBrowserT import { fmtImport, testResult } from '../module.f.mjs' import { error, ok } from '../../types/result/module.f.mjs' -/** @typedef {{ readonly tag: string, attributes: ReadonlyMap, readonly ownerDocument: _Document, textContent: string, children: readonly _Element[], readonly setAttribute: (name: string, value: string) => void, readonly removeAttribute: (name: string) => void, readonly querySelector: (selector: string) => _Element | null, readonly replaceChildren: (...nodes: readonly _Element[]) => void, readonly append: (node: _Element) => void }} _Element */ -/** @typedef {{ defaultView: _View | null, readonly createElement: (tag: string) => _Element }} _Document */ -/** @typedef {{ events: readonly CustomEvent[], readonly dispatchEvent: (event: Event) => boolean, fjsBrowserTestReport?: Promise }} _View */ +/** + * Builds the DOM stand-in the proofs drive the runner with. A single factory + * rather than file-scope helpers so the mutually recursive + * element/document/view types can stay function-local. + */ +const dom = () => { + /** @typedef {{ readonly tag: string, attributes: ReadonlyMap, readonly ownerDocument: _Document, textContent: string, children: readonly _Element[], readonly setAttribute: (name: string, value: string) => void, readonly removeAttribute: (name: string) => void, readonly querySelector: (selector: string) => _Element | null, readonly replaceChildren: (...nodes: readonly _Element[]) => void, readonly append: (node: _Element) => void }} _Element */ + /** @typedef {{ defaultView: _View | null, readonly createElement: (tag: string) => _Element }} _Document */ + /** @typedef {{ events: readonly CustomEvent[], readonly dispatchEvent: (event: Event) => boolean, fjsBrowserTestReport?: Promise }} _View */ -/** @type {(node: _Element, name: string) => _Element | null} */ -const find = (node, name) => - node.attributes.has(name) - ? node - : node.children.reduce( - (/** @type {_Element | null} */ acc, child) => acc ?? find(child, name), - null) + /** @type {(node: _Element, name: string) => _Element | null} */ + const find = (node, name) => + node.attributes.has(name) + ? node + : node.children.reduce( + (/** @type {_Element | null} */ acc, child) => acc ?? find(child, name), + null) -/** @type {(document: _Document, tag: string, attributes: readonly string[], states: string[]) => _Element} */ -const element = (document, tag, attributes, states) => { - /** @type {_Element} */ - const self = { - tag, - attributes: new Map(attributes.map(name => [name, ''])), - ownerDocument: document, - textContent: '', - children: [], - setAttribute: (name, value) => { - if (name === 'data-state') { states.push(value) } - self.attributes = new Map([...self.attributes, [name, value]]) - }, - removeAttribute: name => { - self.attributes = new Map([...self.attributes].filter(([key]) => key !== name)) - }, - // The runner only ever queries an attribute selector of `[name]` form. - querySelector: selector => self.children.reduce( - (/** @type {_Element | null} */ acc, child) => - acc ?? find(child, selector.slice(1, -1)), - null), - replaceChildren: (...nodes) => { self.children = nodes }, - append: node => { self.children = [...self.children, node] }, + /** @type {(document: _Document, tag: string, attributes: readonly string[], states: string[]) => _Element} */ + const element = (document, tag, attributes, states) => { + /** @type {_Element} */ + const self = { + tag, + attributes: new Map(attributes.map(name => [name, ''])), + ownerDocument: document, + textContent: '', + children: [], + setAttribute: (name, value) => { + if (name === 'data-state') { states.push(value) } + self.attributes = new Map([...self.attributes, [name, value]]) + }, + removeAttribute: name => { + self.attributes = new Map([...self.attributes].filter(([key]) => key !== name)) + }, + // The runner only ever queries an attribute selector of `[name]` form. + querySelector: selector => self.children.reduce( + (/** @type {_Element | null} */ acc, child) => + acc ?? find(child, selector.slice(1, -1)), + null), + replaceChildren: (...nodes) => { self.children = nodes }, + append: node => { self.children = [...self.children, node] }, + } + return self } - return self -} -/** - * Builds what the generated page gives the runner: a root carrying the summary - * paragraph and the result list. `states` records every `data-state` written, - * so a proof can check the whole progression and not just its last step. - * - * @type {(withView?: boolean) => { readonly root: Element, readonly summary: _Element, readonly results: _Element, readonly runButton: _Element, readonly view: _View, readonly states: readonly string[] }} - */ -const page = (withView = true) => { - /** @type {string[]} */ - const states = [] - /** @type {_Document} */ - const document = { - defaultView: null, - createElement: tag => element(document, tag, [], states), - } - /** @type {_View} */ - const view = { - events: [], - dispatchEvent: event => { - view.events = [...view.events, /** @type {CustomEvent} */ (event)] - return true - }, - } - if (withView) { document.defaultView = view } - const root = element(document, 'main', ['data-browser-tests'], states) - root.replaceChildren( - element(document, 'p', ['data-test-summary'], states), - element(document, 'button', ['data-test-run'], states), - element(document, 'ol', ['data-test-results'], states)) - return { - root: /** @type {Element} */ (/** @type {unknown} */ (root)), - summary: assertNotNullish(root.querySelector('[data-test-summary]')), - results: assertNotNullish(root.querySelector('[data-test-results]')), - runButton: assertNotNullish(root.querySelector('[data-test-run]')), - view, - states, + /** + * Builds what the generated page gives the runner: a root carrying the summary + * paragraph and the result list. `states` records every `data-state` written, + * so a proof can check the whole progression and not just its last step. + * + * @type {(withView?: boolean) => { readonly root: Element, readonly summary: _Element, readonly results: _Element, readonly runButton: _Element, readonly view: _View, readonly states: readonly string[] }} + */ + const page = (withView = true) => { + /** @type {string[]} */ + const states = [] + /** @type {_Document} */ + const document = { + defaultView: null, + createElement: tag => element(document, tag, [], states), + } + /** @type {_View} */ + const view = { + events: [], + dispatchEvent: event => { + view.events = [...view.events, /** @type {CustomEvent} */ (event)] + return true + }, + } + if (withView) { document.defaultView = view } + const root = element(document, 'main', ['data-browser-tests'], states) + root.replaceChildren( + element(document, 'p', ['data-test-summary'], states), + element(document, 'button', ['data-test-run'], states), + element(document, 'ol', ['data-test-results'], states)) + return { + root: /** @type {Element} */ (/** @type {unknown} */ (root)), + summary: assertNotNullish(root.querySelector('[data-test-summary]')), + results: assertNotNullish(root.querySelector('[data-test-results]')), + runButton: assertNotNullish(root.querySelector('[data-test-run]')), + view, + states, + } } + + /** @type {(element: _Element) => readonly (string | undefined)[]} */ + const statuses = element => element.children.map(child => child.attributes.get('data-status')) + + return { element, page, statuses } } +const { element, page, statuses } = dom() + /** @type {(proof: unknown) => ReturnType} */ const run = proof => runBrowserProofs([['proof', proof]]) -/** @type {(element: _Element) => readonly (string | undefined)[]} */ -const statuses = element => element.children.map(child => child.attributes.get('data-status')) - export const proof = { namedThrow: async () => { const named = { throw: () => { throw 'expected' } }.throw @@ -466,7 +477,7 @@ export const proof = { // than throwing. /** @type {string[]} */ const states = [] - /** @type {_Document} */ + /** @type {Parameters[0]} */ const document = { defaultView: null, createElement: tag => element(document, tag, [], states), diff --git a/fjs/emergent_testing/proof.f.mjs b/fjs/emergent_testing/proof.f.mjs index 55c1b7694..fb4ca11e2 100644 --- a/fjs/emergent_testing/proof.f.mjs +++ b/fjs/emergent_testing/proof.f.mjs @@ -39,21 +39,17 @@ const event = or( /** @type {const} */ (['summary', rttiNumber, rttiNumber, rttiNumber]), ) -/** @typedef {Ts} _Event */ - const parseEvent = rttiParse(event) -/** @typedef {Reporter} _TestReporter */ - -/** @type {(e: _Event) => Effect} */ +/** @type {(e: Ts) => Effect} */ const writeEvent = e => log(JSON.stringify(e)) -/** @type {(stdout: string) => readonly _Event[]} */ +/** @type {(stdout: string) => readonly Ts[]} */ const parseEvents = stdout => stdout === '' ? [] : stdout.trimEnd().split('\n') .map(line => unwrap(parseEvent(unwrap(parseJson(line))))) -/** @type {() => _TestReporter} */ +/** @type {() => Reporter} */ const makeReporter = () => ({ // The leaf-landed event arrives with the shared `TestResult` already // built, so what this writes — and what the proofs below assert on — is @@ -77,7 +73,7 @@ const fail0 = () => ({ result: /** @type {const} */ (['error', 'oops']), duratio /** @type {() => unknown} */ const ok1 = () => ({ result: /** @type {const} */ (['ok', undefined]), duration: 1 }) -/** @type {(dir: Record, initCwd?: string) => readonly [readonly _Event[], number]} */ +/** @type {(dir: Record, initCwd?: string) => readonly [readonly Ts[], number]} */ const run = (dir, initCwd = '.') => { const reporter = makeReporter() const state = { ...emptyState, root: dir } @@ -305,8 +301,6 @@ export const githubReporterOutput = () => { ) } -/** @typedef {All | Import | Readdir | Sandbox | Write} _FailOps */ - // A reporter that cannot write neither panics nor reports success. The failed // `result` line short-circuits its own test, leaves `allOk` as the first error, // skips the summary, and reaches the program tail — which answers exit `1`. @@ -315,6 +309,7 @@ export const githubReporterOutput = () => { // the failure on, so the exit code rather than a message is what is observable: // a run that cannot say anything at all still says it failed. export const reporterWriteFailure = () => { + /** @typedef {All | Import | Readdir | Sandbox | Write} _FailOps */ /** @type {RunInstance<_FailOps, undefined>} */ let runner runner = mockRun(/** @type {Parameters>[0]} */ ({ @@ -339,23 +334,6 @@ export const reporterWriteFailure = () => { assertEq(exitCode(code), 1) } -/** @typedef {readonly string[]} _RegisterMockState */ - -/** @typedef {Test | All | Await} _RegisterMockOps */ - -/** @typedef {RunInstance<_RegisterMockOps, _RegisterMockState>} _RegisterRunner */ - -/** - * The `test` op body for a `registerModule` mock; `runner` is threaded in explicitly (rather than closed over) so it can recurse into sub-effects returned by `fn`. - * @typedef {( - * runner: _RegisterRunner, - * ctx: TestContext, - * name: string, - * expectFailure: boolean, - * fn: (t: TestContext) => Effect<_RegisterMockOps, void, never>, - * ) => (s: _RegisterMockState) => readonly [_RegisterMockState, OpResult]} _RegisterTestOp - */ - /** * A `TestContext` that is never invoked. Every mock runner below intercepts the * `test` *effect* and reads the context as data, so `test` here exists only to @@ -373,9 +351,23 @@ const registerNoopCtx = { test: (_n, _o, _f) => { throw 'registerNoopCtx is data * Builds a synchronous mock runner for `registerModule`'s `Test`/`All`/`Await` * effect operations. Only the `test` op varies between call sites (whether it * invokes the registered callback), so `all`/`await` are shared here. + * + * `testOp` is the `test` op body for a `registerModule` mock; `runner` is + * threaded in explicitly (rather than closed over) so it can recurse into + * sub-effects returned by `fn`. */ -/** @type {(testOp: _RegisterTestOp) => _RegisterRunner} */ +/** @type {(testOp: ( + * runner: RunInstance, + * ctx: TestContext, + * name: string, + * expectFailure: boolean, + * fn: (t: TestContext) => Effect, + * ) => (s: readonly string[]) => readonly [readonly string[], OpResult] + * ) => RunInstance} */ const makeRegisterRunner = testOp => { + /** @typedef {readonly string[]} _RegisterMockState */ + /** @typedef {Test | All | Await} _RegisterMockOps */ + /** @typedef {RunInstance<_RegisterMockOps, _RegisterMockState>} _RegisterRunner */ /** @type {_RegisterRunner} */ let runner runner = mockRun(/** @type {Parameters>[0]} */ ({ @@ -424,6 +416,9 @@ export const registerSuffixes = () => { // which is why `registerOne` ends in a `catchStep` that throws rather than in // a channel nobody reads. const registerBodyPanicsOnUndispatchableEffect = () => { + /** @typedef {readonly string[]} _RegisterMockState */ + /** @typedef {Test | All | Await} _RegisterMockOps */ + /** @typedef {RunInstance<_RegisterMockOps, _RegisterMockState>} _RegisterRunner */ /** @type {_RegisterRunner} */ let runner runner = mockRun(/** @type {Parameters>[0]} */ ({ @@ -500,6 +495,7 @@ export const registerEmptyModuleMap = () => { // so a swapped `engine` ternary or a deleted `inlineTestContext` branch // changes what's observed here, not just whether the line ran. export const registerSelectsContextAndStar = () => { + /** @typedef {Test | All | Await} _RegisterMockOps */ /** @type {TestContext} */ const nodeCtx = { test: todo } /** @type {TestContext} */ diff --git a/fjs/emergent_testing/types.ts b/fjs/emergent_testing/types.ts index 20ac2f6c9..5191ae631 100644 --- a/fjs/emergent_testing/types.ts +++ b/fjs/emergent_testing/types.ts @@ -96,6 +96,44 @@ export type TestResult = { readonly duration: number } +/** + * A leaf's outcome as the browser page reports it: the shared + * {@link TestResult} — identity, status and duration, decided by `testResult` + * rather than by the browser runner — plus the two fields only a browser + * report needs. + * + * `message` and `stack` are the browser's own part, and stay outside the shared + * record for the reason `TestResult` gives: describing a thrown value needs the + * value, a serializable report cannot carry one, and `fjs t` describes it + * differently because it is writing to a terminal rather than to a wire. + * + * @internal + */ +export type _BrowserTestResult = TestResult & { + readonly message?: string + readonly stack?: string +} + +/** The serializable report a browser test run resolves with. */ +export type BrowserTestReport = { + readonly status: string + readonly browser: string + readonly totals: { + readonly tests: number + readonly passed: number + readonly failed: number + } + readonly duration: number + readonly results: readonly _BrowserTestResult[] +} + +/** + * Loads one proof module by its source path for the browser runner. + * + * @internal + */ +export type _BrowserImporter = (source: string) => Promise<{ readonly proof?: unknown }> + /** * A run's outcome, folded from its leaf results: how many passed, how many * failed, and how long they took together. diff --git a/fjs/fsc/README.md b/fjs/fsc/README.md index 2ea737ab0..bd4eba3be 100644 --- a/fjs/fsc/README.md +++ b/fjs/fsc/README.md @@ -126,28 +126,28 @@ move, so a `module.f.mjs` is accompanied by a `proof.f.mjs`. Type-only APIs may remain in `types.ts`. Current FunctionalScript compiler support was never a condition for that rename. -#### Private JSDoc typedefs - -TypeScript declaration emit currently turns JSDoc `@typedef`s into exported type -aliases, including typedefs that exist only as implementation details. This is -tracked upstream by -[microsoft/TypeScript#46407](https://github.com/microsoft/TypeScript/issues/46407). - -Until JSDoc typedefs can be stripped with `@internal` and `stripInternal`, use a -leading `_` for implementation-only typedefs created during the migration: - -```js -/** @typedef {number} _Type */ -``` - -The underscore is an API contract, not declaration-level visibility. Generated -`.d.ts` / `.d.mts` may still contain `export type _Type = number`, but names that -begin with `_` are private FunctionalScript implementation details. Consumers -must not rely on those names directly, so renaming or removing a `_`-prefixed -alias is not a breaking change solely because TypeScript emitted it. The public -contract still governs transitive effects: if a public type depends on `_Type`, -changing `_Type` in a way that changes that public type's assignability is a -breaking change and requires the normal `**BREAKING CHANGES:**` treatment. +#### Private types + +Authored `.mjs` files carry no file-scope JSDoc `@typedef` — anywhere in the +repository (see the repository-wide rule in the root `AGENTS.md` and +`fjs/AGENTS.md` §3.2). A named type migrating out of a `.f.ts` therefore lands +in the sibling `types.ts` (when it is part of the public declaration closure), +in an optional sibling `private.ts` (implementation-private types outside that +closure), inline in the annotations that use it, or — for compile-time proof +types — function-local in a proof. + +Private types and private runtime constants keep a leading `_`, even when +linkage requires an export. The underscore is an API contract, not +declaration-level visibility: generated `.d.ts` / `.d.mts` may still contain +`export type _Type = number` (and, until the packaging stage of +[`../todo/separate-private-types.md`](../todo/separate-private-types.md) lands, +a generated `private.d.ts` still ships), but names that begin with `_` are +private FunctionalScript implementation details. Consumers must not rely on +those names directly, so renaming or removing a `_`-prefixed name is not a +breaking change solely because TypeScript emitted it. The public contract still +governs transitive effects: if a public type depends on `_Type`, changing +`_Type` in a way that changes that public type's assignability is a breaking +change and requires the normal `**BREAKING CHANGES:**` treatment. For example, suppose the generated declaration initially contains: @@ -176,18 +176,18 @@ export type Public = readonly [_Internal] The emitted private alias is still private, but the expanded public contract of `Public` changed from `readonly [number]` to `readonly [string]`. -Public JSDoc typedefs keep ordinary names without the `_` prefix. Which JSDoc -typedefs are public is an API design decision, not a mechanical restatement of -what the pre-migration `.f.ts` file happened to export: a helper that belongs to -the module's public vocabulary may be published under an ordinary name even -though its TypeScript alias was module-private, and a former export may become -`_` when it only ever described an implementation detail. Types intentionally -separated into `types.ts` use ordinary TypeScript source visibility instead of -this JSDoc-emission workaround. - -When upstream support is ready, replace this workaround with `@internal`; that -cleanup is tracked by -[`todo/blocked/jsdoc-typedef-strip-internal.md`](../../todo/blocked/jsdoc-typedef-strip-internal.md). +Public types keep ordinary names without the `_` prefix. Which types are public +is an API design decision, not a mechanical restatement of what the +pre-migration `.f.ts` file happened to export: a helper that belongs to the +module's public vocabulary may be published under an ordinary name even though +its TypeScript alias was module-private, and a former export may become `_` +when it only ever described an implementation detail. + +Removing shipped private declaration artifacts (`private.d.ts`) from the +package is the second stage of +[`../todo/separate-private-types.md`](../todo/separate-private-types.md); the +`_` contract itself is permanent, since `_` helpers in `types.ts` and exported +`_` constants keep shipping in emitted declarations regardless. When the last authored implementation/proof `.ts` / `.f.ts` file is gone, authored `types.ts` files may remain. The TypeScript runtime-emission pass is diff --git a/fjs/fsc/module.f.mjs b/fjs/fsc/module.f.mjs index 920f85777..c06067065 100644 --- a/fjs/fsc/module.f.mjs +++ b/fjs/fsc/module.f.mjs @@ -6,6 +6,7 @@ * @import { RangeMapArray, RangeMerge } from '../types/range_map/types.ts' * @import { List } from '../types/list/types.ts' * @import { Range } from '../types/range/types.ts' + * @import { _CreateToResult, _Result, _State, _ToResult } from './types.ts' */ import { strictEqual } from '../types/function/operator/module.f.mjs' @@ -18,14 +19,6 @@ import { assertEq } from '../asserts/module.f.mjs' const fromCharCode = String.fromCharCode -/** @typedef {readonly [readonly string[], _ToResult]} _Result */ - -/** @typedef {(codePoint: number) => _Result} _ToResult */ - -/** @template T @typedef {(state: T) => _ToResult} _CreateToResult */ - -/** @template T @typedef {RangeMapArray<_CreateToResult>} _State */ - /** @type {_ToResult} */ const unexpectedSymbol = codePoint => [[`unexpected symbol ${codePoint}`], unexpectedSymbol] diff --git a/fjs/fsc/types.ts b/fjs/fsc/types.ts new file mode 100644 index 000000000..052f96006 --- /dev/null +++ b/fjs/fsc/types.ts @@ -0,0 +1,14 @@ +/** + * Types for the FunctionalScript compile-workflow state machine. + */ + +import type { RangeMapArray } from '../types/range_map/types.ts' + +/** A step outcome: diagnostics so far, and the next code-point handler. */ +export type _Result = readonly [readonly string[], _ToResult] + +export type _ToResult = (codePoint: number) => _Result + +export type _CreateToResult = (state: T) => _ToResult + +export type _State = RangeMapArray<_CreateToResult> diff --git a/fjs/fsm/module.f.mjs b/fjs/fsm/module.f.mjs index 383cdadc1..709077c2f 100644 --- a/fjs/fsm/module.f.mjs +++ b/fjs/fsm/module.f.mjs @@ -9,6 +9,7 @@ * @import { SortedSet } from '../types/sorted_set/types.ts' * @import { RangeMap, Properties, RangeMapArray, Entry } from '../types/range_map/types.ts' * @import { Fold } from '../types/function/operator/types.ts' + * @import { Grammar, _Dfa, _Rule } from './types.ts' */ import { equal, isEmpty, fold, map, toArray, foldScan, empty as emptyList } from '../types/list/module.f.mjs' @@ -21,12 +22,6 @@ import { compose } from '../types/function/module.f.mjs' import { at } from '../types/object/module.f.mjs' import { cmp } from '../types/string/module.f.mjs' -/** @typedef {readonly [string, ByteSet, string]} _Rule */ - -/** @typedef {List<_Rule>} Grammar */ - -/** @typedef {StringMap>} _Dfa */ - /** * The byte set of an inclusive ASCII character range, written as the two diff --git a/fjs/fsm/proof.f.mjs b/fjs/fsm/proof.f.mjs index 5f8e47882..7d8a2e56a 100644 --- a/fjs/fsm/proof.f.mjs +++ b/fjs/fsm/proof.f.mjs @@ -1,5 +1,5 @@ /** - * @import { Grammar } from './module.f.mjs' + * @import { Grammar } from './types.ts' * @import { ByteSet } from '../types/byte_set/types.ts' */ diff --git a/fjs/fsm/types.ts b/fjs/fsm/types.ts new file mode 100644 index 000000000..d8425f832 --- /dev/null +++ b/fjs/fsm/types.ts @@ -0,0 +1,16 @@ +/** + * Types for the finite-state-machine grammar and its compiled DFA. + */ + +import type { List } from '../types/list/types.ts' +import type { ByteSet } from '../types/byte_set/types.ts' +import type { StringMap } from '../types/object/types.ts' +import type { RangeMapArray } from '../types/range_map/types.ts' + +/** A transition rule: source state, input bytes, target state. */ +export type _Rule = readonly [string, ByteSet, string] + +export type Grammar = List<_Rule> + +/** The compiled automaton: each state's byte-range transition table. */ +export type _Dfa = StringMap> diff --git a/fjs/js/keywords/module.f.mjs b/fjs/js/keywords/module.f.mjs index b6b1892b8..5f18c639d 100644 --- a/fjs/js/keywords/module.f.mjs +++ b/fjs/js/keywords/module.f.mjs @@ -8,9 +8,6 @@ * keeping a copy, so the sets cannot drift apart. * * @module - * - * @import { Assert } from '../../asserts/types.ts' - * @import { Equal } from '../../types/ts/types.ts' */ /** @@ -49,7 +46,7 @@ export const restrictedNames = /** @type {const} */ (['arguments', 'eval']) * JavaScript that FunctionalScript keeps as a literal keyword. * * The proof verifies this list is exactly the sorted union of the groups, - * and `_KeywordsPinned` ties the two type-level unions together. + * at runtime and at the type level. */ export const keywords = /** @type {const} */ ([ 'arguments', 'await', 'break', 'case', 'catch', 'class', 'const', @@ -61,12 +58,3 @@ export const keywords = /** @type {const} */ ([ 'undefined', 'var', 'void', 'while', 'with', 'yield', ]) -/** - * @typedef {Assert>} _KeywordsPinned - */ diff --git a/fjs/js/keywords/proof.f.mjs b/fjs/js/keywords/proof.f.mjs index ed4678c40..b3f089bfd 100644 --- a/fjs/js/keywords/proof.f.mjs +++ b/fjs/js/keywords/proof.f.mjs @@ -1,9 +1,23 @@ +/** + * @import { Assert } from '../../asserts/types.ts' + * @import { Equal } from '../../types/ts/types.ts' + */ + import { assertEq } from '../../asserts/module.f.mjs' import { keywords, reservedWords, restrictedNames, strictModeReservedWords } from './module.f.mjs' export const proof = { // `keywords` is exactly the sorted union of the groups plus `undefined` aggregate: () => { + /** + * @typedef {Assert>} _KeywordsPinned + */ /** @type {readonly string[]} */ const union = [...reservedWords, ...strictModeReservedWords, ...restrictedNames, 'undefined'] // the names are unique, so the comparator never sees an equal pair diff --git a/fjs/mcp/cas/module.f.mjs b/fjs/mcp/cas/module.f.mjs index 288c7251a..6465c430a 100644 --- a/fjs/mcp/cas/module.f.mjs +++ b/fjs/mcp/cas/module.f.mjs @@ -167,17 +167,15 @@ const toJson = stringify(identity) */ const detectDialect = detect([revisionDialect, lockDialect, noteDialect]) -/** @typedef {{ +/** + * Maps a media-type detector verdict to the `cas_get` wire metadata. + * + * @type {(uri: string) => (detected: { readonly length: bigint, readonly mime_type: string, readonly type: 'text' | 'base64' }) => { * readonly length: number * readonly mimeType: string * readonly type: 'text' | 'base64' * readonly uri: string - * }} _Meta */ - -/** - * Maps a media-type detector verdict to the `cas_get` wire metadata. - * - * @type {(uri: string) => (detected: { readonly length: bigint, readonly mime_type: string, readonly type: 'text' | 'base64' }) => _Meta} + * }} */ const toMeta = uri => ({ length, mime_type: mimeType, type }) => ({ length: Number(length), mimeType, type, uri }) diff --git a/fjs/media/html/module.f.mjs b/fjs/media/html/module.f.mjs index ffc2b360c..9ec960250 100644 --- a/fjs/media/html/module.f.mjs +++ b/fjs/media/html/module.f.mjs @@ -22,8 +22,6 @@ import { quotationMark, ampersand, lessThanSign, greaterThanSign } from '../../t const { fromCharCode } = String -/** @typedef {StringMap} _Attributes */ - /** * Void Elements * @@ -91,10 +89,10 @@ const rawMap = n => concat(mr(n)).replaceAll(' flat([[' ', name, '="'], escape(value), ['"']]) -/** @type {(a: _Attributes) => List} */ +/** @type {(a: StringMap) => List} */ const attributes = a => flatMap(attribute)(definedEntries(a)) -/** @type {(e: Element) => readonly [string, _Attributes, readonly Node[]]} */ +/** @type {(e: Element) => readonly [string, StringMap, readonly Node[]]} */ const parseElement = e => { const [tag, item1, ...list] = e return item1 === undefined ? diff --git a/fjs/media/json/schema/module.f.mjs b/fjs/media/json/schema/module.f.mjs index 7a08c770c..2f9a4a410 100644 --- a/fjs/media/json/schema/module.f.mjs +++ b/fjs/media/json/schema/module.f.mjs @@ -31,23 +31,38 @@ import { absentBit, cmp, toData, unitBit, unknown as top, withoutUnits } from '. import { unknown as jsonUnknown } from '../rtti/module.f.mjs' /** @type {() => readonly ['const', typeof unknownConst]} */ -const unknownThunk = () => ['const', unknownConst] +export const _unknownThunk = () => ['const', unknownConst] /** * rtti schema for a JSON Schema (draft 2020-12) document. - * @type {Phantom} - */ -export const unknown = unknownThunk - -/** - * Checked against the un-annotated thunk, so a wrong `_UnknownConst` above - * would be caught here instead of silently trusted via the `Phantom` lie. - * @typedef {Assert>} _UnknownCheck0 + * + * The `$out` half of the `Phantom` is hand-written. The `?` markers spell what + * `or(option, …)` says in the schema: every field may be absent, and `Ts<>` + * renders such a member optional with absence stripped from its type, so each + * field here is `?:` over the member's present part. JSON Schema objects only + * include the keywords they need. `$defs` is an *open* map — an absent entry + * types as `undefined`, so missing-reference handling cannot be skipped. The + * `consistency` proof checks this hand-written type against the un-annotated + * `_unknownThunk`, so a wrong field here is caught instead of silently trusted + * via the `Phantom` lie. + * + * @type {Phantom + * readonly $ref?: Ts + * readonly $defs?: Ts + * readonly type?: Ts + * readonly const?: Ts + * readonly not?: Ts + * readonly anyOf?: Ts + * readonly items?: Ts + * readonly prefixItems?: Ts + * readonly minItems?: Ts + * readonly properties?: Ts + * readonly required?: Ts + * readonly additionalProperties?: Ts + * }>} */ -/** @typedef {Assert>} _UnknownCheck1 */ - -/** A JSON Schema (draft 2020-12) document — the subset of keywords that `toJsonSchema` emits. */ -/** @typedef {Ts} Unknown */ +export const unknown = _unknownThunk // Every field may be **omitted** — a JSON Schema document carries only the // keywords it needs, and JSON has no `undefined` to hold in a present field — @@ -69,32 +84,6 @@ const unknownConst = /** @type {const} */ ({ additionalProperties: or(option, unknown), }) -/** - * Hand-written base type used as the `$out` annotation on `unknown`. - * - * The `?` markers spell what `or(option, …)` says in the schema: every field - * may be absent, and `Ts<>` renders such a member optional with absence - * stripped from its type, so each field here is `?:` over the member's - * present part. JSON Schema objects only include the keywords they need. - * `$defs` is an *open* map — an absent entry types as `undefined`, so - * missing-reference handling cannot be skipped. - * @typedef {{ - * readonly $schema?: Ts - * readonly $ref?: Ts - * readonly $defs?: Ts - * readonly type?: Ts - * readonly const?: Ts - * readonly not?: Ts - * readonly anyOf?: Ts - * readonly items?: Ts - * readonly prefixItems?: Ts - * readonly minItems?: Ts - * readonly properties?: Ts - * readonly required?: Ts - * readonly additionalProperties?: Ts - * }} _UnknownConst - */ - const nullBit = unitBit(null) const undefinedBit = unitBit(undefined) const falseBit = unitBit(false) @@ -125,14 +114,14 @@ const refEncode = name => { * own-property only, so a name inherited from `Object.prototype` * (`toString`, `constructor`, …) is still rejected. * - * @type {(rules: RuleSet) => (name: string) => Unknown} + * @type {(rules: RuleSet) => (name: string) => Ts} */ const refSchema = rules => name => { assert(at(name)(rules) !== null, `missing definition: ${name}`) return { $ref: `#/$defs/${refEncode(name)}` } } -/** @type {(rules: RuleSet) => (n: Node) => Unknown} */ +/** @type {(rules: RuleSet) => (n: Node) => Ts} */ const nodeSchema = rules => n => typeof n === 'string' ? refSchema(rules)(n) : unionSchema(rules)(n) @@ -142,20 +131,20 @@ const nodeSchema = rules => n => * * @template T * @param {KindSet | undefined} k - * @param {Unknown} whole - * @param {(v: T) => Unknown} item - * @returns {readonly Unknown[]} + * @param {Ts} whole + * @param {(v: T) => Ts} item + * @returns {readonly Ts[]} */ const kindSchemas = (k, whole, item) => k === undefined ? [] : k === true ? [whole] : k.map(item) -/** @type {(v: boolean | number | string | null) => Unknown} */ +/** @type {(v: boolean | number | string | null) => Ts} */ const constSchema = v => ({ const: v }) /** bigint consts are represented as numbers (lossy for |value| > MAX_SAFE_INTEGER) */ -/** @type {(v: bigint) => Unknown} */ +/** @type {(v: bigint) => Ts} */ const bigintConstSchema = v => ({ const: Number(v) }) /** @@ -163,7 +152,7 @@ const bigintConstSchema = v => ({ const: Number(v) }) * value is `undefined`, hence `{ "not": {} }` — and both boolean bits * together are the `boolean` type with no special-case rule. * - * @type {(bits: number) => readonly Unknown[]} + * @type {(bits: number) => readonly Ts[]} */ const unitSchemas = bits => [ ...((bits & nullBit) === 0 ? [] : [constSchema(null)]), @@ -197,7 +186,7 @@ const minLength = rules => prefix => * `undefined`. Both are the object side's `required` / * {@link stripUndefined} pair, one kind over. * - * @type {(rules: RuleSet) => (p: ArraySet) => Unknown} + * @type {(rules: RuleSet) => (p: ArraySet) => Ts} */ const arraySetSchema = rules => p => { const minItems = minLength(rules)(p.prefix) @@ -248,7 +237,7 @@ const stripUndefined = n => * the other keys unconstrained (lenient), matching rtti's open-struct * validation semantics. * - * @type {(rules: RuleSet) => (p: ObjectSet) => Unknown} + * @type {(rules: RuleSet) => (p: ObjectSet) => Ts} */ const objectSetSchema = rules => p => { const ents = definedEntries(p.props) @@ -273,7 +262,7 @@ const isTop = u => cmp([{}, u])([{}, top]) === 0 * it contributes no schema member, and `or(option, unknown)` is the * always-true `{}` like plain `unknown`. * - * @type {(rules: RuleSet) => (u: UnionSet) => Unknown} + * @type {(rules: RuleSet) => (u: UnionSet) => Ts} */ const unionSchema = rules => u0 => { const u = withoutUnits(absentBit)(u0) @@ -302,7 +291,7 @@ const unionSchema = rules => u0 => { * and are JSON Pointer-escaped, then percent-encoded, for the `$ref` * fragment. A reference naming a missing definition panics. * - * @type {(data: Data) => Unknown} + * @type {(data: Data) => Ts} */ export const dataToJsonSchema = ([rules, entry]) => { const ruleEntries = definedEntries(rules) @@ -341,6 +330,6 @@ export const dataToJsonSchema = ([rules, entry]) => { * duplicates collapse — so structurally different but equivalent thunk * schemas produce the same JSON Schema. * - * @type {(rtti: RttiType) => Unknown} + * @type {(rtti: RttiType) => Ts} */ export const toJsonSchema = rtti => dataToJsonSchema(toData(rtti)) diff --git a/fjs/media/json/schema/proof.f.mjs b/fjs/media/json/schema/proof.f.mjs index 9e5639902..557b5ee68 100644 --- a/fjs/media/json/schema/proof.f.mjs +++ b/fjs/media/json/schema/proof.f.mjs @@ -1,5 +1,7 @@ /** - * @import { Unknown } from './module.f.mjs' + * @import { Ts, Check } from '../../../rtti/ts/types.ts' + * @import { Assert } from '../../../asserts/types.ts' + * @import { _unknownThunk } from './module.f.mjs' * @import { Data } from '../../../rtti/data/types.ts' */ @@ -9,57 +11,42 @@ import { dataToJsonSchema, toJsonSchema, unknown as schemaUnknown } from './modu import { absentBit, unitBit } from '../../../rtti/data/module.f.mjs' import { assert, assertEq } from '../../../asserts/module.f.mjs' -/** @type {(v: Unknown) => string} */ +/** @type {(v: Ts) => string} */ const serialize = v => stringify(e => e)(v) -/** @type {(rtti: Parameters[0], expected: Unknown) => () => void} */ +/** @type {(rtti: Parameters[0], expected: Ts) => () => void} */ const eq = (rtti, expected) => () => { const result = serialize(toJsonSchema(rtti)) const exp = serialize(expected) assertEq(result, exp, [result, exp]) } -/** @type {(data: Data, expected: Unknown) => () => void} */ +/** @type {(data: Data, expected: Ts) => () => void} */ const eqData = (data, expected) => () => { const result = serialize(dataToJsonSchema(data)) const exp = serialize(expected) assertEq(result, exp, [result, exp]) } -/** A recursive list: `type _List = readonly _List[]`. */ -/** @typedef {() => readonly ['array', _List]} _List */ -/** @type {_List} */ -const list = () => ['array', list] - -/** Mutual recursion through a container. */ -/** @typedef {() => readonly ['or', typeof number, _Forest]} _Tree */ -/** @typedef {() => readonly ['array', _Tree]} _Forest */ -/** @type {_Tree} */ -const tree = () => ['or', number, forest] -/** @type {_Forest} */ -const forest = () => ['array', tree] - -/** The recursive revision lock schema. Its cycle closes through the - * anonymous `or` thunk, which becomes the (empty-string-named) rule. */ -/** @typedef {() => readonly ['record', () => readonly ['or', typeof string, _Lock]]} _Lock */ -/** @type {_Lock} */ -const lock = () => ['record', or(string, lock)] - -/** Self-recursive record. */ -/** @typedef {() => readonly ['record', _Rec]} _Rec */ -/** @type {_Rec} */ -const rec = () => ['record', rec] - const listRef = /** @type {const} */ ({ $ref: '#/$defs/list' }) const treeRef = /** @type {const} */ ({ $ref: '#/$defs/tree' }) -/** @type {Unknown} */ +/** @type {Ts} */ const listDef = { type: 'array', items: listRef } -/** @type {Unknown} */ +/** @type {Ts} */ const treeDef = { anyOf: [{ type: 'number' }, { type: 'array', items: treeRef }] } export const proof = { + /** + * The hand-written `$out` on `unknown` matches the real thunk — checked + * against the un-annotated `_unknownThunk`, so a wrong field there is + * caught instead of silently trusted via the `Phantom` lie. + */ + consistency: () => { + /** @typedef {Assert, typeof _unknownThunk>>} _UnknownCheck0 */ + /** @typedef {Assert, typeof schemaUnknown>>} _UnknownCheck1 */ + }, tag0: { boolean: eq(boolean, { type: 'boolean' }), number: eq(number, { type: 'number' }), @@ -258,39 +245,90 @@ export const proof = { }, }, recursion: { - selfList: eq(list, { ...listRef, $defs: { list: listDef } }), - mutualEntry: eq(tree, { ...treeRef, $defs: { tree: treeDef } }), - mutualInline: eq(forest, { type: 'array', items: treeRef, $defs: { tree: treeDef } }), - recursiveUnion: eq(or(number, list), { - anyOf: [{ type: 'number' }, { type: 'array', items: listRef }], - $defs: { list: listDef }, - }), - recursiveRecord: eq(rec, { - $ref: '#/$defs/rec', - $defs: { rec: { type: 'object', additionalProperties: { $ref: '#/$defs/rec' } } }, - }), - optionalRecursiveProperty: eq(/** @type {const} */ ({ p: or(option, list) }), { - type: 'object', - properties: { p: { type: 'array', items: listRef } }, - additionalProperties: { not: {} }, - $defs: { list: listDef }, - }), - revisionLock: eq(lock, { - type: 'object', - additionalProperties: { $ref: '#/$defs/' }, - $defs: { - '': { - anyOf: [ - { type: 'string' }, - { type: 'object', additionalProperties: { $ref: '#/$defs/' } }, - ], + selfList: () => { + /** A recursive list: `type _List = readonly _List[]`. */ + /** @typedef {() => readonly ['array', _List]} _List */ + /** @type {_List} */ + const list = () => ['array', list] + eq(list, { ...listRef, $defs: { list: listDef } })() + }, + mutualEntry: () => { + /** Mutual recursion through a container. */ + /** @typedef {() => readonly ['or', typeof number, _Forest]} _Tree */ + /** @typedef {() => readonly ['array', _Tree]} _Forest */ + /** @type {_Tree} */ + const tree = () => ['or', number, forest] + /** @type {_Forest} */ + const forest = () => ['array', tree] + eq(tree, { ...treeRef, $defs: { tree: treeDef } })() + }, + mutualInline: () => { + /** Mutual recursion through a container. */ + /** @typedef {() => readonly ['or', typeof number, _Forest]} _Tree */ + /** @typedef {() => readonly ['array', _Tree]} _Forest */ + /** @type {_Tree} */ + const tree = () => ['or', number, forest] + /** @type {_Forest} */ + const forest = () => ['array', tree] + eq(forest, { type: 'array', items: treeRef, $defs: { tree: treeDef } })() + }, + recursiveUnion: () => { + /** A recursive list: `type _List = readonly _List[]`. */ + /** @typedef {() => readonly ['array', _List]} _List */ + /** @type {_List} */ + const list = () => ['array', list] + eq(or(number, list), { + anyOf: [{ type: 'number' }, { type: 'array', items: listRef }], + $defs: { list: listDef }, + })() + }, + recursiveRecord: () => { + /** Self-recursive record. */ + /** @typedef {() => readonly ['record', _Rec]} _Rec */ + /** @type {_Rec} */ + const rec = () => ['record', rec] + eq(rec, { + $ref: '#/$defs/rec', + $defs: { rec: { type: 'object', additionalProperties: { $ref: '#/$defs/rec' } } }, + })() + }, + optionalRecursiveProperty: () => { + /** A recursive list: `type _List = readonly _List[]`. */ + /** @typedef {() => readonly ['array', _List]} _List */ + /** @type {_List} */ + const list = () => ['array', list] + eq(/** @type {const} */ ({ p: or(option, list) }), { + type: 'object', + properties: { p: { type: 'array', items: listRef } }, + additionalProperties: { not: {} }, + $defs: { list: listDef }, + })() + }, + revisionLock: () => { + /** + * The recursive revision lock schema. Its cycle closes through the + * anonymous `or` thunk, which becomes the (empty-string-named) rule. + */ + /** @typedef {() => readonly ['record', () => readonly ['or', typeof string, _Lock]]} _Lock */ + /** @type {_Lock} */ + const lock = () => ['record', or(string, lock)] + eq(lock, { + type: 'object', + additionalProperties: { $ref: '#/$defs/' }, + $defs: { + '': { + anyOf: [ + { type: 'string' }, + { type: 'object', additionalProperties: { $ref: '#/$defs/' } }, + ], + }, }, - }, - }), + })() + }, sharedNonRecursive: () => { // a shared, non-recursive definition is inlined at each use — no `$defs` const person = /** @type {const} */ ({ name: string }) - /** @type {Unknown} */ + /** @type {Ts} */ const personSchema = { type: 'object', properties: { name: { type: 'string' } }, diff --git a/fjs/media/revision/proof.f.mjs b/fjs/media/revision/proof.f.mjs index a0c4bad44..f53f23900 100644 --- a/fjs/media/revision/proof.f.mjs +++ b/fjs/media/revision/proof.f.mjs @@ -1,6 +1,9 @@ /** + * @import { Assert } from '../../asserts/types.ts' * @import { Object as JsonObject } from '../json/types.ts' - * @import { LockMap } from './types.ts' + * @import { Check } from '../../rtti/ts/types.ts' + * @import { LockField, LockMap } from './types.ts' + * @import { lock, lockField } from './module.f.mjs' */ import { assert, assertEq } from '../../asserts/module.f.mjs' @@ -34,6 +37,15 @@ const revisionOf = extra => ({ }) export const proof = { + /** + * The hand-written `LockMap`/`LockField` in `./types.ts` are pinned + * against the module's rtti schemas, so the two recursions cannot drift + * apart. + */ + consistency: () => { + /** @typedef {Assert>} _LockMap */ + /** @typedef {Assert>} _LockField */ + }, dialectAndMediaType: () => { assertEq(dialect, 'vnd.fjs.revision') assertEq(mediaType, 'application/vnd.fjs.revision+json') diff --git a/fjs/media/revision/types.ts b/fjs/media/revision/types.ts index f7371652b..cfaa6edc0 100644 --- a/fjs/media/revision/types.ts +++ b/fjs/media/revision/types.ts @@ -4,9 +4,10 @@ * `RevisionError`. * * `LockMap` is written by hand rather than derived, so that the recursion - * reads directly, and is then pinned against the module's rtti schema with - * `Assert>` — the same arrangement the JSON - * data model uses in [`../json/types.ts`](../json/types.ts). `LockSchema` is + * reads directly, and is then pinned against the module's rtti schema by the + * `consistency` proof in [`./proof.f.mjs`](./proof.f.mjs) — the same + * hand-written-plus-pinned arrangement the JSON data model uses in + * [`../json/types.ts`](../json/types.ts). `LockSchema` is * the schema side of the same recursion: `lock` cannot infer its own type * (a `const` may not reference itself in its own initializer), so it carries * this named annotation instead. @@ -14,11 +15,10 @@ * @module */ -import type { Assert } from '../../asserts/types.ts' -import type { Ts, Check } from '../../rtti/ts/types.ts' +import type { Ts } from '../../rtti/ts/types.ts' import type { String as RttiString } from '../../rtti/types.ts' import type { ValidationError } from '../../rtti/common/types.ts' -import type { lock, lockField, revisionSchema } from './module.f.mjs' +import type { revisionSchema } from './module.f.mjs' /** * A set of subject-to-snapshot bindings supplied to dependency resolvers. @@ -34,8 +34,6 @@ export type LockMap = { readonly[subject in string]?: string | LockMap } export type LockSchema = () => readonly['record', () => readonly['or', RttiString, LockSchema]] -type _LockMap = Assert> - /** * A revision's `lock` field: the bindings inline as a {@link LockMap}, or the * cbase32 hash of a `vnd.fjs.lock` blob (`fjs/media/lock`) holding one to @@ -48,8 +46,6 @@ export type LockField = string | LockMap export type LockFieldSchema = () => readonly['or', RttiString, LockSchema] -type _LockField = Assert> - /** The TypeScript type derived from `revisionSchema` — the single source of truth. */ export type Revision = Ts diff --git a/fjs/protocol/mcp/proof.f.mjs b/fjs/protocol/mcp/proof.f.mjs index 6776f2900..10c6f10dd 100644 --- a/fjs/protocol/mcp/proof.f.mjs +++ b/fjs/protocol/mcp/proof.f.mjs @@ -31,15 +31,13 @@ import { // ── Memory mock ──────────────────────────────────────────────────────────────── -/** @typedef {{ +/** @type {{ * readonly next: number * readonly values: { readonly [key: string]: unknown } - * }} _MemoryState */ - -/** @type {_MemoryState} */ + * }} */ const initial = { next: 0, values: {} } -/** @type {MemOperationMap} */ +/** @type {MemOperationMap} */ const mock = { memCreate: value => state => { const id = `k${state.next}` @@ -70,8 +68,7 @@ const configNoTools = { ...config, capabilities: {} } /** @type {McpConfig} */ const configTwoVersions = { ...config, protocolVersions: ['2025-06-18', '2024-11-05'] } -/** @typedef {never} _Op */ -/** @type {McpHandlers<_Op>} */ +/** @type {McpHandlers} */ const handlers = { // Echoes a received cursor as `nextCursor` so tests can observe pagination params. toolsList: (/** @type {ToolsListParams} */ p) => @@ -82,8 +79,6 @@ const handlers = { pureOk({ content: [{ type: 'text', text: 'hello' }] }), } -/** @typedef {readonly [unknown, McpSessionState]} _StepResult */ - // Run a memory effect against the mock and unwrap what it answered. The // channel stays generic because nothing here interprets it: a proof has nobody // to report a failure to, so an `error` is a panic and the tests read the `ok`. @@ -99,7 +94,7 @@ const asMemEffect = e => /** @type {Effect} */ (e) // Pairs the last step's response with the session state read back afterwards. // The response is still needed after the read, so it is carried forward in a // history rather than closed over by a nested continuation. -/** @type {(key: Key) => (e: Effect) => Effect} */ +/** @type {(key: Key) => (e: Effect) => Effect} */ const withState = key => e => { const read0 = historyStep(history(e), () => read(key)) // A history holds `ok` values, so `resp` is the response itself rather @@ -109,14 +104,14 @@ const withState = key => e => { } // Run one step from uninitializedState, return [response, newState]. -/** @type {(cfg: McpConfig) => (msg: Unknown) => _StepResult} */ +/** @type {(cfg: McpConfig) => (msg: Unknown) => readonly [unknown, McpSessionState]} */ const step1 = cfg => msg => runMem(asMemEffect(step( create(uninitializedState), key => withState(key)(mcpStep(cfg)(handlers)(key)(msg))))) // Run initialize then a second step, return [response, newState] of the second. -/** @type {(cfg: McpConfig) => (msg1: Unknown) => (msg2: Unknown) => _StepResult} */ +/** @type {(cfg: McpConfig) => (msg1: Unknown) => (msg2: Unknown) => readonly [unknown, McpSessionState]} */ const step2 = cfg => msg1 => msg2 => runMem(asMemEffect(step( create(uninitializedState), @@ -127,7 +122,7 @@ const step2 = cfg => msg1 => msg2 => }))) // Run initialize, notifications/initialized, then a third step; return [response, newState] of the third. -/** @type {(cfg: McpConfig) => (msg1: Unknown) => (msg2: Unknown) => (msg3: Unknown) => _StepResult} */ +/** @type {(cfg: McpConfig) => (msg1: Unknown) => (msg2: Unknown) => (msg3: Unknown) => readonly [unknown, McpSessionState]} */ const step3 = cfg => msg1 => msg2 => msg3 => runMem(asMemEffect(step( create(uninitializedState), @@ -249,15 +244,15 @@ const initMsg = initMsgFor('2024-11-05') const initNotif = { jsonrpc: '2.0', method: 'notifications/initialized' } /** A memory handler that answers as a runner with no such operation. */ -const memNotImplemented = () => (/** @type {_MemoryState} */ state) => +const memNotImplemented = () => (/** @type {typeof initial} */ state) => /** @type {const} */ ([state, error(['notImplemented', 'memRead'])]) // Runs one step against a memory mock with `overrides` applied, from a session // slot created before them so the slot itself always exists. -/** @type {(overrides: Partial>) => (msg: Unknown) => unknown} */ +/** @type {(overrides: Partial>) => (msg: Unknown) => unknown} */ const failingStep = overrides => msg => { const [state, key] = run(mock)(initial)(create(uninitializedState)) - const runner = run(/** @type {MemOperationMap} */ ({ ...mock, ...overrides })) + const runner = run(/** @type {MemOperationMap} */ ({ ...mock, ...overrides })) // A `Handle` answers `Effect<…, Response | null, never>`, so the payload // the runner hands back is the `ok` around the response and the unwrap is // total — the failures these tests inject are the ones `mcpStep` itself diff --git a/fjs/rtti/common/proof.f.mjs b/fjs/rtti/common/proof.f.mjs index f31d8f95e..9f8fa28e4 100644 --- a/fjs/rtti/common/proof.f.mjs +++ b/fjs/rtti/common/proof.f.mjs @@ -7,14 +7,12 @@ import { eachEntry, structSchemaEntries, tupleSchemaEntries, undeclaredMembers } import { error, ok } from '../../types/result/module.f.mjs' import { assert, assertEq, assertStructurallySame } from '../../asserts/module.f.mjs' -/** @typedef {ReadonlyArray} _Entries */ - /** @type {(k: string, v: number) => Result} */ const item = (k, v) => v < 0 ? error({ path: [], message: `negative at ${k}` }) : ok(v * 2) /** Mirrors `parse`'s accumulate step, kept simple (a small test list, not a `List`). */ -/** @type {(acc: _Entries, k: string, v: number) => _Entries} */ +/** @type {(acc: ReadonlyArray, k: string, v: number) => ReadonlyArray} */ const collect = (acc, k, v) => [...acc, [k, v]] export const proof = { diff --git a/fjs/rtti/data/module.f.mjs b/fjs/rtti/data/module.f.mjs index 76e8d8d9a..0fcb5cbf8 100644 --- a/fjs/rtti/data/module.f.mjs +++ b/fjs/rtti/data/module.f.mjs @@ -20,6 +20,7 @@ * @import { ResultE } from '../common/types.ts' * @import { StringMap } from '../../types/object/types.ts' * @import { ArraySet, Data, KindSet, Node, ObjectSet, RuleSet, UnionSet } from './types.ts' + * @import { _Assumed, _Ctx, _Key, _Keyed, _NodeMap, _State, _Thunk } from './private.ts' */ import { assert, assertNotNullish } from '../../asserts/module.f.mjs' @@ -439,15 +440,11 @@ const objectSet = (props, rest0) => { // ── subset ─────────────────────────────────────────────────────────────────── /** The rule sets of the two compared schemas: `[left, right]`. */ -/** @typedef {readonly [RuleSet, RuleSet]} _Ctx */ - /** * Node pairs assumed included while they are being checked — the standard * coinductive treatment of reference cycles, keyed by {@link _Keyed} node * identities. */ -/** @typedef {StringMap>} _Assumed */ - /** * A node with a canonical identity for the coinductive memo: `r:` a * rule reference, `u:` a rule's object read-set (its rest plus @@ -455,8 +452,6 @@ const objectSet = (props, rest0) => { * identity (`undefined`) — recursion through it descends its finite tree, * so every cycle still crosses identified pairs and the memo closes it. */ -/** @typedef {readonly [Node, string | undefined]} _Keyed */ - /** * Own-property lookups only: a `RuleSet`/`props` map is a plain object, so * reading through the prototype chain would return `Object.prototype` @@ -686,8 +681,6 @@ const sortedDedup = cmpItem => list => { return sorted.filter((x, i) => i === 0 || cmpItem(sorted[i - 1], x) !== 0) } -/** @typedef {(n: Node) => Node} _NodeMap */ - /** @type {(f: _NodeMap) => (p: ArraySet) => ArraySet} */ const mapArraySet = f => p => ({ prefix: p.prefix.map(f), @@ -796,29 +789,7 @@ const internData = (rules, entry) => [ // ── toData ─────────────────────────────────────────────────────────────────── /** A thunk — the only schema form that can close a reference cycle. */ -/** @typedef {Exclude} _Thunk */ - /** A schema tracked by identity: a thunk or a const container. */ -/** @typedef {Exclude} _Key */ - -/** - * The conversion state, threaded functionally: - * - * - `converting` — thunks whose union is being computed (the recursion stack). - * - `names` — rule names, assigned to a thunk the moment something needs to - * reference it (a cycle, or a deferred merge). - * - `done` — computed unions, memoized by identity. - * - `deferred` — union merges `target ∪= source` that could not run eagerly - * because `source`'s union was not final; resolved by {@link fixpoint}. - * - * @typedef {{ - * readonly converting: readonly _Thunk[] - * readonly names: readonly (readonly [_Thunk, string])[] - * readonly done: readonly (readonly [_Key, UnionSet])[] - * readonly deferred: readonly (readonly [_Thunk, _Thunk])[] - * }} _State - */ - /** * The first value associated with `key` by identity. * diff --git a/fjs/rtti/data/private.ts b/fjs/rtti/data/private.ts new file mode 100644 index 000000000..641509001 --- /dev/null +++ b/fjs/rtti/data/private.ts @@ -0,0 +1,37 @@ +/** + * Implementation-private types for the RTTI data conversion. + */ + +import type { StringMap } from '../../types/object/types.ts' +import type { Const, Type } from '../types.ts' +import type { Primitive } from '../ts/types.ts' +import type { Node, RuleSet, UnionSet } from './types.ts' + +export type _Ctx = readonly [RuleSet, RuleSet] + +export type _Assumed = StringMap> + +export type _Keyed = readonly [Node, string | undefined] + +export type _NodeMap = (n: Node) => Node + +export type _Thunk = Exclude + +export type _Key = Exclude + +/** + * The conversion state, threaded functionally: + * + * - `converting` — thunks whose union is being computed (the recursion stack). + * - `names` — rule names, assigned to a thunk the moment something needs to + * reference it (a cycle, or a deferred merge). + * - `done` — computed unions, memoized by identity. + * - `deferred` — union merges `target ∪= source` that could not run eagerly + * because `source`'s union was not final; resolved by `fixpoint`. + */ +export type _State = { + readonly converting: readonly _Thunk[] + readonly names: readonly (readonly [_Thunk, string])[] + readonly done: readonly (readonly [_Key, UnionSet])[] + readonly deferred: readonly (readonly [_Thunk, _Thunk])[] +} diff --git a/fjs/rtti/data/proof.f.mjs b/fjs/rtti/data/proof.f.mjs index 56a589252..b5a4bd690 100644 --- a/fjs/rtti/data/proof.f.mjs +++ b/fjs/rtti/data/proof.f.mjs @@ -24,149 +24,167 @@ import { absentBit, cmp, equal, never, subset, toData, unitBit, unitList, unknow const assertData = actual => expected => assert(equal(actual)(expected), [actual, expected]) -/** A recursive list: `type _List = readonly _List[]`. */ -/** @typedef {() => readonly ['array', _List]} _List */ -/** @type {_List} */ -const list = () => ['array', list] +/** + * The recursive schemas the proofs below share. They are declared inside a + * factory so their typedefs — recursive, so not inlinable — are + * function-local: an authored `.mjs` carries no file-scope `@typedef` + * (root `AGENTS.md`). The values are destructured back out, so every use + * site below reads exactly as it would have. + */ +const recursiveSchemas = () => { + /** A recursive list: `type _List = readonly _List[]`. */ + /** @typedef {() => readonly ['array', _List]} _List */ + /** @type {_List} */ + const list = () => ['array', list] + + /** Mutual recursion through a container: `_Tree = number | _Forest`, `_Forest = readonly _Tree[]`. */ + /** @typedef {() => readonly ['or', typeof number, _Forest]} _Tree */ + /** @typedef {() => readonly ['array', _Tree]} _Forest */ + /** @type {_Tree} */ + const tree = () => ['or', number, forest] + /** @type {_Forest} */ + const forest = () => ['array', tree] -/** Mutual recursion through a container: `_Tree = number | _Forest`, `_Forest = readonly _Tree[]`. */ -/** @typedef {() => readonly ['or', typeof number, _Forest]} _Tree */ -/** @typedef {() => readonly ['array', _Tree]} _Forest */ -/** @type {_Tree} */ -const tree = () => ['or', number, forest] -/** @type {_Forest} */ -const forest = () => ['array', tree] + /** A pure `or` self-cycle: `_SelfOr = number | _SelfOr`. */ + /** @typedef {() => readonly ['or', typeof number, _SelfOr]} _SelfOr */ + /** @type {_SelfOr} */ + const selfOr = () => ['or', number, selfOr] -/** A pure `or` self-cycle: `_SelfOr = number | _SelfOr`. */ -/** @typedef {() => readonly ['or', typeof number, _SelfOr]} _SelfOr */ -/** @type {_SelfOr} */ -const selfOr = () => ['or', number, selfOr] + /** A mutual `or` cycle: `_OrA = _OrB | number`, `_OrB = _OrA | string`. */ + /** @typedef {() => readonly ['or', _OrB, typeof number]} _OrA */ + /** @typedef {() => readonly ['or', _OrA, typeof string]} _OrB */ + /** @type {_OrA} */ + const orA = () => ['or', orB, number] + /** @type {_OrB} */ + const orB = () => ['or', orA, string] -/** A mutual `or` cycle: `_OrA = _OrB | number`, `_OrB = _OrA | string`. */ -/** @typedef {() => readonly ['or', _OrB, typeof number]} _OrA */ -/** @typedef {() => readonly ['or', _OrA, typeof string]} _OrB */ -/** @type {_OrA} */ -const orA = () => ['or', orB, number] -/** @type {_OrB} */ -const orB = () => ['or', orA, string] + /** An `or` over a rule that still has pending merges when it is consumed. */ + /** @typedef {() => readonly ['or', typeof string, _Inner]} _Outer */ + /** @typedef {() => readonly ['or', _Outer, _T2]} _Inner */ + /** @typedef {() => readonly ['array', _Inner]} _T2 */ + /** @type {_Outer} */ + const outer = () => ['or', string, inner] + /** @type {_Inner} */ + const inner = () => ['or', outer, t2] + /** @type {_T2} */ + const t2 = () => ['array', inner] -/** An `or` over a rule that still has pending merges when it is consumed. */ -/** @typedef {() => readonly ['or', typeof string, _Inner]} _Outer */ -/** @typedef {() => readonly ['or', _Outer, _T2]} _Inner */ -/** @typedef {() => readonly ['array', _Inner]} _T2 */ -/** @type {_Outer} */ -const outer = () => ['or', string, inner] -/** @type {_Inner} */ -const inner = () => ['or', outer, t2] -/** @type {_T2} */ -const t2 = () => ['array', inner] + /** Two `or` operands deferred onto the same target rule. */ + /** @typedef {() => readonly ['array', _Y]} _X */ + /** @typedef {() => readonly ['array', _W]} _Y */ + /** @typedef {() => readonly ['or', _X, _Y, typeof number]} _W */ + /** @type {_X} */ + const x = () => ['array', y] + /** @type {_Y} */ + const y = () => ['array', w] + /** @type {_W} */ + const w = () => ['or', x, y, number] -/** Two `or` operands deferred onto the same target rule. */ -/** @typedef {() => readonly ['array', _Y]} _X */ -/** @typedef {() => readonly ['array', _W]} _Y */ -/** @typedef {() => readonly ['or', _X, _Y, typeof number]} _W */ -/** @type {_X} */ -const x = () => ['array', y] -/** @type {_Y} */ -const y = () => ['array', w] -/** @type {_W} */ -const w = () => ['or', x, y, number] + /** A cycle whose union is the whole value domain. */ + /** @typedef {() => readonly ['or', typeof unknownRtti, _TopArr]} _TopOr */ + /** @typedef {() => readonly ['array', _TopOr]} _TopArr */ + /** @type {_TopOr} */ + const topOr = () => ['or', unknownRtti, topArr] + /** @type {_TopArr} */ + const topArr = () => ['array', topOr] -/** A cycle whose union is the whole value domain. */ -/** @typedef {() => readonly ['or', typeof unknownRtti, _TopArr]} _TopOr */ -/** @typedef {() => readonly ['array', _TopOr]} _TopArr */ -/** @type {_TopOr} */ -const topOr = () => ['or', unknownRtti, topArr] -/** @type {_TopArr} */ -const topArr = () => ['array', topOr] + /** Two named rules where only one is referenced by the entry. */ + /** @typedef {() => readonly ['array', _B2]} _B2 */ + /** @typedef {() => readonly ['array', readonly [_A2, _B2]]} _A2 */ + /** @type {_A2} */ + const a2 = () => ['array', [a2, b2]] + /** @type {_B2} */ + const b2 = () => ['array', b2] -/** Two named rules where only one is referenced by the entry. */ -/** @typedef {() => readonly ['array', _B2]} _B2 */ -/** @typedef {() => readonly ['array', readonly [_A2, _B2]]} _A2 */ -/** @type {_A2} */ -const a2 = () => ['array', [a2, b2]] -/** @type {_B2} */ -const b2 = () => ['array', b2] + /** A self-recursive record: rest-based object recursion. */ + /** @typedef {() => readonly ['record', _RecordSelf]} _RecordSelf */ + /** @type {_RecordSelf} */ + const recordSelf = () => ['record', recordSelf] -/** A self-recursive record: rest-based object recursion. */ -/** @typedef {() => readonly ['record', _RecordSelf]} _RecordSelf */ -/** @type {_RecordSelf} */ -const recordSelf = () => ['record', recordSelf] + /** Mutual recursion through object *properties* rather than containers. */ + /** @typedef {() => readonly ['const', { readonly value: typeof number, readonly next: Or }]} _Even */ + /** @typedef {() => readonly ['const', { readonly value: typeof number, readonly next: Or }]} _Odd */ + /** @type {_Even} */ + const even = () => ['const', { value: number, next: or(option, odd) }] + /** @type {_Odd} */ + const odd = () => ['const', { value: number, next: or(option, even) }] -/** Mutual recursion through object *properties* rather than containers. */ -/** @typedef {() => readonly ['const', { readonly value: typeof number, readonly next: Or }]} _Even */ -/** @typedef {() => readonly ['const', { readonly value: typeof number, readonly next: Or }]} _Odd */ -/** @type {_Even} */ -const even = () => ['const', { value: number, next: or(option, odd) }] -/** @type {_Odd} */ -const odd = () => ['const', { value: number, next: or(option, even) }] + /** @typedef {() => readonly ['array', _Rec]} _Rec */ + /** Every call returns a fresh recursive thunk whose function name is `f`. */ + /** @type {() => _Rec} */ + const mkRec = () => { + /** @type {_Rec} */ + const f = () => ['array', f] + return f + } -/** @typedef {() => readonly ['array', _Rec]} _Rec */ -/** Every call returns a fresh recursive thunk whose function name is `f`. */ -/** @type {() => _Rec} */ -const mkRec = () => { + /** @type {(f: _Rec) => _Rec} */ + const identityRec = f => f + /** A recursive thunk whose function name is the empty string. */ /** @type {_Rec} */ - const f = () => ['array', f] - return f -} + const anon = identityRec(() => ['array', anon]) -/** @type {(f: _Rec) => _Rec} */ -const identityRec = f => f -/** A recursive thunk whose function name is the empty string. */ -/** @type {_Rec} */ -const anon = identityRec(() => ['array', anon]) + /** A cycle through a closed tuple: `_ClosedNode = [number, readonly _ClosedNode[]]`. */ + /** @typedef {() => readonly ['const', readonly [typeof number, _ClosedChildren]]} _ClosedNode */ + /** @typedef {() => readonly ['array', _ClosedNode]} _ClosedChildren */ + /** @type {_ClosedNode} */ + const closedNode = () => ['const', [number, closedChildren]] + /** @type {_ClosedChildren} */ + const closedChildren = () => ['array', closedNode] -/** A cycle through a closed tuple: `_ClosedNode = [number, readonly _ClosedNode[]]`. */ -/** @typedef {() => readonly ['const', readonly [typeof number, _ClosedChildren]]} _ClosedNode */ -/** @typedef {() => readonly ['array', _ClosedNode]} _ClosedChildren */ -/** @type {_ClosedNode} */ -const closedNode = () => ['const', [number, closedChildren]] -/** @type {_ClosedChildren} */ -const closedChildren = () => ['array', closedNode] + /** A cycle through a struct's stated `rest`. */ + /** @typedef {() => readonly ['rest', { readonly a: typeof number }, _NestedRest]} _NestedRest */ + /** @type {_NestedRest} */ + const nestedRest = () => ['rest', { a: number }, nestedRest] -/** A cycle through a struct's stated `rest`. */ -/** @typedef {() => readonly ['rest', { readonly a: typeof number }, _NestedRest]} _NestedRest */ -/** @type {_NestedRest} */ -const nestedRest = () => ['rest', { a: number }, nestedRest] + /** + * A recursive rule that admits absence, with a non-empty present part — + * the referenced-rest exemption's ordinary case. + * + * @typedef {() => readonly ['or', typeof option, () => readonly ['array', _OptList]]} _OptList + */ -/** - * A recursive rule that admits absence, with a non-empty present part — - * the referenced-rest exemption's ordinary case. - * - * @typedef {() => readonly ['or', typeof option, () => readonly ['array', _OptList]]} _OptList - */ + /** @type {_OptList} */ + const optList = () => ['or', option, array(optList)] -/** @type {_OptList} */ -const optList = () => ['or', option, array(optList)] + /** + * An absence-only cycle: the pure `or` cycle dissolves to the absent bit + * alone, so the rule's present part is empty — the case that shows masking + * a referenced rest would be unsound. + * + * @typedef {() => readonly ['or', typeof option, _AbsCycleB]} _AbsCycleA + * @typedef {() => readonly ['or', _AbsCycleA]} _AbsCycleB + */ -/** - * An absence-only cycle: the pure `or` cycle dissolves to the absent bit - * alone, so the rule's present part is empty — the case that shows masking - * a referenced rest would be unsound. - * - * @typedef {() => readonly ['or', typeof option, _AbsCycleB]} _AbsCycleA - * @typedef {() => readonly ['or', _AbsCycleA]} _AbsCycleB - */ + /** @type {_AbsCycleA} */ + const absCycleA = () => ['or', option, absCycleB] -/** @type {_AbsCycleA} */ -const absCycleA = () => ['or', option, absCycleB] + /** @type {_AbsCycleB} */ + const absCycleB = () => ['or', absCycleA] -/** @type {_AbsCycleB} */ -const absCycleB = () => ['or', absCycleA] + /** + * A pure `or` cycle normalizing to `or(option, number)` — a *referenced* + * node whose stripped set equals a rest it trails. + * + * @typedef {() => readonly ['or', typeof option, typeof number, _OptNumB]} _OptNumA + * @typedef {() => readonly ['or', _OptNumA]} _OptNumB + */ -/** - * A pure `or` cycle normalizing to `or(option, number)` — a *referenced* - * node whose stripped set equals a rest it trails. - * - * @typedef {() => readonly ['or', typeof option, typeof number, _OptNumB]} _OptNumA - * @typedef {() => readonly ['or', _OptNumA]} _OptNumB - */ + /** @type {_OptNumA} */ + const optNumA = () => ['or', option, number, optNumB] -/** @type {_OptNumA} */ -const optNumA = () => ['or', option, number, optNumB] + /** @type {_OptNumB} */ + const optNumB = () => ['or', optNumA] + return { list, tree, forest, selfOr, orA, outer, inner, x, y, topArr, a2, recordSelf, even, odd, mkRec, anon, closedNode, nestedRest, optList, absCycleA, optNumA } +} -/** @type {_OptNumB} */ -const optNumB = () => ['or', optNumA] +const { + list, tree, forest, selfOr, orA, + outer, inner, x, y, topArr, + a2, recordSelf, even, odd, mkRec, + anon, closedNode, nestedRest, optList, absCycleA, + optNumA, +} = recursiveSchemas() const tupleNumber = /** @type {const} */ ([number]) const tupleString = /** @type {const} */ ([string]) diff --git a/fjs/rtti/module.f.mjs b/fjs/rtti/module.f.mjs index 500aa3d18..65d2a6a66 100644 --- a/fjs/rtti/module.f.mjs +++ b/fjs/rtti/module.f.mjs @@ -5,18 +5,14 @@ * @module * * @import { Includes } from '../types/array/types.ts' - * @import { Assert } from '../asserts/types.ts' - * @import { Equal } from '../types/ts/types.ts' - * @import { Tag0, Primitive0, _Type0, Bigint, Unknown, Option, Tag1, _MakeType1, _MakeOpen, _MakeRest, Or, Type } from './types.ts' + * @import { Tag0, _Type0, Bigint, Unknown, Option, Tag1, _MakeType1, _MakeOpen, _MakeRest, Or, Type } from './types.ts' */ import { includes } from '../types/array/module.f.mjs' -const primitive0List = /** @type {const} */ (['bigint', 'boolean', 'number', 'string']) +export const _primitive0List = /** @type {const} */ (['bigint', 'boolean', 'number', 'string']) -/** @typedef {Assert>} _Primitive0Pinned */ - -export const tag0List = /** @type {const} */ ([...primitive0List, 'unknown', 'option']) +export const tag0List = /** @type {const} */ ([..._primitive0List, 'unknown', 'option']) const type0 = /** @@ -53,12 +49,10 @@ export const bigint = type0('bigint') */ export const unknown = type0('unknown') -const tag1List = /** @type {const} */ (['array', 'record']) - -/** @typedef {Assert>} _Tag1Pinned */ +export const _tag1List = /** @type {const} */ (['array', 'record']) -/** @type {Includes} */ -export const isTag1 = includes(tag1List) +/** @type {Includes} */ +export const isTag1 = includes(_tag1List) const type1 = /** diff --git a/fjs/rtti/parse/module.f.mjs b/fjs/rtti/parse/module.f.mjs index d155ac0c6..4351c69b6 100644 --- a/fjs/rtti/parse/module.f.mjs +++ b/fjs/rtti/parse/module.f.mjs @@ -48,11 +48,11 @@ * @module * * @import { ConstObject, Info1, Tag1, Type } from '../types.ts' - * @import { Result as CommonResult } from '../../types/result/types.ts' * @import { StringMap } from '../../types/object/types.ts' * @import { Container, Fits, IsContainer, Presence, SchemaEntries, ValidateE, ValidationError, Visitor } from '../common/types.ts' * @import { Unknown } from '../ts/types.ts' * @import { Parse } from './types.ts' + * @import { _Declared, _Entries, _Rebuild } from './private.ts' */ import { ok } from '../../types/result/module.f.mjs' @@ -73,19 +73,6 @@ import { } from '../common/module.f.mjs' import { emptyRest } from '../data/module.f.mjs' -/** @typedef {CommonResult} _ItemResult */ - -/** - * The parsed `[key, parsedValue]` pairs as {@link consEntry} and - * {@link consDeclared} fold them: a cons list in **reverse** member order, so - * its head is the last member parsed — for the array kinds, the highest - * present index. - */ -/** @typedef {null | { readonly first: readonly [string, Unknown], readonly tail: _Entries }} _Entries */ - -/** Rebuilds a parsed container from its entries. */ -/** @typedef {(entries: _Entries) => Unknown} _Rebuild */ - /** * The rebuilds' one construction step, captured at module load. * @@ -207,8 +194,6 @@ const consEntry = (acc, k, v) => ({ first: [k, v], tail: acc }) /** What the declared-member fold carries: the present entries, and every member's presence bit. */ -/** @typedef {{ readonly entries: _Entries, readonly presence: Presence }} _Declared */ - /** {@link consDeclared}'s seed. */ /** @type {_Declared} */ const emptyDeclared = { entries: null, presence: null } diff --git a/fjs/rtti/parse/private.ts b/fjs/rtti/parse/private.ts new file mode 100644 index 000000000..4a72d1ef8 --- /dev/null +++ b/fjs/rtti/parse/private.ts @@ -0,0 +1,20 @@ +/** + * Implementation-private types for the RTTI parser's container rebuilds. + */ + +import type { Presence } from '../common/types.ts' +import type { Unknown } from '../ts/types.ts' + +/** + * The parsed `[key, parsedValue]` pairs as `consEntry` and `consDeclared` fold + * them: a cons list in **reverse** member order, so its head is the last member + * parsed — for the array kinds, the highest present index. + */ +export type _Entries = + null | { readonly first: readonly [string, Unknown], readonly tail: _Entries } + +/** Rebuilds a parsed container from its entries. */ +export type _Rebuild = (entries: _Entries) => Unknown + +/** A declared container's parsed entries together with its presence flags. */ +export type _Declared = { readonly entries: _Entries, readonly presence: Presence } diff --git a/fjs/rtti/parse/proof.f.mjs b/fjs/rtti/parse/proof.f.mjs index 6531809db..161f96f50 100644 --- a/fjs/rtti/parse/proof.f.mjs +++ b/fjs/rtti/parse/proof.f.mjs @@ -32,23 +32,6 @@ const unwrap = r => { return /** @type {T} */ (r[1]) } -/** A container that contains itself: `[number, node?]`. */ -/** @typedef {readonly [number, _Node?]} _Node */ - -const _node = () => /** @type {const} */ (['const', [number, or(option, _node)]]) - -/** @type {Phantom} */ -const node = _node - -/** A struct whose every undeclared key holds another one of these. */ -/** @typedef {() => readonly ['rest', { readonly a: typeof number }, _Nest]} _Nest */ - -/** @type {_Nest} */ -const _nest = () => ['rest', { a: number }, _nest] - -/** @type {Phantom<_Nest, { readonly a: number }>} */ -const nest = _nest - /** @type {(expected: readonly string[]) => (r: readonly [string, unknown]) => void} */ const assertErrorPath = expected => r => { @@ -532,6 +515,11 @@ export const proof = { // would not terminate over a recursive container — see // `../ts/types.ts`); it is the *value* half under test here. recursive: () => { + /** A container that contains itself: `[number, node?]`. */ + /** @typedef {readonly [number, _Node?]} _Node */ + const _node = () => /** @type {const} */ (['const', [number, or(option, _node)]]) + /** @type {Phantom} */ + const node = _node const p = parse(node) assertStructurallySame(unwrap(p([1])), [1]) assertStructurallySame(unwrap(p([1, [2]])), [1, [2]]) @@ -540,6 +528,12 @@ export const proof = { // A cycle through the `rest` itself: every key other than `a` holds // another one of these. recursiveRest: () => { + /** A struct whose every undeclared key holds another one of these. */ + /** @typedef {() => readonly ['rest', { readonly a: typeof number }, _Nest]} _Nest */ + /** @type {_Nest} */ + const _nest = () => ['rest', { a: number }, _nest] + /** @type {Phantom<_Nest, { readonly a: number }>} */ + const nest = _nest const p = parse(nest) assertStructurallySame(unwrap(p({ a: 1, b: { a: 2 } })), { a: 1 }) assertError(p({ a: 1, b: { a: 'x' } })) diff --git a/fjs/rtti/proof.f.mjs b/fjs/rtti/proof.f.mjs index de6bd698b..5d5557040 100644 --- a/fjs/rtti/proof.f.mjs +++ b/fjs/rtti/proof.f.mjs @@ -2,15 +2,14 @@ * @import { StringMap } from '../types/object/types.ts' * @import { Assert } from '../asserts/types.ts' * @import { Equal } from '../types/ts/types.ts' - * @import { Option, Or, Rest, Type1, Unknown } from './types.ts' + * @import { Option, Or, Primitive0, Rest, Tag1, Type1, Unknown } from './types.ts' + * @import { _primitive0List, _tag1List } from './module.f.mjs' */ import { assertNotNullish, assertStructurallySame } from '../asserts/module.f.mjs' import { array, number, open, option, or, record, rest, string, unknown } from './module.f.mjs' -/** @typedef {StringMap} _Tests */ - -/** @type {_Tests} */ +/** @type {StringMap} */ const tests = { undefined: [undefined], boolean: [true, false], @@ -62,6 +61,11 @@ const constInference = () => { } export const proof = { + /** The literal tag lists match the type-level unions in `./types.ts`. */ + pinnedLists: () => { + /** @typedef {Assert>} _Primitive0Pinned */ + /** @typedef {Assert>} _Tag1Pinned */ + }, constInference, typeof: Object.fromEntries(Object.entries(tests).map(([k, a]) => [k, assertNotNullish(a).map(v => () => { if (typeof v !== k) { throw `typeof ${v} !== ${k}` } diff --git a/fjs/rtti/ts/module.f.mjs b/fjs/rtti/ts/module.f.mjs index bc28a044b..8e26835ff 100644 --- a/fjs/rtti/ts/module.f.mjs +++ b/fjs/rtti/ts/module.f.mjs @@ -16,6 +16,7 @@ * @import { Printer, StructField } from '../../types/ts/types.ts' * @import { Type } from '../types.ts' * @import { ArraySet, Data, KindSet, Node, ObjectSet, RuleSet, UnionSet } from '../data/types.ts' + * @import { _Ctx } from './private.ts' */ import { assertNotNullish } from '../../asserts/module.f.mjs' @@ -90,14 +91,6 @@ const identifiers = rules => { return result } -/** - * @typedef {{ - * readonly ts: Printer - * readonly ids: readonly (readonly [string, string])[] - * readonly rules: RuleSet - * }} _Ctx - */ - /** @type {(ids: readonly (readonly [string, string])[], name: string) => string | undefined} */ const idOf = (ids, name) => { for (const [k, v] of ids) { diff --git a/fjs/rtti/ts/private.ts b/fjs/rtti/ts/private.ts new file mode 100644 index 000000000..64c9f4305 --- /dev/null +++ b/fjs/rtti/ts/private.ts @@ -0,0 +1,12 @@ +/** + * Implementation-private types for the RTTI-to-TypeScript printer. + */ + +import type { Printer } from '../../types/ts/types.ts' +import type { RuleSet } from '../data/types.ts' + +export type _Ctx = { + readonly ts: Printer + readonly ids: readonly (readonly [string, string])[] + readonly rules: RuleSet +} diff --git a/fjs/rtti/ts/proof.f.mjs b/fjs/rtti/ts/proof.f.mjs index a531271fe..2a1775e74 100644 --- a/fjs/rtti/ts/proof.f.mjs +++ b/fjs/rtti/ts/proof.f.mjs @@ -15,69 +15,71 @@ import { dataToTs, printer } from './module.f.mjs' // // Spelled as schema *types* rather than `typeof` a value: these are type-level // facts, and a value existing only to be pointed at is an unused one. -// -// `TupleTs` splits off the trailing run of positions admitting `undefined` and -// renders it optional, which needs a known length. A schema array of non-fixed -// length — what `.map()` produces — has no trailing position to split off, so -// it keeps its element type instead, the homomorphic mapping's answer. Pinned -// because a split that falls back to the empty tuple silently renders such a -// schema `readonly []`, and nothing else here would have caught it. -/** @typedef {Assert, readonly (number | bigint)[]>>} _NonFixedLength */ +const tupleTs = () => { + // `TupleTs` splits off the trailing run of positions admitting `undefined` + // and renders it optional, which needs a known length. A schema array of + // non-fixed length — what `.map()` produces — has no trailing position to + // split off, so it keeps its element type instead, the homomorphic + // mapping's answer. Pinned because a split that falls back to the empty + // tuple silently renders such a schema `readonly []`, and nothing else + // here would have caught it. + /** @typedef {Assert, readonly (number | bigint)[]>>} _NonFixedLength */ -// `or(option, t)` — a member that may be absent; these are the schema types -// the spelling produces. -/** @typedef {Or} _OptionBoolean */ -/** @typedef {Or} _OptionString */ + // `or(option, t)` — a member that may be absent; these are the schema types + // the spelling produces. + /** @typedef {Or} _OptionBoolean */ + /** @typedef {Or} _OptionString */ -// A variadic tuple is the shape the `length` guard exists for, and the only -// one: its peel *succeeds*, binding the unknown-length prefix to `I`, so -// without the guard the reconstruction flattens it. The others below reach the -// fallback because their peel fails, and are held by that alone. -// -// Asserted as assignability rather than with `Equal<>`. `Equal<>` reports this -// shape as unchanged whether or not the guard is in place — it cannot see the -// difference — so an `Equal<>` pin here passes over the bug it is meant to -// catch. What the flattening actually costs is a string admitted in the number -// prefix, so that is what these state. -/** @typedef {readonly [...(typeof number)[], _OptionString]} _VariadicSchema */ -/** @typedef {Assert ? false : true>} _VariadicPrefixRejectsMixedPrefix */ -/** @typedef {Assert ? true : false>} _VariadicPrefixAdmitsItsOwnShape */ + // A variadic tuple is the shape the `length` guard exists for, and the only + // one: its peel *succeeds*, binding the unknown-length prefix to `I`, so + // without the guard the reconstruction flattens it. The others below reach the + // fallback because their peel fails, and are held by that alone. + // + // Asserted as assignability rather than with `Equal<>`. `Equal<>` reports this + // shape as unchanged whether or not the guard is in place — it cannot see the + // difference — so an `Equal<>` pin here passes over the bug it is meant to + // catch. What the flattening actually costs is a string admitted in the number + // prefix, so that is what these state. + /** @typedef {readonly [...(typeof number)[], _OptionString]} _VariadicSchema */ + /** @typedef {Assert ? false : true>} _VariadicPrefixRejectsMixedPrefix */ + /** @typedef {Assert ? true : false>} _VariadicPrefixAdmitsItsOwnShape */ -// A rest element after a fixed prefix is the same shape from the other side, -// and is held for the same reason: `length` is `number`, so the mapping stands. -// -// This row and `_NonFixedLength` document intent rather than discriminate a -// mechanism. The guard and the fallback both answer `M` for these two shapes, -// so neither single mutation moves them — only removing both at once does. -// The rows that pin one mechanism each are `_VariadicPrefixRejectsMixedPrefix` -// (the guard), `_OptionalMember` (the fallback) and -// `_UnionKeepsBranchCorrelation` (the distribution). -/** @typedef {Assert, readonly [number, ...string[]]>>} _RestTuple */ + // A rest element after a fixed prefix is the same shape from the other side, + // and is held for the same reason: `length` is `number`, so the mapping stands. + // + // This row and `_NonFixedLength` document intent rather than discriminate a + // mechanism. The guard and the fallback both answer `M` for these two shapes, + // so neither single mutation moves them — only removing both at once does. + // The rows that pin one mechanism each are `_VariadicPrefixRejectsMixedPrefix` + // (the guard), `_OptionalMember` (the fallback) and + // `_UnionKeepsBranchCorrelation` (the distribution). + /** @typedef {Assert, readonly [number, ...string[]]>>} _RestTuple */ -// A schema whose own tuple type already marks a member optional is held by the -// *fallback* rather than the length guard: its length is `1 | 2`, not `number`, -// so it reaches the split, where the peel needs a required last element and -// finds none. An optional position is what this transform produces, so one the -// caller wrote is already in the target form and the mapping stands. -/** @typedef {Assert, readonly [number, string?]>>} _OptionalMember */ + // A schema whose own tuple type already marks a member optional is held by the + // *fallback* rather than the length guard: its length is `1 | 2`, not `number`, + // so it reaches the split, where the peel needs a required last element and + // finds none. An optional position is what this transform produces, so one the + // caller wrote is already in the target form and the mapping stands. + /** @typedef {Assert, readonly [number, string?]>>} _OptionalMember */ -// A union of tuple schemas is split per member, not once across the union. -// Splitting the union lets the two halves distribute independently and the -// spread then pairs every prefix with every suffix, so `[number, boolean]` — -// A's prefix with B's suffix — would pass. Assignability again: this is a -// statement about which values the union admits. -/** @typedef {readonly [typeof number, _OptionString]} _BranchA */ -/** @typedef {readonly [typeof string, _OptionBoolean, _OptionNumber]} _BranchB */ -/** @typedef {Or} _OptionNumber */ -/** @typedef {Assert ? false : true>} _UnionKeepsBranchCorrelation */ -/** @typedef {Assert ? true : false>} _UnionAdmitsItsOwnBranches */ + // A union of tuple schemas is split per member, not once across the union. + // Splitting the union lets the two halves distribute independently and the + // spread then pairs every prefix with every suffix, so `[number, boolean]` — + // A's prefix with B's suffix — would pass. Assignability again: this is a + // statement about which values the union admits. + /** @typedef {readonly [typeof number, _OptionString]} _BranchA */ + /** @typedef {readonly [typeof string, _OptionBoolean, _OptionNumber]} _BranchB */ + /** @typedef {Or} _OptionNumber */ + /** @typedef {Assert ? false : true>} _UnionKeepsBranchCorrelation */ + /** @typedef {Assert ? true : false>} _UnionAdmitsItsOwnBranches */ -/** @typedef {Assert, readonly [number, bigint, boolean?, string?]>>} _OptionalTail */ + /** @typedef {Assert, readonly [number, bigint, boolean?, string?]>>} _OptionalTail */ -// Only the *trailing* run: TypeScript forbids a required element after an -// optional one, so an interior position that admits absence stays required, -// with `undefined` — what reading a hole gives — in its type. -/** @typedef {Assert, readonly [string | undefined, number]>>} _InteriorStaysRequired */ + // Only the *trailing* run: TypeScript forbids a required element after an + // optional one, so an interior position that admits absence stays required, + // with `undefined` — what reading a hole gives — in its type. + /** @typedef {Assert, readonly [string | undefined, number]>>} _InteriorStaysRequired */ +} const toTs = printer() @@ -102,43 +104,8 @@ const eqData = (data, expected) => { assertEq(result, exp, [result, exp]) } -/** A recursive list: `type list = readonly list[]`. */ -/** @typedef {() => readonly ['array', _List]} _List */ -/** @type {_List} */ -const list = () => ['array', list] - -/** Mutual recursion through a container. */ -/** @typedef {() => readonly ['or', typeof number, _Forest]} _Tree */ -/** @typedef {() => readonly ['array', _Tree]} _Forest */ -/** @type {_Tree} */ -const tree = () => ['or', number, forest] -/** @type {_Forest} */ -const forest = () => ['array', tree] - -/** A cycle closing through an anonymous `or` thunk — an empty rule name. */ -/** @typedef {() => readonly ['record', () => readonly ['or', typeof string, _Lock]]} _Lock */ -/** @type {_Lock} */ -const lock = () => ['record', or(string, lock)] - -/** A recursive rule whose function name is the predefined type name `string`. */ -/** @typedef {() => readonly ['array', _StringNamed]} _StringNamed */ -/** @type {{ readonly string: _StringNamed }} */ -const stringNamedHolder = { string: () => ['array', stringNamedHolder.string] } -const stringNamed = stringNamedHolder.string - -/** A recursive rule whose function name is `T0` — the first generated identifier. */ -/** @typedef {() => readonly ['array', _T0Named]} _T0Named */ -/** @type {{ readonly T0: _T0Named }} */ -const t0NamedHolder = { T0: () => ['array', t0NamedHolder.T0] } -const t0Named = t0NamedHolder.T0 - -/** A recursive rule whose function name is the reserved word `if`. */ -/** @typedef {() => readonly ['array', _IfNamed]} _IfNamed */ -/** @type {{ readonly if: _IfNamed }} */ -const ifNamedHolder = { if: () => ['array', ifNamedHolder.if] } -const ifNamed = ifNamedHolder.if - export const proof = { + tupleTs, tag0: { boolean: () => eq(boolean, 'boolean'), number: () => eq(number, 'number'), @@ -306,17 +273,36 @@ export const proof = { }, recursion: { selfList: () => { + /** A recursive list: `type list = readonly list[]`. */ + /** @typedef {() => readonly ['array', _List]} _List */ + /** @type {_List} */ + const list = () => ['array', list] eq(list, 'list') eqData(toData(list), [[['list', 'readonly(list)[]']], 'list']) }, mutual: () => { + /** Mutual recursion through a container. */ + /** @typedef {() => readonly ['or', typeof number, _Forest]} _Tree */ + /** @typedef {() => readonly ['array', _Tree]} _Forest */ + /** @type {_Tree} */ + const tree = () => ['or', number, forest] + /** @type {_Forest} */ + const forest = () => ['array', tree] eqData(toData(tree), [[['tree', 'number|readonly(tree)[]']], 'tree']) eqData(toData(forest), [[['tree', 'number|readonly(tree)[]']], 'readonly(tree)[]']) }, recursiveUnion: () => { + /** A recursive list: `type list = readonly list[]`. */ + /** @typedef {() => readonly ['array', _List]} _List */ + /** @type {_List} */ + const list = () => ['array', list] eqData(toData(or(number, list)), [[['list', 'readonly(list)[]']], 'number|readonly(list)[]']) }, mutable: () => { + /** A recursive list: `type list = readonly list[]`. */ + /** @typedef {() => readonly ['array', _List]} _List */ + /** @type {_List} */ + const list = () => ['array', list] const [defs, entry] = dataToTs(true)(toData(list)) assertEq(JSON.stringify([defs, entry]), JSON.stringify([[['list', '(list)[]']], 'list'])) }, @@ -324,6 +310,10 @@ export const proof = { identifiers: { // the empty rule name is not an identifier — generated `T0` emptyName: () => { + /** A cycle closing through an anonymous `or` thunk — an empty rule name. */ + /** @typedef {() => readonly ['record', () => readonly ['or', typeof string, _Lock]]} _Lock */ + /** @type {_Lock} */ + const lock = () => ['record', or(string, lock)] eqData(toData(lock), [ [['T0', 'string|{readonly[k in string]?:T0}']], '{readonly[k in string]?:T0}', @@ -331,10 +321,20 @@ export const proof = { }, // a predefined type name cannot name an alias — generated `T0` predefinedName: () => { + /** A recursive rule whose function name is the predefined type name `string`. */ + /** @typedef {() => readonly ['array', _StringNamed]} _StringNamed */ + /** @type {{ readonly string: _StringNamed }} */ + const stringNamedHolder = { string: () => ['array', stringNamedHolder.string] } + const stringNamed = stringNamedHolder.string eqData(toData(stringNamed), [[['T0', 'readonly(T0)[]']], 'T0']) }, // reserved words cannot name an alias either — generated `T0` reservedName: () => { + /** A recursive rule whose function name is the reserved word `if`. */ + /** @typedef {() => readonly ['array', _IfNamed]} _IfNamed */ + /** @type {{ readonly if: _IfNamed }} */ + const ifNamedHolder = { if: () => ['array', ifNamedHolder.if] } + const ifNamed = ifNamedHolder.if eqData(toData(ifNamed), [[['T0', 'readonly(T0)[]']], 'T0']) }, typeOperatorName: () => { @@ -350,6 +350,15 @@ export const proof = { }, // a generated identifier skips names already kept generatedCollision: () => { + /** A recursive rule whose function name is `T0` — the first generated identifier. */ + /** @typedef {() => readonly ['array', _T0Named]} _T0Named */ + /** @type {{ readonly T0: _T0Named }} */ + const t0NamedHolder = { T0: () => ['array', t0NamedHolder.T0] } + const t0Named = t0NamedHolder.T0 + /** A cycle closing through an anonymous `or` thunk — an empty rule name. */ + /** @typedef {() => readonly ['record', () => readonly ['or', typeof string, _Lock]]} _Lock */ + /** @type {_Lock} */ + const lock = () => ['record', or(string, lock)] eqData(toData(/** @type {const} */ ([t0Named, lock])), [ [['T1', 'string|{readonly[k in string]?:T1}'], ['T0', 'readonly(T0)[]']], 'readonly[T0,{readonly[k in string]?:T1}]', diff --git a/fjs/rtti/validate/proof.f.mjs b/fjs/rtti/validate/proof.f.mjs index bb8d9ba42..d60243162 100644 --- a/fjs/rtti/validate/proof.f.mjs +++ b/fjs/rtti/validate/proof.f.mjs @@ -41,171 +41,174 @@ const p = t => /** @type {any} */ (parse(t)) const d = t => dataValidate(toData(t)) /** - * A rest that is its own container, so nothing about it is inline: the - * conversion keeps `rest: "recursiveRest"` rather than recognizing that no - * finite array inhabits it, and every reader accepts a hole past the prefix - * accordingly. It is one of the two rests {@link emptyRests} must *not* - * recognize. + * The acceptance table. Rows cover both container kinds, the closed default + * and a stated rest on both, the short-array rule, primitives, `or`, and + * misses — every reader of a schema has to answer them the same way. Built by + * a thunk so the recursive schemas it needs can carry function-local typedefs. * - * @typedef {() => readonly ['rest', readonly [_RecursiveRest], typeof never]} _RecursiveRest + * @type {() => readonly (readonly [Type, Unknown])[]} */ +const rows = () => { + /** + * A rest that is its own container, so nothing about it is inline: the + * conversion keeps `rest: "recursiveRest"` rather than recognizing that no + * finite array inhabits it, and every reader accepts a hole past the prefix + * accordingly. It is one of the two rests {@link emptyRests} must *not* + * recognize. + * + * @typedef {() => readonly ['rest', readonly [_RecursiveRest], typeof never]} _RecursiveRest + */ -/** @type {_RecursiveRest} */ -const recursiveRest = () => ['rest', [recursiveRest], never] + /** @type {_RecursiveRest} */ + const recursiveRest = () => ['rest', [recursiveRest], never] -/** - * The other one: a pure `or` cycle. `toData(orCycleA)` **is** `never`, yet as a - * rest it converts to a reference and stays, so a test on the rest's own - * canonical data would answer the opposite of the criterion. - * - * @typedef {() => readonly ['or', _OrCycleB]} _OrCycleA - * @typedef {() => readonly ['or', _OrCycleA]} _OrCycleB - */ + /** + * The other one: a pure `or` cycle. `toData(orCycleA)` **is** `never`, yet as a + * rest it converts to a reference and stays, so a test on the rest's own + * canonical data would answer the opposite of the criterion. + * + * @typedef {() => readonly ['or', _OrCycleB]} _OrCycleA + * @typedef {() => readonly ['or', _OrCycleA]} _OrCycleB + */ -/** @type {_OrCycleA} */ -const orCycleA = () => ['or', orCycleB] + /** @type {_OrCycleA} */ + const orCycleA = () => ['or', orCycleB] -/** @type {_OrCycleB} */ -const orCycleB = () => ['or', orCycleA] + /** @type {_OrCycleB} */ + const orCycleB = () => ['or', orCycleA] -/** - * Two separately constructed copies of one recursive rule. Converting a rest - * reserves its rule name first, so the container's copy is named `r0` where - * converting the container alone names it `r` — which is what rules `equal` - * out as the comparison behind {@link emptyRests}. - * - * @typedef {() => readonly ['or', undefined, () => readonly ['array', _SelfList]]} _SelfList - */ + /** + * Two separately constructed copies of one recursive rule. Converting a rest + * reserves its rule name first, so the container's copy is named `r0` where + * converting the container alone names it `r` — which is what rules `equal` + * out as the comparison behind {@link emptyRests}. + * + * @typedef {() => readonly ['or', undefined, () => readonly ['array', _SelfList]]} _SelfList + */ -/** @type {_SelfList} */ -const selfList0 = () => ['or', undefined, array(selfList0)] + /** @type {_SelfList} */ + const selfList0 = () => ['or', undefined, array(selfList0)] -/** @type {_SelfList} */ -const selfList1 = () => ['or', undefined, array(selfList1)] + /** @type {_SelfList} */ + const selfList1 = () => ['or', undefined, array(selfList1)] -/** - * The acceptance table. Rows cover both container kinds, the closed default - * and a stated rest on both, the short-array rule, primitives, `or`, and - * misses — every reader of a schema has to answer them the same way. - * - * @type {readonly (readonly [Type, Unknown])[]} - */ -const rows = [ - [number, 42], - [number, '42'], - [string, 42], - [boolean, false], - [bigint, 7n], - [unknown, { a: [1, 'x'] }], - [/** @type {const} */ (42), 42], - [/** @type {const} */ (42), 43], - [array(number), [1, 2, 3]], - [array(number), [1, 'two']], - [array(number), {}], - // an enumerable non-index key is an entry every reader walks, so it is - // held to the element type like any other — and a key is an index only in - // the canonical spelling, whatever `Number` makes of it - [array(number), Object.assign([1], { foo: 2 })], - [array(number), Object.assign([1], { foo: 'x' })], - [array(number), Object.assign([1], { '-1': 'x' })], - [array(number), Object.assign([1], { '01': 'x' })], - // an empty element set is the empty array, not "any number of holes": the - // data form normalizes such a rest away, which leaves the exact-length - // pattern, and the thunk readers bound the length to match - [array(or()), []], - [array(or()), new Array(1)], - [array(number), [, ,]], - [record(number), { a: 1 }], - [record(number), { a: 'one' }], - [record(number), []], - // the closed default, on both kinds - [[/** @type {const} */ (42)], [42, 'extra']], - [[/** @type {const} */ (42)], [42]], - [[/** @type {const} */ (42)], [42, undefined]], - [[/** @type {const} */ (42)], [42, ,]], - [[/** @type {const} */ (42)], Object.assign([42], { foo: 1 })], - [[/** @type {const} */ (42)], []], - [{ a: /** @type {const} */ (42) }, { a: 42, b: 'x' }], - [{ a: /** @type {const} */ (42) }, { a: 42 }], - // a key declared `unknown` is a member the schema has, so the canonical - // form must not drop it the way an `open` struct's is dropped — and one - // that must be *present*, `unknown` excluding absence - [{ a: unknown }, { a: 1 }], - [{ a: unknown }, { a: 1, b: 2 }], - [{ a: unknown }, {}], - // the declared-member top — anything, or nothing — is still closed over - // its undeclared keys - [{ a: or(option, unknown) }, {}], - [{ a: or(option, unknown) }, { a: 1 }], - [{ a: or(option, unknown) }, { a: 1, b: 2 }], - // and the same rows under `open`, which is the form that admits them - [open([/** @type {const} */ (42)]), [42, 'extra']], - [open({ a: /** @type {const} */ (42) }), { a: 42, b: 'x' }], - [open([]), [1]], - [open({}), { a: 1 }], - // closedness is about *undeclared* members and leaves the short-array rule - // alone - [[number, or(option, string)], [42]], - // the rule is per position, not "the last one": every trailing position - // whose set admits `undefined` may be absent, so an array may stop at the - // last required one - [[number, bigint, or(option, string), or(option, null)], [2, 4n]], - [[number, bigint, or(option, string), or(option, null)], [2, 4n, 'x']], - [[number, bigint, or(option, string), or(option, null)], [2, 4n, 'x', null]], - [[number, bigint, or(option, string), or(option, null)], [2]], - [[number, bigint, or(option, string), or(option, null)], [2, 4n, 5]], - [{ a: number, b: or(option, string) }, { a: 1 }], - [{ a: number }, { a: 'one' }], - // a hole in a tuple schema is a declared position whose schema is - // `undefined`, so the schema's length is what it declares — the reading - // the data form has always had, and the one `Object.entries` lost - [new Array(1), [1, 2, 3]], - [new Array(1), new Array(1)], - [new Array(1), [undefined]], - [new Array(1), [1]], - [new Array(1), []], - [[, number], [9, 5]], - [[, number], [undefined, 5]], - // and a non-index enumerable own property is no position at all: a tuple - // schema is read by index, so `foo` declares nothing — which leaves a - // value's own `foo` an undeclared member like any other - [Object.assign([number], { foo: string }), [1]], - [Object.assign([number], { foo: string }), Object.assign([1], { foo: 'x' })], - [open(Object.assign([number], { foo: string })), Object.assign([1], { foo: 'x' })], - // a stated rest: what an undeclared member must be - [rest([number], string), [1, 'x', 'y']], - [rest([number], string), [1, 2]], - // a hole past the prefix is no member, so it meets no rest — which is what - // the `| undefined` in the rendered tail says - [rest([number], string), [1, ,]], - // An index the prototype supplies, and a key past the index range, are - // members too — both need in-place mutation to build, so their rows run - // through the same three readers in `../host.proof.mjs`. - [rest({ a: number }, string), { a: 1, b: 'x' }], - [rest({ a: number }, string), { a: 1, b: 2 }], - // a stated rest with nothing to answer for: the struct kind has no length, - // so it fits whatever the rest is - [rest({ a: number }, string), { a: 1 }], - // an unconstrained rest is `open` - [rest([number], unknown), [1, 'x']], - [rest({ a: number }, unknown), { a: 1, b: 'x' }], - // an empty one is the bare form, so the length is bounded again - [rest([number], never), [1, ,]], - [rest([number], or()), [1, ,]], - [rest([number], [or()]), [1, ,]], - [rest([number], [or()]), [1, 2]], - // …and a rest the conversion keeps is not empty, however few values it - // has: these two are the pair that tells the criterion from an emptiness - // analysis - [rest([number], recursiveRest), [1, ,]], - [rest([number], orCycleA), [1, ,]], - [rest([selfList0], [selfList1, never]), [undefined, ,]], - [or(number, string), true], - [or(number, string), 'hello'], - [or(option, number), undefined], - [or(option, number), null], - [{ user: { name: string, age: number } }, { user: { name: 'A', age: 'old' } }], -] + return [ + [number, 42], + [number, '42'], + [string, 42], + [boolean, false], + [bigint, 7n], + [unknown, { a: [1, 'x'] }], + [/** @type {const} */ (42), 42], + [/** @type {const} */ (42), 43], + [array(number), [1, 2, 3]], + [array(number), [1, 'two']], + [array(number), {}], + // an enumerable non-index key is an entry every reader walks, so it is + // held to the element type like any other — and a key is an index only in + // the canonical spelling, whatever `Number` makes of it + [array(number), Object.assign([1], { foo: 2 })], + [array(number), Object.assign([1], { foo: 'x' })], + [array(number), Object.assign([1], { '-1': 'x' })], + [array(number), Object.assign([1], { '01': 'x' })], + // an empty element set is the empty array, not "any number of holes": the + // data form normalizes such a rest away, which leaves the exact-length + // pattern, and the thunk readers bound the length to match + [array(or()), []], + [array(or()), new Array(1)], + [array(number), [, ,]], + [record(number), { a: 1 }], + [record(number), { a: 'one' }], + [record(number), []], + // the closed default, on both kinds + [[/** @type {const} */ (42)], [42, 'extra']], + [[/** @type {const} */ (42)], [42]], + [[/** @type {const} */ (42)], [42, undefined]], + [[/** @type {const} */ (42)], [42, ,]], + [[/** @type {const} */ (42)], Object.assign([42], { foo: 1 })], + [[/** @type {const} */ (42)], []], + [{ a: /** @type {const} */ (42) }, { a: 42, b: 'x' }], + [{ a: /** @type {const} */ (42) }, { a: 42 }], + // a key declared `unknown` is a member the schema has, so the canonical + // form must not drop it the way an `open` struct's is dropped — and one + // that must be *present*, `unknown` excluding absence + [{ a: unknown }, { a: 1 }], + [{ a: unknown }, { a: 1, b: 2 }], + [{ a: unknown }, {}], + // the declared-member top — anything, or nothing — is still closed over + // its undeclared keys + [{ a: or(option, unknown) }, {}], + [{ a: or(option, unknown) }, { a: 1 }], + [{ a: or(option, unknown) }, { a: 1, b: 2 }], + // and the same rows under `open`, which is the form that admits them + [open([/** @type {const} */ (42)]), [42, 'extra']], + [open({ a: /** @type {const} */ (42) }), { a: 42, b: 'x' }], + [open([]), [1]], + [open({}), { a: 1 }], + // closedness is about *undeclared* members and leaves the short-array rule + // alone + [[number, or(option, string)], [42]], + // the rule is per position, not "the last one": every trailing position + // whose set admits `undefined` may be absent, so an array may stop at the + // last required one + [[number, bigint, or(option, string), or(option, null)], [2, 4n]], + [[number, bigint, or(option, string), or(option, null)], [2, 4n, 'x']], + [[number, bigint, or(option, string), or(option, null)], [2, 4n, 'x', null]], + [[number, bigint, or(option, string), or(option, null)], [2]], + [[number, bigint, or(option, string), or(option, null)], [2, 4n, 5]], + [{ a: number, b: or(option, string) }, { a: 1 }], + [{ a: number }, { a: 'one' }], + // a hole in a tuple schema is a declared position whose schema is + // `undefined`, so the schema's length is what it declares — the reading + // the data form has always had, and the one `Object.entries` lost + [new Array(1), [1, 2, 3]], + [new Array(1), new Array(1)], + [new Array(1), [undefined]], + [new Array(1), [1]], + [new Array(1), []], + [[, number], [9, 5]], + [[, number], [undefined, 5]], + // and a non-index enumerable own property is no position at all: a tuple + // schema is read by index, so `foo` declares nothing — which leaves a + // value's own `foo` an undeclared member like any other + [Object.assign([number], { foo: string }), [1]], + [Object.assign([number], { foo: string }), Object.assign([1], { foo: 'x' })], + [open(Object.assign([number], { foo: string })), Object.assign([1], { foo: 'x' })], + // a stated rest: what an undeclared member must be + [rest([number], string), [1, 'x', 'y']], + [rest([number], string), [1, 2]], + // a hole past the prefix is no member, so it meets no rest — which is what + // the `| undefined` in the rendered tail says + [rest([number], string), [1, ,]], + // An index the prototype supplies, and a key past the index range, are + // members too — both need in-place mutation to build, so their rows run + // through the same three readers in `../host.proof.mjs`. + [rest({ a: number }, string), { a: 1, b: 'x' }], + [rest({ a: number }, string), { a: 1, b: 2 }], + // a stated rest with nothing to answer for: the struct kind has no length, + // so it fits whatever the rest is + [rest({ a: number }, string), { a: 1 }], + // an unconstrained rest is `open` + [rest([number], unknown), [1, 'x']], + [rest({ a: number }, unknown), { a: 1, b: 'x' }], + // an empty one is the bare form, so the length is bounded again + [rest([number], never), [1, ,]], + [rest([number], or()), [1, ,]], + [rest([number], [or()]), [1, ,]], + [rest([number], [or()]), [1, 2]], + // …and a rest the conversion keeps is not empty, however few values it + // has: these two are the pair that tells the criterion from an emptiness + // analysis + [rest([number], recursiveRest), [1, ,]], + [rest([number], orCycleA), [1, ,]], + [rest([selfList0], [selfList1, never]), [undefined, ,]], + [or(number, string), true], + [or(number, string), 'hello'], + [or(option, number), undefined], + [or(option, number), null], + [{ user: { name: string, age: number } }, { user: { name: 'A', age: 'old' } }], + ] +} export const proof = { // ── the three properties this module exists for ────────────────────────── @@ -266,7 +269,7 @@ export const proof = { // Acceptance is `parse`'s, exactly: the two readers differ in what a // success carries and in nothing else. sameAcceptanceAsParse: () => { - for (const [t, value] of rows) { + for (const [t, value] of rows()) { const rv = v(t)(value) const rp = p(t)(value) assertEq(rv[0], rp[0], 'validate and parse must agree on acceptance') @@ -286,7 +289,7 @@ export const proof = { // it reports a miss as its own kind-wise failure rather than repeating // `or`'s `no match`. sameAcceptanceInTheDataForm: () => { - for (const [t, value] of rows) { + for (const [t, value] of rows()) { assertEq(d(t)(value)[0], p(t)(value)[0], 'the data form must accept what `parse` accepts') } }, @@ -794,6 +797,17 @@ export const proof = { // `never`'s identity passes the converse. emptyRests: { dropped: () => { + /** + * Two separately constructed copies of one recursive rule — the + * pair behind the name-collision comparison; see the acceptance + * table's own copy for the full story. + * + * @typedef {() => readonly ['or', undefined, () => readonly ['array', _SelfList]]} _SelfList + */ + /** @type {_SelfList} */ + const selfList0 = () => ['or', undefined, array(selfList0)] + /** @type {_SelfList} */ + const selfList1 = () => ['or', undefined, array(selfList1)] for (const r of [never, or(), [or()]]) { assertError(validate(rest([number], r))([42, ,])) } @@ -803,6 +817,24 @@ export const proof = { assertError(v(rest([selfList0], [selfList1, never]))([undefined, ,])) }, kept: () => { + /** + * A rest that is its own container, so nothing about it is + * inline; the acceptance table's copy carries the full story. + * + * @typedef {() => readonly ['rest', readonly [_RecursiveRest], typeof never]} _RecursiveRest + */ + /** @type {_RecursiveRest} */ + const recursiveRest = () => ['rest', [recursiveRest], never] + /** + * The other one: a pure `or` cycle. + * + * @typedef {() => readonly ['or', _OrCycleB]} _OrCycleA + * @typedef {() => readonly ['or', _OrCycleA]} _OrCycleB + */ + /** @type {_OrCycleA} */ + const orCycleA = () => ['or', orCycleB] + /** @type {_OrCycleB} */ + const orCycleB = () => ['or', orCycleA] // A rest the conversion keeps is not empty however few values it // has: `recursiveRest` catches an emptiness analysis that reaches // container cycles, `orCycleA` one that tests the rest's own diff --git a/fjs/sul/level/hash/proof.f.mjs b/fjs/sul/level/hash/proof.f.mjs index 793260fce..7f15100b3 100644 --- a/fjs/sul/level/hash/proof.f.mjs +++ b/fjs/sul/level/hash/proof.f.mjs @@ -7,16 +7,14 @@ import { assert, assertEq, assertNotNullish } from '../../../asserts/module.f.mj import { compress, level3Id } from '../../id/module.f.mjs' import { emptyEncodeState, encode } from './module.f.mjs' -/** @typedef {readonly (readonly [Id, Id, Id, boolean])[]} _NodeList */ - -/** @type {(l: Id, r: Id, m: Id, isSymbol: boolean, s: _NodeList) => _NodeList} */ +/** @type {(l: Id, r: Id, m: Id, isSymbol: boolean, s: readonly (readonly [Id, Id, Id, boolean])[]) => readonly (readonly [Id, Id, Id, boolean])[]} */ const add = (l, r, m, isSymbol, s) => [...s, [l, r, m, isSymbol]] const enc = encode(add) -/** @type {EncodeState<_NodeList>} */ +/** @type {EncodeState} */ const initial = emptyEncodeState([]) // Run a complete valid word from a clean state; throws if no output is produced. -/** @type {(symbols: readonly Id[]) => readonly [Id, _NodeList]} */ +/** @type {(symbols: readonly Id[]) => readonly [Id, readonly (readonly [Id, Id, Id, boolean])[]]} */ const runWord = symbols => { let state = initial for (const s of symbols) { @@ -28,7 +26,7 @@ const runWord = symbols => { } // Every stored triple must satisfy m === compress(l, r). -/** @type {(storage: _NodeList) => void} */ +/** @type {(storage: readonly (readonly [Id, Id, Id, boolean])[]) => void} */ const verifyStorage = storage => { for (const [l, r, m] of storage) { assertEq(m, compress(l, r)) } } diff --git a/fjs/sul/module.f.mjs b/fjs/sul/module.f.mjs index c252b4860..e225314b0 100644 --- a/fjs/sul/module.f.mjs +++ b/fjs/sul/module.f.mjs @@ -15,8 +15,6 @@ import { emptyPipelineState, pipelineStep } from './level/literal/module.f.mjs' import { encode as hashEncode } from './level/hash/module.f.mjs' import { level3Id } from './id/module.f.mjs' -/** @typedef {InternalState} _HashState */ - /** @type {(storage: S) => EncodeState} */ export const emptyEncodeState = storage => [emptyPipelineState, storage, []] @@ -30,14 +28,14 @@ export const encode = add => { const step = hashEncode(add) - /** @typedef {readonly [Id | undefined, S, readonly _HashState[]]} _CascadeResult */ + /** @typedef {readonly [Id | undefined, S, readonly InternalState[]]} _CascadeResult */ // Recursive rather than a `for(;;)` loop: every exit is one of the two // `return`s below, so a bare `for(;;)` picks up a phantom "loop falls // through" branch that V8's coverage instrumentation can never mark // taken — there is no third way out to take it. Recursion has no such // branch to begin with. - /** @type {(id: Id, storage: S, stacks: readonly _HashState[], index: number) => _CascadeResult} */ + /** @type {(id: Id, storage: S, stacks: readonly InternalState[], index: number) => _CascadeResult} */ const cascadeFrom = (id, storage, stacks, index) => { if (index >= stacks.length) { const [, [newStorage, newStack]] = step(id, [storage, []]) @@ -50,7 +48,7 @@ export const encode = : cascadeFrom(out, newStorage, newStacks, index + 1) } - /** @type {(id0: Id, storage0: S, stacks0: readonly _HashState[]) => _CascadeResult} */ + /** @type {(id0: Id, storage0: S, stacks0: readonly InternalState[]) => _CascadeResult} */ const cascade = (id0, storage0, stacks0) => cascadeFrom(id0, storage0, stacks0, 0) /** @type {(bit: bigint, state: EncodeState) => readonly [Id | undefined, EncodeState]} */ diff --git a/fjs/sul/proof.f.mjs b/fjs/sul/proof.f.mjs index 17a6c5f23..d334a50ec 100644 --- a/fjs/sul/proof.f.mjs +++ b/fjs/sul/proof.f.mjs @@ -7,11 +7,9 @@ import { assert, assertEq } from '../asserts/module.f.mjs' import { compress } from './id/module.f.mjs' import { encode, emptyEncodeState } from './module.f.mjs' -/** @typedef {readonly [Id, Id, Id, boolean]} _Merge */ - -/** @type {(bits: readonly bigint[]) => readonly [Id, readonly _Merge[]]} */ +/** @type {(bits: readonly bigint[]) => readonly [Id, readonly (readonly [Id, Id, Id, boolean])[]]} */ const run = bits => { - /** @type {_Merge[]} */ + /** @type {(readonly [Id, Id, Id, boolean])[]} */ const log = [] /** @type {Add} */ const add = (l, r, m, isSymbol) => { log.push([l, r, m, isSymbol]); return null } diff --git a/fjs/text/sgr/module.f.mjs b/fjs/text/sgr/module.f.mjs index 93ec54cc6..8ad134874 100644 --- a/fjs/text/sgr/module.f.mjs +++ b/fjs/text/sgr/module.f.mjs @@ -21,10 +21,6 @@ export const backspace = '\x08' // -/** @typedef {'m'} _End */ - -/** @typedef {(code: number | string) => string} _Csi */ - const begin = '\x1b[' /** @@ -34,7 +30,7 @@ const begin = '\x1b[' * @param end - The final character that indicates the type of sequence. * @returns A function that takes a code (number or string) and returns the complete ANSI escape sequence. * - * @type {(end: _End) => _Csi} + * @type {(end: 'm') => (code: number | string) => string} */ export const csi = end => code => `${begin}${code.toString()}${end}` @@ -43,7 +39,7 @@ export const csi = end => code => * Specialization of CSI for Select Graphic Rendition (SGR) sequences. * https://en.wikipedia.org/wiki/ANSI_escape_code#SGR * - * @type {_Csi} + * @type {(code: number | string) => string} */ export const sgr = csi('m') diff --git a/fjs/text/utf16/module.f.mjs b/fjs/text/utf16/module.f.mjs index e2567bb78..f63e80eff 100644 --- a/fjs/text/utf16/module.f.mjs +++ b/fjs/text/utf16/module.f.mjs @@ -31,12 +31,9 @@ import { isSupplementaryPlane, } from '../code_point/module.f.mjs' -/** - * Optional Utf16State - represents the state of utf16 decoding operation or null. - * - number is used an unsigned integer. - * - * @typedef {number | null} _Utf16State - */ +// The `number | null` state threaded through the decoder below is the UTF-16 +// decoding state: a pending high surrogate as an unsigned integer, or `null` +// when no code unit is pending. /** * The BMP / surrogate / supplementary-plane predicates used below live in @@ -180,7 +177,7 @@ const u16 = i => Number.isInteger(i) && isInU16Range(i) * const [decodedCodePoints, newState] = utf16ByteToCodePointOp(word, state); * ``` * - * @type {StateScan>} + * @type {StateScan>} */ const utf16ByteToCodePointOp = (word, state) => { if (!u16(word)) { @@ -223,7 +220,7 @@ const utf16StateToError = state => state | errorMask * to flag the invalid sequence. The flush itself is `eofFlush` from * `code_point`, shared with UTF-8. * - * @type {(state: _Utf16State) => readonly[List, _Utf16State]} + * @type {(state: number | null) => readonly[List, number | null]} */ const utf16EofToCodePointOp = eofFlush(utf16StateToError) diff --git a/fjs/todo/module-tag-on-types-ts.md b/fjs/todo/module-tag-on-types-ts.md new file mode 100644 index 000000000..f1ebf7079 --- /dev/null +++ b/fjs/todo/module-tag-on-types-ts.md @@ -0,0 +1,27 @@ +## `@module` on existing `types.ts` files contradicts the header convention + +**Priority:** P3 +**Status:** open + +### Problem + +`fjs/AGENTS.md` §2 reserves the `@module` tag for a package's entry-point file +(`module.f.mjs` / `module.mjs`) and explicitly excludes `types.ts`. Yet the +pre-existing `types.ts` files across the repository — all 23 under +`fjs/types/*/types.ts`, plus others such as `fjs/effects/types.ts` — carry +`@module` in their header block. + +New helper type files (`types.ts` / `private.ts`) added since the private-type +migration follow the documented rule and carry no `@module`; the older files +were left as found so that the migration did not widen. + +### Tasks + +- [ ] Decide which side is right: strip `@module` from the existing non-entry + `.ts` files, or narrow `fjs/AGENTS.md` §2 if `types.ts` files are meant + to be documented entry points. +- [ ] Apply the decision consistently across the repository. + +### Related + +- [`../AGENTS.md`](../AGENTS.md) — §2 module-header convention. diff --git a/fjs/todo/separate-private-types.md b/fjs/todo/separate-private-types.md index 28fe762af..ebe07e019 100644 --- a/fjs/todo/separate-private-types.md +++ b/fjs/todo/separate-private-types.md @@ -274,31 +274,41 @@ type-only and use named `import type { ... }` imports. #### Stage 1 — source restructuring -- [ ] Document the repository-wide prohibition on file-scope JSDoc `@typedef` in +- [x] Document the repository-wide prohibition on file-scope JSDoc `@typedef` in authored `.mjs`; allow function-local typedefs. -- [ ] Migrate existing violations, including authored `.mjs` outside `fjs/` such +- [x] Migrate existing violations, including authored `.mjs` outside `fjs/` such as `todo/proof.f.mjs`. -- [ ] Keep `types.ts` as the public declaration closure; retain/in-line private +- [x] Keep `types.ts` as the public declaration closure; retain/in-line private helpers required by public declarations. -- [ ] Use `private.ts` only where separating implementation-private file-scope +- [x] Use `private.ts` only where separating implementation-private file-scope types improves the design. -- [ ] Preserve the intra-directory dependency direction shown above; move +- [x] Preserve the intra-directory dependency direction shown above; move verification downstream when that is cleaner. -- [ ] Move the `fjs/effects/types.ts` implementation-signature asserts into proof +- [x] Move the `fjs/effects/types.ts` implementation-signature asserts into proof functions in `fjs/effects/proof.f.mjs`. -- [ ] Review recursive cases individually, including `fjs/media/revision` and +- [x] Review recursive cases individually, including `fjs/media/revision` and `fjs/edag`; keep recursive RTTI in `module.f.mjs` when required by layering and move consistency asserts into proof functions. -- [ ] Where useful, split declarative compile-time/runtime constants into a normal - subordinate module such as `meta/module.f.mjs`; do not require it. -- [ ] Preserve leading `_` for private types and private runtime constants. -- [ ] Treat chosen public import-path moves as breaking changes with no +- [x] Where useful, split declarative compile-time/runtime constants into a normal + subordinate module such as `meta/module.f.mjs`; do not require it. The + migration warranted none: every recursive metaprogramming constant + (`fjs/edag`, `fjs/media/json/schema`) reads best staying in its + `module.f.mjs`; the option stays documented in `fjs/AGENTS.md` §3.2. +- [x] Preserve leading `_` for private types and private runtime constants. +- [x] Treat chosen public import-path moves as breaking changes with no compatibility re-exports. -- [ ] Add fixtures/examples covering: public-declaration helpers, optional +- [x] Add fixtures/examples covering: public-declaration helpers, optional `private.ts`, function-local proof typedefs, recursive RTTI kept in `module.f.mjs`, optional `meta/module.f.mjs`, and authored `.mjs` outside - `fjs/`. -- [ ] Update root and `fjs/` `AGENTS.md` policy documentation; rewrite the + `fjs/`. Live modules serve as the examples, cited from `fjs/AGENTS.md` + §3.2: `fjs/types/byte_set/types.ts` (`_Byte` public-closure helper), + `fjs/common/monoid/private.ts` and `fjs/rtti/data/private.ts` + (`private.ts`), `fjs/edag/proof.f.mjs` and `fjs/effects/proof.f.mjs` + (function-local proof typedefs), `fjs/edag/module.f.mjs` and + `fjs/media/json/schema/module.f.mjs` (recursive RTTI kept in place), + `todo/proof.f.mjs` (authored `.mjs` outside `fjs/`); `meta/module.f.mjs` + remains a documented option with no current instance. +- [x] Update root and `fjs/` `AGENTS.md` policy documentation; rewrite the `fjs/fsc/README.md` typedef prescription; delete or narrow the blocked `@internal` TODO; sweep all remaining Markdown documents for file-scope typedef prescriptions and retarget each to the Stage 1 forms. diff --git a/fjs/types/bigfloat/module.f.mjs b/fjs/types/bigfloat/module.f.mjs index cb28340a8..e0726bf16 100644 --- a/fjs/types/bigfloat/module.f.mjs +++ b/fjs/types/bigfloat/module.f.mjs @@ -5,20 +5,11 @@ * * @import { BigFloat, Format } from './types.ts' * @import { Nullable } from '../nullable/types.ts' + * @import { _BigFloatWithRemainder } from './private.ts' */ import { abs, bitLength, mask, sign } from '../bigint/module.f.mjs' -/** - * A magnitude that has been truncated, paired with what was cut off: the exact - * value is `m * 2^e` when `r` is `0n`, and strictly between `m * 2^e` and - * `(m + 1) * 2^e` otherwise. Only `r === 0n` is ever asked, so any non-zero - * `r` — a division remainder, the bits a shift dropped, or both — says the - * same thing. - * - * @typedef {readonly [BigFloat, bigint]} _BigFloatWithRemainder - */ - /** @type {(exp: number) => bigint} */ const twoPow = exp => 1n << BigInt(exp) diff --git a/fjs/types/bigfloat/private.ts b/fjs/types/bigfloat/private.ts new file mode 100644 index 000000000..d0dd82c1d --- /dev/null +++ b/fjs/types/bigfloat/private.ts @@ -0,0 +1,14 @@ +/** + * Implementation-private types for the big-float module. + */ + +import type { BigFloat } from './types.ts' + +/** + * A magnitude that has been truncated, paired with what was cut off: the exact + * value is `m * 2^e` when `r` is `0n`, and strictly between `m * 2^e` and + * `(m + 1) * 2^e` otherwise. Only `r === 0n` is ever asked, so any non-zero + * `r` — a division remainder, the bits a shift dropped, or both — says the + * same thing. + */ +export type _BigFloatWithRemainder = readonly [BigFloat, bigint] diff --git a/fjs/types/bigint/proof.f.mjs b/fjs/types/bigint/proof.f.mjs index 7a6fd29e3..3284a7cbb 100644 --- a/fjs/types/bigint/proof.f.mjs +++ b/fjs/types/bigint/proof.f.mjs @@ -139,9 +139,7 @@ const m1023log2 = v => { return result + rem + (v >> rem) } -/** @typedef {(f: (_: bigint) => bigint) => () => void} _Benchmark */ - -/** @type {_Benchmark} */ +/** @type {(f: (_: bigint) => bigint) => () => void} */ const benchmark = f => () => { let e = 1_048_575n let c = 1n << e @@ -160,7 +158,7 @@ const benchmark = f => () => { } -/** @type {_Benchmark} */ +/** @type {(f: (_: bigint) => bigint) => () => void} */ const benchmarkSmall = f => () => { let e = 2_000n let c = 1n << e @@ -211,7 +209,7 @@ export const proof = { // m1023log2, log2, } - const transform = (/** @type {_Benchmark} */ b) => + const transform = (/** @type {(f: (_: bigint) => bigint) => () => void} */ b) => Object.fromEntries(Object.entries(list).map(([k, f]) => [k, b(f)])) return { big: transform(benchmark), diff --git a/fjs/types/bit_vec/module.f.mjs b/fjs/types/bit_vec/module.f.mjs index 396de7788..6228f5ca1 100644 --- a/fjs/types/bit_vec/module.f.mjs +++ b/fjs/types/bit_vec/module.f.mjs @@ -27,7 +27,7 @@ * @import { Absorbing } from '../../common/monoid/types.ts' * @import { Sign } from '../function/compare/types.ts' * @import { Nullable } from '../nullable/types.ts' - * @import { BitOrder, PopFront, Reduce, Unpacked, Vec, _NormOp, _UnpackConcat, } from './types.ts' + * @import { BitOrder, PopFront, Reduce, Unpacked, Vec, _Base, _NormOp, _UnpackConcat, } from './types.ts' */ import { bitLength, divUp, mask, maxLength, xor } from '../bigint/module.f.mjs' @@ -170,15 +170,6 @@ const op = norm => op => ap => bp => { return vec(len)(op(a)(b)) } -/** - * @typedef {{ - * readonly norm: _NormOp - * readonly uintCmp: (a: bigint) => (b: bigint) => Sign - * readonly unpackSplit: (len: bigint) => (u: Unpacked) => readonly[bigint, bigint] - * readonly unpackConcatUint: (a: Unpacked) => (b: Unpacked) => bigint - * }} _Base - */ - const unpackEmpty = /** @type {const} */{ length: 0n, uint: 0n } /** diff --git a/fjs/types/bit_vec/types.ts b/fjs/types/bit_vec/types.ts index 93b1d7bed..58c940a51 100644 --- a/fjs/types/bit_vec/types.ts +++ b/fjs/types/bit_vec/types.ts @@ -38,6 +38,14 @@ export type _NormOp = Binary export type _UnpackConcat = (a: Unpacked) => (b: Unpacked) => Unpacked +/** The order-specific operations a `BitOrder` is assembled from. */ +export type _Base = { + readonly norm: _NormOp + readonly uintCmp: (a: bigint) => (b: bigint) => Sign + readonly unpackSplit: (len: bigint) => (u: Unpacked) => readonly [bigint, bigint] + readonly unpackConcatUint: (a: Unpacked) => (b: Unpacked) => bigint +} + export type Reduce = OpReduce export type PopFront = (len: bigint) => (u: T) => readonly [bigint, T] diff --git a/fjs/types/btree/remove/module.f.mjs b/fjs/types/btree/remove/module.f.mjs index c93b27602..77703d56e 100644 --- a/fjs/types/btree/remove/module.f.mjs +++ b/fjs/types/btree/remove/module.f.mjs @@ -7,6 +7,7 @@ * @import { Compare } from '../../function/compare/types.ts' * @import { Path, PathItem } from '../find/types.ts' * @import { Tuple } from '../../array/types.ts' + * @import { _Branch, _Leaf01, _Merge, _RemovePath } from './private.ts' */ import { collapseRoot } from '../types/module.f.mjs' @@ -14,19 +15,6 @@ import { find } from '../find/module.f.mjs' import { fold, concat, next } from '../../list/module.f.mjs' import { map } from '../../nullable/module.f.mjs' -/** - * @template T - * @typedef {null | Leaf1} _Leaf01 - */ - -/** - * @template T - * @typedef {{ - * readonly first: _Leaf01, - * readonly tail: Path - * }} _RemovePath - */ - /** @type {(tail: Path) => (n: TNode) => readonly[T, _RemovePath]} */ const path = tail => n => { switch (n.length) { @@ -37,11 +25,6 @@ const path = tail => n => { } } -/** - * @template T - * @typedef {Branch1 | Branch3 | Branch5} _Branch - */ - /** @type {(a: _Branch) => (n: Branch3) => Branch1 | Branch3} */ const reduceValue0 = a => n => { const [, v1, n2] = n @@ -96,12 +79,6 @@ const initValue1 = a => n => { } else { return [n0, v1, a] } } -/** - * @template A - * @template T - * @typedef {(a: A) => (n: Branch3) => Branch1 | Branch3} _Merge - */ - /** @type {(ms: Tuple<2, _Merge>) => (item: PathItem) => (a: A) => _Branch} */ const reduceX = ms => ([i, n]) => a => { /** @typedef {(typeof n)[1]} T */ diff --git a/fjs/types/btree/remove/private.ts b/fjs/types/btree/remove/private.ts new file mode 100644 index 000000000..1c1d8152f --- /dev/null +++ b/fjs/types/btree/remove/private.ts @@ -0,0 +1,17 @@ +/** + * Implementation-private types for B-tree removal. + */ + +import type { Branch1, Branch3, Branch5, Leaf1 } from '../types/types.ts' +import type { Path } from '../find/types.ts' + +export type _Leaf01 = null | Leaf1 + +export type _RemovePath = { + readonly first: _Leaf01, + readonly tail: Path +} + +export type _Branch = Branch1 | Branch3 | Branch5 + +export type _Merge = (a: A) => (n: Branch3) => Branch1 | Branch3 diff --git a/fjs/types/btree/set/module.f.mjs b/fjs/types/btree/set/module.f.mjs index d65153747..56595b082 100644 --- a/fjs/types/btree/set/module.f.mjs +++ b/fjs/types/btree/set/module.f.mjs @@ -13,15 +13,10 @@ import { find } from '../find/module.f.mjs' import { fold } from '../../list/module.f.mjs' import { assert } from '../../../asserts/module.f.mjs' -/** - * @template T - * @typedef {Branch1 | Branch3} _Branch1To3 - */ - -/** @type {(b: Branch5 | Branch7) => _Branch1To3} */ +/** @type {(b: Branch5 | Branch7) => Branch1 | Branch3} */ const b57 = b => b.length === 5 ? [b] : [[b[0], b[1], b[2]], b[3], [b[4], b[5], b[6]]] -/** @type {(i: PathItem) => (a: _Branch1To3) => _Branch1To3} */ +/** @type {(i: PathItem) => (a: Branch1 | Branch3) => Branch1 | Branch3} */ const reduceOp = ([i, x]) => a => { switch (i) { case 0: { @@ -57,7 +52,7 @@ const nodeSet = c => g => node => { // readonly[1|3, Branch5] /** @type {First} */ const [i, x] = first - /** @type {() => _Branch1To3} */ + /** @type {() => Branch1 | Branch3} */ const f = () => { switch (i) { case 0: { diff --git a/fjs/types/byte_set/module.f.mjs b/fjs/types/byte_set/module.f.mjs index d230c264a..96b0aa0dd 100644 --- a/fjs/types/byte_set/module.f.mjs +++ b/fjs/types/byte_set/module.f.mjs @@ -5,14 +5,12 @@ * @module * * @import { RangeMap } from '../range_map/types.ts' - * @import { ByteSet } from './types.ts' + * @import { ByteSet, _Byte } from './types.ts' */ import { compose } from '../function/module.f.mjs' import { reverse, countdown, flat, map } from '../list/module.f.mjs' -/** @typedef {number} _Byte */ - /** @type {(n: _Byte) => (s: ByteSet) => boolean} */ export const has = n => s => ((s >> BigInt(n)) & 1n) === 1n diff --git a/fjs/types/byte_set/types.ts b/fjs/types/byte_set/types.ts index de4ce510e..b43392007 100644 --- a/fjs/types/byte_set/types.ts +++ b/fjs/types/byte_set/types.ts @@ -5,3 +5,6 @@ */ export type ByteSet = bigint + +/** A member of a `ByteSet`: an unsigned integer below 256. */ +export type _Byte = number diff --git a/fjs/types/function/todo/uncurry-accumulator-types.md b/fjs/types/function/todo/uncurry-accumulator-types.md index 912aa0408..573c36643 100644 --- a/fjs/types/function/todo/uncurry-accumulator-types.md +++ b/fjs/types/function/todo/uncurry-accumulator-types.md @@ -10,13 +10,14 @@ Several sibling accumulator types still curry their data parameters, contradicting that precedent: ```ts -// fjs/types/function/operator/module.f.mjs +// fjs/types/function/operator/types.ts export type Fold = Binary // (input: I) => (acc: O) => O export type Reduce = Fold // (value: T) => (acc: T) => T -// fjs/types/sorted_list/module.f.mjs -/** @typedef {(state: S) => (a: T) => (b: T) => readonly [Nullable, Sign, S]} ReduceOp */ -/** @typedef {(state: S) => (tail: List) => List} TailReduce */ +// fjs/types/sorted_list/types.ts +export type ReduceOp = + (state: S) => (a: T) => (b: T) => readonly [Nullable, Sign, S] +export type TailReduce = (state: S) => (tail: List) => List ``` ### Proposal diff --git a/fjs/types/number/module.f.mjs b/fjs/types/number/module.f.mjs index 650917b05..7dfebe1e3 100644 --- a/fjs/types/number/module.f.mjs +++ b/fjs/types/number/module.f.mjs @@ -32,9 +32,7 @@ export const max = reduce(maxReduce)(null) /** @type {(a: number) => (b: number) => Sign} */ export const cmp = uCmp -/** @typedef {readonly [number, number]} _MaskOffset */ - -/** @type {readonly _MaskOffset[]} */ +/** @type {readonly (readonly [number, number])[]} */ const mo = [ [0x5555_5555, 1], [0x3333_3333, 2], diff --git a/fjs/types/object/proof.f.mjs b/fjs/types/object/proof.f.mjs index 9d8d2cbd4..3f0d04b4f 100644 --- a/fjs/types/object/proof.f.mjs +++ b/fjs/types/object/proof.f.mjs @@ -7,15 +7,13 @@ import { at } from './module.f.mjs' import { assertEq } from '../../asserts/module.f.mjs' -/** @typedef {Assert, { readonly [k in string]?: bigint }>>} _StringMapIsOptional */ - -/** @typedef {Assert, { readonly a?: bigint; readonly b?: bigint }>>} _OptionalIsPartial */ - -/** @typedef {Assert, { readonly a: bigint; readonly b: bigint }>>} _RequiredIsRequired */ - -/** @typedef {Assert, never>>} _RequiredOverAnyStringIsNever */ - export const proof = { + maps: () => { + /** @typedef {Assert, { readonly [k in string]?: bigint }>>} _StringMapIsOptional */ + /** @typedef {Assert, { readonly a?: bigint; readonly b?: bigint }>>} _OptionalIsPartial */ + /** @typedef {Assert, { readonly a: bigint; readonly b: bigint }>>} _RequiredIsRequired */ + /** @typedef {Assert, never>>} _RequiredOverAnyStringIsNever */ + }, ctor: () => { const a = {} const value = at('constructor')(a) diff --git a/fjs/types/patricia_trie/proof.f.mjs b/fjs/types/patricia_trie/proof.f.mjs index e7a44a266..c303f3811 100644 --- a/fjs/types/patricia_trie/proof.f.mjs +++ b/fjs/types/patricia_trie/proof.f.mjs @@ -5,12 +5,10 @@ import { assert, assertEq } from '../../asserts/module.f.mjs' import { emptyState, patriciaTrie } from './module.f.mjs' -/** @typedef {readonly [bigint, bigint, bigint][]} _NodeList */ - /** @type {(a: bigint, b: bigint) => bigint} */ const combine = (a, b) => a * 1_000n + b -/** @type {(a: bigint, b: bigint, s: _NodeList) => readonly [bigint, _NodeList]} */ +/** @type {(a: bigint, b: bigint, s: readonly [bigint, bigint, bigint][]) => readonly [bigint, readonly [bigint, bigint, bigint][]]} */ const create = (a, b, s) => { const h = combine(a, b) return [h, [...s, [a, b, h]]] @@ -18,12 +16,12 @@ const create = (a, b, s) => { const { push, end } = patriciaTrie(create) -/** @type {(state: State<_NodeList, bigint>) => readonly bigint[]} */ +/** @type {(state: State) => readonly bigint[]} */ const leaves = ([, candidates]) => candidates.map(([leaf]) => leaf) /** @type {(inputs: readonly bigint[], expectedLeaves: readonly (readonly bigint[])[], expectedNodeCounts: readonly number[]) => void} */ const runExample = (inputs, expectedLeaves, expectedNodeCounts) => { - /** @type {State<_NodeList, bigint>} */ + /** @type {State} */ let state = emptyState([]) for (let i = 0; i < inputs.length; i++) { const x = inputs[i] diff --git a/fjs/types/range_map/module.f.mjs b/fjs/types/range_map/module.f.mjs index 71515cf4a..80a38ef90 100644 --- a/fjs/types/range_map/module.f.mjs +++ b/fjs/types/range_map/module.f.mjs @@ -47,13 +47,11 @@ import { next } from '../list/module.f.mjs' import { cmp } from '../number/module.f.mjs' import { bsearch } from '../function/compare/module.f.mjs' -/** @template T @typedef {Nullable>} _RangeState */ - const reduceOp = /** * @template T * @param {Properties} p - * @returns {ReduceOp, _RangeState>} + * @returns {ReduceOp, Nullable>>} */ ({ union, equal }) => state => ([aItem, aMax]) => ([bItem, bMax]) => { const sign = cmp(aMax)(bMax) @@ -67,7 +65,7 @@ const tailReduce = /** * @template T * @param {Equal} equal - * @returns {TailReduce, _RangeState>} + * @returns {TailReduce, Nullable>>} */ equal => state => tail => { if (state === null) { return tail } diff --git a/fjs/types/sorted_list/module.f.mjs b/fjs/types/sorted_list/module.f.mjs index 3838149ea..effdadb2b 100644 --- a/fjs/types/sorted_list/module.f.mjs +++ b/fjs/types/sorted_list/module.f.mjs @@ -12,8 +12,6 @@ import { bsearch } from '../function/compare/module.f.mjs' import { next } from '../list/module.f.mjs' import { identity } from '../function/module.f.mjs' -/** @template T @typedef {readonly T[]} _SortedArray */ - /** * Two-way sorted-list merge. * `reduceOp` returns `[output, sign, nextState]` where sign `-1` advances `a`, `1` advances `b`, `0` advances both; `null` output skips emission. @@ -46,8 +44,6 @@ export const genericMerge = return f } -/** @template T @typedef {ReduceOp} _CmpReduceOp */ - export const merge = /** * @template T @@ -60,7 +56,7 @@ const cmpReduce = /** * @template T * @param {Cmp} cmp - * @returns {_CmpReduceOp} + * @returns {ReduceOp} */ cmp => () => a => b => { const sign = cmp(a)(b) @@ -111,7 +107,7 @@ export const find = cmp => /** @param {T} value */ value => - /** @param {_SortedArray} array */ + /** @param {readonly T[]} array */ array => { const cmpValue = cmp(value) const pos = bsearch(array.length)(mid => cmpValue(array[mid])) diff --git a/fjs/web/module.f.mjs b/fjs/web/module.f.mjs index cc4642695..d212cbe1e 100644 --- a/fjs/web/module.f.mjs +++ b/fjs/web/module.f.mjs @@ -125,16 +125,6 @@ const percentDecode = s => { return utf8String([...utf8Bytes(literal), ...escaped.flatMap(escapeBytes)]) } -/** - * A request target, split into the two parts that decide the answer. - * - * `authority` is the host the *target* names, which only an absolute-form target - * carries; `null` says the target named none, and the `Host` header is then the - * only thing that does. - * - * @typedef {{ readonly authority: Nullable, readonly path: string }} _Target - */ - /** What separates a scheme from the authority that follows it. * * @type {string} @@ -178,7 +168,7 @@ const portMark = ':' * The fragment is stripped although a client keeps it to itself; a `respond` * called directly might still be given one, and it costs one `split`. * - * @type {(target: string) => Nullable<_Target>} + * @type {(target: string) => Nullable<{ readonly authority: Nullable, readonly path: string }>} */ const parseTarget = target => { const [beforeFragment] = target.split('#') @@ -291,20 +281,16 @@ export const resolve = root => url => { * A file too large to answer with. `readFile` yields a single `Vec`, so this is * a limit of the effect rather than a policy: see the README. * - * @typedef {readonly['tooLarge', number]} _TooLarge + * @type {(size: number) => readonly['tooLarge', number]} */ - -/** @type {(size: number) => _TooLarge} */ const tooLarge = size => ['tooLarge', size] /** * An entry that is not a regular file — a FIFO, a device, a socket. It exists, * so this is not a missing path, and it is not something this server will read. * - * @typedef {readonly['notRegular']} _NotRegular + * @type {readonly['notRegular']} */ - -/** @type {_NotRegular} */ const notRegular = ['notRegular'] /** @@ -468,7 +454,7 @@ const methodNotAllowed = () => { * stall every other response. Size cannot stand in for that check, because a * FIFO stats as zero bytes and passes every bound. * - * @type {(path: string) => (s: FileStat) => Effect} + * @type {(path: string) => (s: FileStat) => Effect} */ const readBounded = path => ({ size, isFile }) => { if (!isFile) { return pureError(notRegular) } @@ -480,7 +466,7 @@ const readBounded = path => ({ size, isFile }) => { * error channel ends: every failure becomes a status code, which is what lets * a `RequestListener` declare `never`. * - * @type {(path: string) => (r: Result) => ServerResponse} + * @type {(path: string) => (r: Result) => ServerResponse} */ const fileResponse = path => r => { if (r[0] === 'ok') { return response(200)(detectPath(path))(r[1]) } @@ -544,7 +530,7 @@ const isServableRoot = s => s[0] === 'ok' && s[1].isDirectory * [stat-then-read](./todo/stat-then-read.md) already describes: a wrong status * in a vanishing window rather than a wrong status forever. * - * @type {(root: string) => (path: string) => (r: Result) => Effect} + * @type {(root: string) => (path: string) => (r: Result) => Effect} */ const answer = root => path => r => { const hostAnswer = fileResponse(path)(r) diff --git a/fjs/website/browser-prepare.mjs b/fjs/website/browser-prepare.mjs index 1515d18c7..e2e026adb 100644 --- a/fjs/website/browser-prepare.mjs +++ b/fjs/website/browser-prepare.mjs @@ -25,8 +25,6 @@ const files = async directory => { }))).flat() } -/** @typedef {{ readonly blockers: readonly string[], readonly local: readonly URL[] }} _Module */ - /** * Reads the modules reachable from `frontier` one level at a time, recording * for each the bare and `node:` specifiers that would keep a browser from @@ -39,7 +37,10 @@ const files = async directory => { * that were never its own. A genuinely missing relative import cannot survive * anyway, since the proof suite loads every one of these modules in Node. * - * @type {(frontier: readonly URL[], graph: ReadonlyMap) => Promise>} + * @type {( + * frontier: readonly URL[], + * graph: ReadonlyMap, + * ) => Promise>} */ const readGraph = async (frontier, graph) => { const next = frontier.filter(url => !graph.has(url.href)) @@ -60,7 +61,7 @@ const readGraph = async (frontier, graph) => { * The blockers reachable from `root`, deduplicated. Empty means the whole * dependency graph is plain relative ES modules, which a browser can link. * - * @type {(graph: ReadonlyMap, root: URL) => readonly string[]} + * @type {(graph: Awaited>, root: URL) => readonly string[]} */ const blockersOf = (graph, root) => { /** @type {(frontier: readonly string[], visited: ReadonlySet) => ReadonlySet} */ diff --git a/fjs/website/browser-source.mjs b/fjs/website/browser-source.mjs index 3c7b2a871..980f3d3ac 100644 --- a/fjs/website/browser-source.mjs +++ b/fjs/website/browser-source.mjs @@ -25,8 +25,6 @@ const nameChar = char => /** @type {(char: string) => boolean} */ const space = char => char === ' ' || char === '\t' || char === '\n' || char === '\r' -/** @typedef {{ readonly kind: 'name' | 'string' | 'punctuation', readonly text: string }} _Token */ - /** * Separates tokens while they are collected. The scan is a single pass over a * whole file, so tokens accumulate as text rather than into a growing array; @@ -43,7 +41,7 @@ const separator = '\u0000' * An escape inside a string becomes a space: escapes belong to prose, and a * module specifier — the only string this module reads — has none. * - * @type {(source: string) => readonly _Token[]} + * @type {(source: string) => readonly { readonly kind: 'name' | 'string' | 'punctuation', readonly text: string }[]} */ const read = source => { let out = '' @@ -100,7 +98,7 @@ const read = source => { * The tokens as bare words, every string literal standing in as a quote: a * declaration is read by its names, and no string can pass for one. * - * @type {(tokens: readonly _Token[]) => readonly string[]} + * @type {(tokens: ReturnType) => readonly string[]} */ const words = tokens => tokens.map(token => token.kind === 'string' ? '\'' : token.text) diff --git a/todo/blocked/jsdoc-typedef-doc-declaration-emit.md b/todo/blocked/jsdoc-typedef-doc-declaration-emit.md index c141d98d4..9a8e4e782 100644 --- a/todo/blocked/jsdoc-typedef-doc-declaration-emit.md +++ b/todo/blocked/jsdoc-typedef-doc-declaration-emit.md @@ -1,5 +1,10 @@ # JSDoc `@typedef` documentation is dropped by tsgo declaration emit +> Authored `.mjs` no longer carries file-scope `@typedef`s +> ([`../../fjs/todo/separate-private-types.md`](../../fjs/todo/separate-private-types.md)), +> so no authored typedef documentation reaches declaration emit any more; this +> upstream behavior matters again only if that rule is ever relaxed. + **Priority:** P2 **Status:** blocked @@ -208,8 +213,8 @@ Body: - [`todo/migrate-typescript-to-mjs.md`](../migrate-typescript-to-mjs.md) — "Typedef documentation does not survive declaration emit". -- [`jsdoc-typedef-strip-internal.md`](./jsdoc-typedef-strip-internal.md) — - the adjacent `@internal` + `stripInternal` gap for JSDoc typedefs. +- [`../../fjs/todo/separate-private-types.md`](../../fjs/todo/separate-private-types.md) + — private-type placement; superseded the wait-for-`@internal` strategy. - [microsoft/TypeScript#43534](https://github.com/microsoft/TypeScript/issues/43534), [microsoft/TypeScript#61664](https://github.com/microsoft/TypeScript/issues/61664) — adjacent strada behaviors. diff --git a/todo/blocked/jsdoc-typedef-strip-internal.md b/todo/blocked/jsdoc-typedef-strip-internal.md deleted file mode 100644 index fadfd6d1f..000000000 --- a/todo/blocked/jsdoc-typedef-strip-internal.md +++ /dev/null @@ -1,113 +0,0 @@ -# Use `@internal` for private JSDoc typedefs - -**Priority:** P3 -**Status:** blocked - -### Problem - -During the TypeScript-to-JavaScript migration, implementation-only TypeScript -types become JSDoc `@typedef`s. TypeScript currently emits those typedefs as -exported type aliases in generated declarations even when they are not intended -to be public API. - -Until the declaration emitter can strip private JSDoc typedefs, the repository -uses a leading `_` as an API convention: a typedef such as `_Node` is private by -contract even if the generated `.d.ts` / `.d.mts` contains `export type _Node`. -Consumers must not depend on that emitted name directly, so renaming or removing -the alias is not a breaking change solely because it was emitted. This does not -exempt changes propagated into public types: if a public declaration depends on -`_Node`, any change that alters that public declaration's assignability remains -a breaking API change. - -The desired long-term representation is `@internal` plus `stripInternal`, so the -generated declaration does not expose the private type at all. - -### Trigger - -Unblocked when the TypeScript compiler used by this repository supports applying -`@internal` to JSDoc `@typedef` declarations and `stripInternal` reliably omits -those typedefs from generated `.d.ts` / `.d.mts` files. - -The canonical blocker is -[microsoft/TypeScript#46407](https://github.com/microsoft/TypeScript/issues/46407), -which is still open and specifically requests `stripInternal` support for types -defined with JSDoc. - -Another open TypeScript declaration/comment-emission issue, -[microsoft/TypeScript#62453](https://github.com/microsoft/TypeScript/issues/62453), -demonstrates the same JSDoc typedef-to-`export type` emission path while tracking -duplicated typedef comments. It is related context, not the visibility blocker. - -A separate equivalent TypeScript 7 / Go issue was not found in -`microsoft/typescript-go`. The native compiler does implement `stripInternal` -in general, but the known JSDoc declaration-emission reports still show -`@typedef`s becoming exported aliases. Related TypeScript-Go issues are: - -- [microsoft/typescript-go#4363](https://github.com/microsoft/typescript-go/issues/4363) - — open; emitted JSDoc typedef aliases and their documentation ordering. -- [microsoft/typescript-go#4235](https://github.com/microsoft/typescript-go/issues/4235) - — closed; JSDoc typedef/property documentation in declaration emit. -- [microsoft/typescript-go#4011](https://github.com/microsoft/typescript-go/issues/4011) - — closed; correctness of generated declaration syntax for JSDoc typedefs. - -These TypeScript-Go issues are adjacent declaration-emitter bugs, not substitutes -for #46407. Re-check the current TypeScript tracker when this task is unblocked, -especially as the native compiler work is consolidated with the main TypeScript -project. - -### Proposal - -Once the trigger is satisfied: - -1. enable or retain `stripInternal` for declaration emission; -2. mark implementation-only JSDoc typedefs with `@internal`; -3. remove leading `_` from private typedef names where the prefix exists only as - the current visibility workaround; -4. add a package/declaration fixture proving that private typedefs are absent - from emitted declarations while public declarations remain valid; -5. update migration, compiler, package, and contributor documentation to remove - the underscore workaround. - -Do not strip a private typedef if a public declaration still depends on its name; -refactor the public declaration first so emitted declarations remain -self-contained and preserve the same public assignability contract. - -### Tasks - -- [ ] Enable or retain `stripInternal` for declaration emission once the trigger - is satisfied. -- [ ] Mark implementation-only JSDoc typedefs with `@internal`. -- [ ] Remove leading `_` from private typedef names where the prefix exists only - as the temporary visibility workaround. -- [ ] Refactor public declarations that refer to private typedef names so they - remain self-contained and preserve the same public assignability contract - before those private typedefs are stripped. -- [ ] Add a package/declaration fixture proving that private typedefs are absent - from emitted declarations while public declarations remain valid. -- [ ] Update migration, compiler, package, and contributor documentation to - remove the underscore workaround. - -### Acceptance criteria - -- `@internal` on a JSDoc `@typedef` is honored by the repository's TypeScript - declaration emitter when `stripInternal` is enabled. -- Generated `.d.ts` / `.d.mts` files omit implementation-only typedefs. -- Public emitted declarations never reference a stripped private type. -- Removing the `_` workaround does not weaken or otherwise change public - assignability unless that change is explicitly treated as breaking. -- The `_`-prefix workaround is removed from repository documentation and from - private typedefs that used it solely for visibility. -- Clean package-consumer type checking still passes. - -### Related - -- [`../migrate-typescript-to-mjs.md`](../migrate-typescript-to-mjs.md) — Stage 1 - TypeScript-to-JSDoc migration and the temporary `_` convention. -- [`../../fjs/fsc/README.md`](../../fjs/fsc/README.md) — source migration and - JSDoc visibility contract. -- [`../../fjs/ci/todo/f-mjs-package-support.md`](../../fjs/ci/todo/f-mjs-package-support.md) - — declaration-emission and clean-consumer validation. -- [microsoft/TypeScript#46407](https://github.com/microsoft/TypeScript/issues/46407) - — canonical upstream feature request. -- [microsoft/TypeScript#62453](https://github.com/microsoft/TypeScript/issues/62453) - — related JSDoc typedef declaration/comment emission bug. diff --git a/todo/migrate-typescript-to-mjs.md b/todo/migrate-typescript-to-mjs.md index 0065dce91..6d29eb8bf 100644 --- a/todo/migrate-typescript-to-mjs.md +++ b/todo/migrate-typescript-to-mjs.md @@ -340,45 +340,41 @@ consumer all work; that is tracked in #### Preserve private type intent with `_` -A non-exported TypeScript type that is translated into a JavaScript `@typedef` -can become externally visible merely because TypeScript currently emits JSDoc -typedefs as exported aliases. The upstream request to make `@internal` plus -`stripInternal` work for JSDoc typedefs is -[microsoft/TypeScript#46407](https://github.com/microsoft/TypeScript/issues/46407). - -Until that support is available, prefix implementation-only **JSDoc typedef** -names with `_` during migration. For example: +A named type migrating out of a `.f.ts` never becomes a **file-scope** JSDoc +`@typedef` — authored `.mjs` files carry none, repository-wide (root +`AGENTS.md`; design in +[`../fjs/todo/separate-private-types.md`](../fjs/todo/separate-private-types.md)). +It lands in the sibling `types.ts` when it is part of the public declaration +closure, in an optional sibling `private.ts` when it is implementation-private +and separating it reads cleaner than inlining, inline in the annotations that +use it, or function-local in a proof when it is a compile-time proof type. For +example: ```ts type Node = number export type Tree = readonly Node[] ``` -becomes conceptually: +becomes, in `types.ts`: -```js -/** @typedef {number} _Node */ -/** @typedef {readonly _Node[]} Tree */ +```ts +export type _Node = number +export type Tree = readonly _Node[] ``` -The leading `_` is the FunctionalScript API visibility convention. It does not -prevent declaration emission, so generated declarations may contain -`export type _Node = number`. `_Node` is still private by contract: consumers -must not depend on that emitted name directly, so renaming or removing `_Node` -is not a breaking change solely because TypeScript exposed the alias. +The leading `_` is the FunctionalScript API visibility convention, kept even +when linkage requires an export: `_Node` is private by contract, so consumers +must not depend on the name directly, and renaming or removing `_Node` is not a +breaking change solely because a declaration exposed it. The public contract still governs transitive effects. In the example above, `Tree` is public and depends on `_Node`; changing `_Node` from `number` to `string` changes `Tree`'s public assignability and is therefore a breaking change. The underscore exempts only the private alias itself, never a change to -the expanded public API. Public typedefs keep ordinary names without a leading +the expanded public API. Public types keep ordinary names without a leading `_`. -Types intentionally separated into `types.ts` use ordinary TypeScript source -visibility and syntax and do not need the JSDoc underscore workaround merely -because they remain TypeScript. - -Which JSDoc typedefs are public is an API design decision made at the migration +Which types are public is an API design decision made at the migration boundary, not a mechanical copy of what the `.f.ts` happened to export. The `.f.ts` -> `.f.mjs` rename is already a breaking change — importers must update the specifier — so it is the one moment where a module's JSDoc visibility @@ -402,12 +398,19 @@ plans to remove both. Hiding a type behind `_` to make its eventual removal cheaper gives up a real present-day API in exchange for a discount on a breaking change that should simply be documented when it happens. -This convention is temporary. Once TypeScript can strip `@internal` JSDoc -typedefs correctly, replace the underscore workaround as tracked by -[`blocked/jsdoc-typedef-strip-internal.md`](./blocked/jsdoc-typedef-strip-internal.md). +Unshipping generated private declaration artifacts is the packaging stage of +[`../fjs/todo/separate-private-types.md`](../fjs/todo/separate-private-types.md); +the `_` contract itself is permanent. #### Typedef documentation does not survive declaration emit +> Since the repository-wide prohibition on file-scope `@typedef` in authored +> `.mjs` ([`../fjs/todo/separate-private-types.md`](../fjs/todo/separate-private-types.md)), +> named types live in `types.ts`/`private.ts`, whose documentation emits +> through the normal TypeScript pipeline — so this loss no longer affects +> authored code. The record below explains the behavior and why the +> prohibition avoids it. + The same upstream gap has a second, opposite-facing symptom: declaration emit drops the documentation written on a JSDoc `@typedef`. A TypeScript `/** 8-word SHA-2 state vector. */ export type V8 = …` keeps its comment in the @@ -866,12 +869,13 @@ blocking, plus the prose sweep. The remaining items are listed under emitted declarations measure zero `elided` repo-wide after it. (Its Phantom `$out` intentionally differs from `Ts` in field optionality, so no exact `Equal` round-trip assert applies there.) -- [ ] Decide each JSDoc typedef's visibility at the migration boundary: prefix - implementation-only typedefs with `_` and leave publicly useful ones +- [ ] Decide each migrated type's visibility at the migration boundary: prefix + implementation-only types with `_` and leave publicly useful ones unprefixed, judged by what the module should offer its consumers rather than by what the `.f.ts` happened to export or by what a pending refactor - plans to delete. Types intentionally moved to `types.ts` use normal - TypeScript source visibility instead. + plans to delete. Place each per the file-scope-typedef prohibition: + `types.ts` for the public declaration closure, optional `private.ts`, + inline, or function-local in a proof. - [x] Apply the module-header/import convention: `@module` belongs only to `module.*` entry-point files, never to `proof.*` or other files; group module-level JavaScript `@import` tags into one leading JSDoc block — @@ -1250,9 +1254,9 @@ person can re-check rather than re-derive. Counts are as of — broader package-publishing plan. - [`../fjs/fsc/README.md`](../fjs/fsc/README.md) — authoritative FunctionalScript extension and migration contract. -- [`blocked/jsdoc-typedef-strip-internal.md`](./blocked/jsdoc-typedef-strip-internal.md) - — replace the temporary `_` convention with `@internal` when upstream - declaration emit supports it. +- [`../fjs/todo/separate-private-types.md`](../fjs/todo/separate-private-types.md) + — private-type placement rules and the packaging stage that unships + generated private declarations. - [microsoft/TypeScript#46407](https://github.com/microsoft/TypeScript/issues/46407) — upstream request for `stripInternal` support on JSDoc typedefs. - [`fjs-nanvm-integration.md`](./fjs-nanvm-integration.md) — existing compiler diff --git a/todo/proof.f.mjs b/todo/proof.f.mjs index 311694345..b4c75b5a7 100644 --- a/todo/proof.f.mjs +++ b/todo/proof.f.mjs @@ -1,5 +1,3 @@ -/** @typedef {`${string}`} _TemplateType */ - /** @type {(a: unknown) => (i: any) => unknown} */ const at = a => i => Object.getOwnPropertyDescriptor(a, i)?.value @@ -8,6 +6,7 @@ const utf8 = (...x) => x export const proof = { literal: () => { + /** @typedef {`${string}`} _TemplateType */ const x = utf8`17` /** @type {_TemplateType} */ const m = 'Hello' diff --git a/todo/rtti-type-system.md b/todo/rtti-type-system.md index 06146a7f4..7de1974d5 100644 --- a/todo/rtti-type-system.md +++ b/todo/rtti-type-system.md @@ -9,13 +9,13 @@ file; this one is where they are read together. ### Problem -A type in this repository is written more than once. The same shape is a JSDoc -`@typedef`, a declaration in a sibling `types.ts`, and — where a value has to be -checked at run time — an [RTTI](../fjs/rtti/README.md) schema. Nothing -keeps the three in agreement: `tsc` checks the first two against the code and -the third against nothing, so a schema and its `@typedef` drift silently, and -the drift shows up as a value that type-checks and fails validation, or the -reverse. +A type in this repository is written more than once. The same shape is a +declaration in a sibling `types.ts` (or `private.ts`), the JSDoc annotations +that name it, and — where a value has to be checked at run time — an +[RTTI](../fjs/rtti/README.md) schema. Nothing keeps them in agreement: `tsc` +checks the declaration against the code and the schema against nothing, so a +schema and its declared type drift silently, and the drift shows up as a value +that type-checks and fails validation, or the reverse. The bridge that exists runs the wrong way. `Ts` ([`fjs/rtti/ts/README.md`](../fjs/rtti/ts/README.md)) maps a schema