diff --git a/CHANGELOG.md b/CHANGELOG.md index d137b4d8af..f9ff2a8b20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,37 @@ history. ## Unreleased +- **BREAKING CHANGES:** `fjs/effects/list` migrates from authored + TypeScript (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`), splitting + its type-level API into a sibling `types.ts` — importers must use + the `.f.mjs` specifier for runtime values and the `types.ts` + specifier for types + [#1487](https://github.com/functionalscript/functionalscript/pull/1487) +- **BREAKING CHANGES:** `fjs/effects/module.f.ts` migrates from + authored TypeScript (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`), + splitting its type-level API into a sibling `types.ts` — importers + must use the `.f.mjs` specifier for runtime values and the + `types.ts` specifier for types. Updates all 30+ dependents across + the repo; `proof.f.ts` stays TypeScript for now + [#1487](https://github.com/functionalscript/functionalscript/pull/1487) +- **BREAKING CHANGES:** `fjs/bnf/descent` migrates from authored + TypeScript (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`), splitting + its type-level API into a sibling `types.ts` — importers must use + the `.f.mjs` specifier for runtime values and the `types.ts` + specifier for types. `proof.f.ts` stays TypeScript for now + [#1487](https://github.com/functionalscript/functionalscript/pull/1487) +- **BREAKING CHANGES:** `fjs/bnf/ll1` migrates from authored TypeScript + (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`), splitting its + type-level API into a sibling `types.ts` — importers must use the + `.f.mjs` specifier for runtime values and the `types.ts` specifier + for types. `proof.f.ts` stays TypeScript for now + [#1487](https://github.com/functionalscript/functionalscript/pull/1487) +- **BREAKING CHANGES:** `fjs/bnf/data` migrates from authored TypeScript + (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`), splitting its + type-level API into a sibling `types.ts` — importers must use the + `.f.mjs` specifier for runtime values and the `types.ts` specifier + for types. `proof.f.ts` stays TypeScript for now + [#1487](https://github.com/functionalscript/functionalscript/pull/1487) - **BREAKING CHANGES:** `fjs/bnf/token_symbol` migrates from authored TypeScript (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`), splitting the `Encoding` type into a sibling `types.ts` — importers must use diff --git a/fjs/bnf/data/module.f.ts b/fjs/bnf/data/module.f.mjs similarity index 66% rename from fjs/bnf/data/module.f.ts rename to fjs/bnf/data/module.f.mjs index 36237655de..720f5d7ce6 100644 --- a/fjs/bnf/data/module.f.ts +++ b/fjs/bnf/data/module.f.mjs @@ -8,6 +8,8 @@ * {@link RuleSet} live in their own sibling modules (`fjs/bnf/ll1`, * `fjs/bnf/descent`, …), so the IR stays free of any one parser's machinery. * + * See `./types.ts` for the type-level API. + * * @module */ import { stringToCodePointList } from '../../text/utf16/module.f.mjs' @@ -15,58 +17,22 @@ import { map, toArray } from '../../types/list/module.f.mjs' import { oneEncode, } from '../module.f.mjs' -import type { - DataRule, - Rule as FRule, - Sequence as FSequence, -} from '../types.ts' +/** @import { DataRule, Rule as FRule, Sequence as FSequence } from '../types.ts' */ import { definedEntries } from '../../types/object/module.f.mjs' -import type { StringMap } from '../../types/object/types.ts' - -/** - * Encoded terminal range value used by BNF data rules. - * - * The same as the functional TerminalRange. - */ -export type TerminalRange = number - -/** - * Ordered list of grammar rule names. - */ -export type Sequence = readonly string[] - -/** A variant of rule names. */ -export type Variant = StringMap - -/** - * Grammar rule definition. - * - * It can be one of: - * - a tagged variant map, - * - a sequence of referenced rule names, - * - an encoded terminal range. - */ -export type Rule = Variant | Sequence | TerminalRange - -/** The full grammar */ -export type RuleSet = Readonly> - -/** - * Whether a rule can match empty input: `undefined` if it never can, `true` - * if it can with no tag (a nullable sequence), or the tag of the nullable - * variant branch. - */ -export type EmptyTag = string | true | undefined +/** @import { StringMap } from '../../types/object/types.ts' */ +/** @import { EmptyTag, Rule, RuleSet, Sequence, Variant } from './types.ts' */ -type _EmptyTagMap = StringMap +/** @typedef {StringMap} _EmptyTagMap */ -const emptyTagOf = (map: _EmptyTagMap) => (rule: Rule): EmptyTag => { +/** @type {(map: _EmptyTagMap) => (rule: Rule) => EmptyTag} */ +const emptyTagOf = map => rule => { if (typeof rule === 'number') { return undefined } else if (rule instanceof Array) { return rule.every(item => map[item] !== undefined) ? true : undefined } else { - let tag: EmptyTag = undefined + /** @type {EmptyTag} */ + let tag = undefined for (const [k, item] of definedEntries(rule)) { if (map[item] !== undefined) { tag = k @@ -76,7 +42,8 @@ const emptyTagOf = (map: _EmptyTagMap) => (rule: Rule): EmptyTag => { } } -const emptyTagStep = (ruleSet: RuleSet) => (map: _EmptyTagMap): readonly [_EmptyTagMap, boolean] => { +/** @type {(ruleSet: RuleSet) => (map: _EmptyTagMap) => readonly [_EmptyTagMap, boolean]} */ +const emptyTagStep = ruleSet => map => { let next = map let changed = false for (const name in ruleSet) { @@ -101,10 +68,13 @@ const emptyTagStep = (ruleSet: RuleSet) => (map: _EmptyTagMap): readonly [_Empty * terminates — but a rule's tag can still change for rounds *after* its own * nullable/non-nullable status has already settled, while a cyclic * dependency's tag catches up, so a fixed round count isn't enough. + * + * @type {(ruleSet: RuleSet) => _EmptyTagMap} */ -export const emptyTagMap = (ruleSet: RuleSet): _EmptyTagMap => { +export const emptyTagMap = ruleSet => { const step = emptyTagStep(ruleSet) - const relax = (map: _EmptyTagMap): _EmptyTagMap => { + /** @type {(map: _EmptyTagMap) => _EmptyTagMap} */ + const relax = map => { const [next, changed] = step(map) return changed ? relax(next) : next } @@ -113,11 +83,12 @@ export const emptyTagMap = (ruleSet: RuleSet): _EmptyTagMap => { // -type _FRuleMap = StringMap +/** @typedef {StringMap} _FRuleMap */ const { entries } = Object -const find = (map: _FRuleMap) => (fr: FRule): string | undefined => { +/** @type {(map: _FRuleMap) => (fr: FRule) => string | undefined} */ +const find = map => fr => { for (const [k, v] of entries(map)) { if (v === fr) { return k @@ -126,7 +97,8 @@ const find = (map: _FRuleMap) => (fr: FRule): string | undefined => { return undefined } -const newName = (map: _FRuleMap, name: string) => { +/** @type {(map: _FRuleMap, name: string) => string} */ +const newName = (map, name) => { let i = 0 let result = name while (result in map) { @@ -136,10 +108,13 @@ const newName = (map: _FRuleMap, name: string) => { return result } -type _NewRule = (m: _FRuleMap) => readonly [_FRuleMap, RuleSet, Rule] +/** @typedef {(m: _FRuleMap) => readonly [_FRuleMap, RuleSet, Rule]} _NewRule */ -const sequence = (list: FSequence): _NewRule => map => { - let result: Sequence = [] +/** @type {(list: FSequence) => _NewRule} */ +const sequence = list => map => { + /** @type {Sequence} */ + let result = [] + /** @type {RuleSet} */ let set = {} for (const fr of list) { const [map1, set1, id] = toDataAdd(map)(fr) @@ -150,9 +125,12 @@ const sequence = (list: FSequence): _NewRule => map => { return [map, set, result] } -const variant = (fr: FRule): _NewRule => map => { - let set: RuleSet = {} - let rule: Variant = {} +/** @type {(fr: FRule) => _NewRule} */ +const variant = fr => map => { + /** @type {RuleSet} */ + let set = {} + /** @type {Variant} */ + let rule = {} for (const [k, v] of entries(fr)) { const [m1, s, id] = toDataAdd(map)(v) map = m1 @@ -164,7 +142,8 @@ const variant = (fr: FRule): _NewRule => map => { const mapOneEncode = map(oneEncode) -const data = (dr: DataRule): _NewRule => { +/** @type {(dr: DataRule) => _NewRule} */ +const data = dr => { switch (typeof dr) { case 'string': { return sequence(toArray(mapOneEncode(stringToCodePointList(dr)))) @@ -179,14 +158,16 @@ const data = (dr: DataRule): _NewRule => { } } -const toDataAdd = (map: _FRuleMap) => (fr: FRule): readonly [_FRuleMap, RuleSet, string] => { +/** @type {(map: _FRuleMap) => (fr: FRule) => readonly [_FRuleMap, RuleSet, string]} */ +const toDataAdd = map => fr => { { const id = find(map)(fr) if (id !== undefined) { return [map, {}, id] } } - const [dr, tmpId]: readonly [DataRule, string] = + /** @type {readonly [DataRule, string]} */ + const [dr, tmpId] = typeof fr === 'function' ? [fr(), fr.name] : [fr, ''] const newRule = data(dr) const id = newName(map, tmpId) @@ -198,8 +179,10 @@ const toDataAdd = (map: _FRuleMap) => (fr: FRule): readonly [_FRuleMap, RuleSet, /** * Converts a functional grammar rule into serializable BNF data and returns * the generated rule set with the entry rule identifier. + * + * @type {(fr: FRule) => readonly [RuleSet, string]} */ -export const toData = (fr: FRule): readonly [RuleSet, string] => { +export const toData = fr => { const [, ruleSet, id] = toDataAdd({})(fr) return [ruleSet, id] } diff --git a/fjs/bnf/data/proof.f.ts b/fjs/bnf/data/proof.f.ts index f50d4ee345..f41d8ae069 100644 --- a/fjs/bnf/data/proof.f.ts +++ b/fjs/bnf/data/proof.f.ts @@ -3,7 +3,8 @@ import { identity } from '../../types/function/module.f.mjs' import { sort } from '../../types/object/module.f.mjs' import { oneEncode, option, range, rangeDecode, repeat0Plus, set } from '../module.f.mjs' import { classic, deterministic } from '../testlib.f.ts' -import { emptyTagMap, type RuleSet, toData } from './module.f.ts' +import { emptyTagMap, toData } from './module.f.mjs' +import type { RuleSet } from './types.ts' import { assertEq } from '../../asserts/module.f.mjs' export const proof = { diff --git a/fjs/bnf/data/types.ts b/fjs/bnf/data/types.ts new file mode 100644 index 0000000000..42acc3ebb7 --- /dev/null +++ b/fjs/bnf/data/types.ts @@ -0,0 +1,42 @@ +/** + * Types for the serializable BNF intermediate representation (IR). + * + * @module + */ + +import type { StringMap } from '../../types/object/types.ts' + +/** + * Encoded terminal range value used by BNF data rules. + * + * The same as the functional TerminalRange. + */ +export type TerminalRange = number + +/** + * Ordered list of grammar rule names. + */ +export type Sequence = readonly string[] + +/** A variant of rule names. */ +export type Variant = StringMap + +/** + * Grammar rule definition. + * + * It can be one of: + * - a tagged variant map, + * - a sequence of referenced rule names, + * - an encoded terminal range. + */ +export type Rule = Variant | Sequence | TerminalRange + +/** The full grammar */ +export type RuleSet = Readonly> + +/** + * Whether a rule can match empty input: `undefined` if it never can, `true` + * if it can with no tag (a nullable sequence), or the tag of the nullable + * variant branch. + */ +export type EmptyTag = string | true | undefined diff --git a/fjs/bnf/descent/module.f.ts b/fjs/bnf/descent/module.f.mjs similarity index 61% rename from fjs/bnf/descent/module.f.ts rename to fjs/bnf/descent/module.f.mjs index 6ded72a26e..7a2587bf17 100644 --- a/fjs/bnf/descent/module.f.ts +++ b/fjs/bnf/descent/module.f.mjs @@ -11,144 +11,89 @@ * terminal was rejected at, which — unlike the result's own index — never * rewinds and is what diagnostics should be built from. * + * See `./types.ts` for the type-level API. + * * @module */ -import type { CodePoint } from '../../text/utf16/types.ts' import { rangeDecode } from '../module.f.mjs' -import type { TerminalRange } from '../types.ts' +/** @import { TerminalRange } from '../types.ts' */ import { contains as rangeContains } from '../../types/range/module.f.mjs' import { definedEntries } from '../../types/object/module.f.mjs' -import { emptyTagMap, toData, type Rule as DataRule, type Sequence } from '../data/module.f.ts' -import type { Rule as FRule } from '../types.ts' - -export type AstTag = string|true|undefined - -/** - * Recursive descent matcher for a single named rule. - */ -export type DescentMatchRule = (name: string, tag: AstTag, s: readonly CodePointMeta[], idx: number) => DescentMatchResult - -/** - * Where a match ran out of road, for diagnostics. - * - * `idx` is the furthest position any terminal was tried at and rejected, and - * `expected` holds the terminals that would have allowed progress there, in the - * order the grammar tried them and without repeats. - * - * Unlike a failed result's own index, this never rewinds: a failing sequence - * item rewinds the result to the sequence's start, while the furthest failure is - * a high-water mark over the whole match — including branches the grammar - * backtracked out of. That is what makes "expected X or Y at N" possible. - * - * `idx` is `0` with an empty `expected` when the match failed without ever - * rejecting a terminal, as an empty variant does. - */ -export type DescentFailure = { - readonly idx: number - readonly expected: readonly TerminalRange[] -} - -/** - * Result of a descent match operation. - * - * `failure` is present exactly when `success` is `false`: a successful match has - * nothing to diagnose, and its `idx` already says where matching stopped. Note - * the consequence for a match that succeeds *without consuming all input* — - * `idx` still locates the position it stopped at, but the terminals that would - * have let it continue are not reported. - * - * On failure `idx` has rewound to the start of the enclosing sequence and - * locates nothing; read `failure.idx` instead. - * - * The same type describes a match in progress, where `failure` is likewise - * absent until the match ends. - */ -export type DescentMatchResult = { - readonly ast: AstRuleMeta - readonly success: boolean - readonly idx: number - readonly failure?: DescentFailure -} +import { emptyTagMap, toData } from '../data/module.f.mjs' +/** @import { Rule as DataRule, Sequence } from '../data/types.ts' */ +/** @import { Rule as FRule } from '../types.ts' */ +/** @import { AstTag, AstSequenceMeta, DescentFailure, DescentMatch, DescentMatchResult, DescentMatchRule } from './types.ts' */ /** * Folds one rejected terminal into the furthest-failure record: further along * replaces, the same position accumulates (ignoring repeats), earlier is * discarded. + * + * @type {(failure: DescentFailure, idx: number, terminal: TerminalRange) => DescentFailure} */ -const recordFailure = (failure: DescentFailure, idx: number, terminal: TerminalRange): DescentFailure => { +const recordFailure = (failure, idx, terminal) => { if (idx > failure.idx) { return { idx, expected: [terminal] } } if (idx < failure.idx || failure.expected.includes(terminal)) { return failure } return { idx, expected: [...failure.expected, terminal] } } -/** - * Entry-point recursive descent matcher. - */ -export type DescentMatch = (name: string, s: readonly CodePointMeta[]) => DescentMatchResult - -/** - * Code point value paired with metadata. - */ -export type CodePointMeta = readonly[CodePoint, T] - -/** - * AST sequence for the metadata-aware parser. - */ -export type AstSequenceMeta = readonly(AstRuleMeta|CodePointMeta)[] - -/** - * Metadata-aware AST node. - */ -export type AstRuleMeta = { - readonly tag: AstTag, - readonly sequence: AstSequenceMeta -} - /** * Creates a recursive descent parser that preserves metadata for each consumed * code point. + * + * @template T + * @param {FRule} fr + * @returns {DescentMatch} */ -export const descentParser = (fr: FRule): DescentMatch => { +export const descentParser = fr => { const data = toData(fr) const emptyTags = emptyTagMap(data[0]) // A suspended sequence match: items[itemIndex] is being matched by the current // task; `seq` holds the ASTs of the items already matched. - type SeqFrame = { - readonly kind: 'seq' - readonly tag: AstTag - readonly items: Sequence - readonly itemIndex: number - readonly startIdx: number - readonly seq: AstSequenceMeta - } + /** + * @typedef {{ + * readonly kind: 'seq' + * readonly tag: AstTag + * readonly items: Sequence + * readonly itemIndex: number + * readonly startIdx: number + * readonly seq: AstSequenceMeta + * }} _SeqFrame + */ // A suspended variant match: entries[entryIndex] is being matched by the current // task; `emptyResult` is the best zero-consumption success seen so far (or the // initial failure), returned if no branch consumes input. - type VariantFrame = { - readonly kind: 'variant' - readonly entries: readonly (readonly [string, string])[] - readonly entryIndex: number - readonly idx: number - readonly emptyResult: DescentMatchResult - } - - type Frame = SeqFrame | VariantFrame + /** + * @typedef {{ + * readonly kind: 'variant' + * readonly entries: readonly (readonly [string, string])[] + * readonly entryIndex: number + * readonly idx: number + * readonly emptyResult: DescentMatchResult + * }} _VariantFrame + */ + + /** @typedef {_SeqFrame | _VariantFrame} _Frame */ // Immutable cons-cell stack: O(1) push/pop, no array copying per step. - type Stack = null | { - readonly top: Frame - readonly rest: Stack - } + /** + * @typedef {null | { + * readonly top: _Frame + * readonly rest: _Stack + * }} _Stack + */ // The rule invocation about to be evaluated (the recursive version's argument // tuple), or null when a result is ready to resume the innermost frame. - type Task = { - readonly name: string - readonly tag: AstTag - readonly idx: number - } + /** + * @typedef {{ + * readonly name: string + * readonly tag: AstTag + * readonly idx: number + * }} _Task + */ // The recursive-descent matcher as an explicit-stack machine: each iteration either // starts the current task (pushing a frame for a sequence/variant and descending into @@ -156,25 +101,33 @@ export const descentParser = (fr: FRule): DescentMatch => { // identical to the former recursive `f`, but the JS call stack stays O(1) regardless of // grammar recursion depth — right-recursive rules (e.g. repeat0Plus chains) no longer // overflow on long input (see the longInput proof group). - const f: DescentMatchRule = (name, tag, cp, idx): DescentMatchResult => { - const mrSuccess = (tag: AstTag, sequence: AstSequenceMeta, idx: number): DescentMatchResult => ({ ast: {tag, sequence}, success: true, idx }) - const mrFail = (tag: AstTag, sequence: AstSequenceMeta, idx: number): DescentMatchResult => ({ ast: {tag, sequence}, success: false, idx }) - - let stack: Stack = null - let task: Task | null = { name, tag, idx } - let result: DescentMatchResult = mrFail(undefined, [], idx) + /** @type {DescentMatchRule} */ + const f = (name, tag, cp, idx) => { + /** @type {(tag: AstTag, sequence: AstSequenceMeta, idx: number) => DescentMatchResult} */ + const mrSuccess = (tag, sequence, idx) => ({ ast: {tag, sequence}, success: true, idx }) + /** @type {(tag: AstTag, sequence: AstSequenceMeta, idx: number) => DescentMatchResult} */ + const mrFail = (tag, sequence, idx) => ({ ast: {tag, sequence}, success: false, idx }) + + /** @type {_Stack} */ + let stack = null + /** @type {_Task | null} */ + let task = { name, tag, idx } + /** @type {DescentMatchResult} */ + let result = mrFail(undefined, [], idx) // High-water mark across the whole match, so it survives the rewinds a // failing sequence item does to `result`. - let furthest: DescentFailure = { idx: 0, expected: [] } + /** @type {DescentFailure} */ + let furthest = { idx: 0, expected: [] } while (true) { if (task !== null) { - const { name, tag, idx }: Task = task - task = null // The explicit annotation cuts a control-flow inference cycle (TS7022): // `name`'s narrowed type feeds `rule`, whose type would otherwise feed the // later `task` assignments that `name`'s narrowing depends on. - const rule: DataRule = data[0][name] + const { name, tag, idx } = /** @type {_Task} */ (task) + task = null + /** @type {DataRule} */ + const rule = data[0][name] if (typeof rule === 'number') { // No nullable case: `emptyTagOf` in `bnf/data` returns `undefined` // for every terminal, so `emptyTags[name]` here is always @@ -248,7 +201,8 @@ export const descentParser = (fr: FRule): DescentMatch => { } } - const match: DescentMatch = (name, cp): DescentMatchResult => { + /** @type {DescentMatch} */ + const match = (name, cp) => { return f(name, undefined, cp, 0) } diff --git a/fjs/bnf/descent/proof.f.ts b/fjs/bnf/descent/proof.f.ts index d0bd505c01..6948dc47a1 100644 --- a/fjs/bnf/descent/proof.f.ts +++ b/fjs/bnf/descent/proof.f.ts @@ -3,8 +3,9 @@ import { stringToCodePointList } from '../../text/utf16/module.f.mjs' import { map, toArray } from '../../types/list/module.f.mjs' import { commaJoin0Plus, option, range, repeat0Plus, set } from '../module.f.mjs' import { deterministic } from '../testlib.f.ts' -import { emptyTagMap, toData } from '../data/module.f.ts' -import { descentParser, type DescentMatch, type CodePointMeta, type DescentMatchResult } from './module.f.ts' +import { emptyTagMap, toData } from '../data/module.f.mjs' +import { descentParser } from './module.f.mjs' +import type { DescentMatch, CodePointMeta, DescentMatchResult } from './types.ts' import { assertEq, assertNotNullish } from '../../asserts/module.f.mjs' const mapCodePoint = (cp: CodePoint): CodePointMeta => [cp, undefined] diff --git a/fjs/bnf/descent/types.ts b/fjs/bnf/descent/types.ts new file mode 100644 index 0000000000..c409f153cf --- /dev/null +++ b/fjs/bnf/descent/types.ts @@ -0,0 +1,80 @@ +/** + * Types for the recursive descent matcher backend. + * + * @module + */ + +import type { CodePoint } from '../../text/utf16/types.ts' +import type { TerminalRange } from '../types.ts' + +export type AstTag = string|true|undefined + +/** + * Recursive descent matcher for a single named rule. + */ +export type DescentMatchRule = (name: string, tag: AstTag, s: readonly CodePointMeta[], idx: number) => DescentMatchResult + +/** + * Where a match ran out of road, for diagnostics. + * + * `idx` is the furthest position any terminal was tried at and rejected, and + * `expected` holds the terminals that would have allowed progress there, in the + * order the grammar tried them and without repeats. + * + * Unlike a failed result's own index, this never rewinds: a failing sequence + * item rewinds the result to the sequence's start, while the furthest failure is + * a high-water mark over the whole match — including branches the grammar + * backtracked out of. That is what makes "expected X or Y at N" possible. + * + * `idx` is `0` with an empty `expected` when the match failed without ever + * rejecting a terminal, as an empty variant does. + */ +export type DescentFailure = { + readonly idx: number + readonly expected: readonly TerminalRange[] +} + +/** + * Result of a descent match operation. + * + * `failure` is present exactly when `success` is `false`: a successful match has + * nothing to diagnose, and its `idx` already says where matching stopped. Note + * the consequence for a match that succeeds *without consuming all input* — + * `idx` still locates the position it stopped at, but the terminals that would + * have let it continue are not reported. + * + * On failure `idx` has rewound to the start of the enclosing sequence and + * locates nothing; read `failure.idx` instead. + * + * The same type describes a match in progress, where `failure` is likewise + * absent until the match ends. + */ +export type DescentMatchResult = { + readonly ast: AstRuleMeta + readonly success: boolean + readonly idx: number + readonly failure?: DescentFailure +} + +/** + * Entry-point recursive descent matcher. + */ +export type DescentMatch = (name: string, s: readonly CodePointMeta[]) => DescentMatchResult + +/** + * Code point value paired with metadata. + */ +export type CodePointMeta = readonly[CodePoint, T] + +/** + * AST sequence for the metadata-aware parser. + */ +export type AstSequenceMeta = readonly(AstRuleMeta|CodePointMeta)[] + +/** + * Metadata-aware AST node. + */ +export type AstRuleMeta = { + readonly tag: AstTag, + readonly sequence: AstSequenceMeta +} diff --git a/fjs/bnf/ll1/module.f.ts b/fjs/bnf/ll1/module.f.mjs similarity index 56% rename from fjs/bnf/ll1/module.f.ts rename to fjs/bnf/ll1/module.f.mjs index b690984e91..8767a75805 100644 --- a/fjs/bnf/ll1/module.f.ts +++ b/fjs/bnf/ll1/module.f.mjs @@ -8,77 +8,25 @@ * grammar is not LL(1) — a first/first conflict. Nullability is looked up from * {@link emptyTagMap} in `fjs/bnf/data` rather than re-derived here. * + * See `./types.ts` for the type-level API. + * * @module */ -import type { CodePoint } from '../../text/utf16/types.ts' import { strictEqual } from '../../types/function/operator/module.f.mjs' import { toArray } from '../../types/list/module.f.mjs' -import type { RangeMapArray } from '../../types/range_map/types.ts' import { rangeMap } from '../../types/range_map/module.f.mjs' +/** @import { Properties } from '../../types/range_map/types.ts' */ import { contains, set } from '../../types/string_set/module.f.mjs' -import type { StringSet } from '../../types/string_set/types.ts' +/** @import { StringSet } from '../../types/string_set/types.ts' */ import { rangeDecode } from '../module.f.mjs' import { definedEntries } from '../../types/object/module.f.mjs' -import type { StringMap } from '../../types/object/types.ts' -import { emptyTagMap, type EmptyTag, type RuleSet, toData } from '../data/module.f.ts' -import type { Rule as FRule } from '../types.ts' - -type DispatchRule = { - readonly emptyTag: EmptyTag, - readonly rangeMap: Dispatch -} - -type Dispatch = RangeMapArray - -type DispatchResult = DispatchRuleCollection | null - -type DispatchRuleOrName = DispatchRule | string - -type DispatchRuleCollection = { - readonly tag: string | undefined, - readonly rules: DispatchRuleOrName[] -} - -type DispatchMap = StringMap +import { emptyTagMap, toData } from '../data/module.f.mjs' +/** @import { EmptyTag, RuleSet } from '../data/types.ts' */ +/** @import { Rule as FRule } from '../types.ts' */ +/** @import { AstSequence, AstTag, Match, MatchResult, MatchRule, Remainder, _Dispatch, _DispatchMap, _DispatchResult, _DispatchRule } from './types.ts' */ -/** - * Represents a parsed AST sequence. - */ -export type AstSequence = readonly(AstRule|CodePoint)[] - -export type AstTag = string|true|undefined - -/** - * Represents a parsed AST rule, consisting of a rule name and its parsed sequence. - */ -type AstRule = { - readonly tag: AstTag, - readonly sequence: AstSequence -} - -/** - * Represents the remaining input after a match attempt, or `null` if no match is possible. - */ -export type Remainder = readonly CodePoint[] | null - -/** - * Parsing result of {@link parser} and {@link parserRuleSet}. - * - * Represents the result of a match operation, including the parsed AST rule and the remainder of the input. - */ -export type MatchResult = readonly[AstRule, boolean, Remainder] - -/** - * LL(1) parser function for matching by rule name. - */ -export type Match = (name: string, s: readonly CodePoint[]) => MatchResult - -/** - * Internal match function signature used by compiled dispatch rules. - */ -export type MatchRule = (dr: DispatchRule, s: readonly CodePoint[]) => MatchResult - -const dispatchOp = rangeMap({ +/** @type {Properties<_DispatchResult>} */ +const dispatchProps = { union: a => b => { if (a === null) { return b @@ -90,47 +38,57 @@ const dispatchOp = rangeMap({ }, equal: strictEqual, def: null, -}) +} + +const dispatchOp = rangeMap(dispatchProps) /** * Builds a dispatch map for a {@link RuleSet} to enable predictive parsing. + * + * @type {(ruleSet: RuleSet) => _DispatchMap} */ -export const dispatchMap = (ruleSet: RuleSet): DispatchMap => { +export const dispatchMap = ruleSet => { const nullMap = emptyTagMap(ruleSet) - const addRuleToDispatch = (dr: DispatchResult, name: string): DispatchResult => { + /** @type {(dr: _DispatchResult, name: string) => _DispatchResult} */ + const addRuleToDispatch = (dr, name) => { if (dr === null) return null return { tag: dr.tag, rules: [...dr.rules, name]} } - const addTagToDispatch = (dr: DispatchResult, tag: string): DispatchResult => { + /** @type {(dr: _DispatchResult, tag: string) => _DispatchResult} */ + const addTagToDispatch = (dr, tag) => { if (dr === null) return null return { tag, rules: dr.rules} } - const dispatchRule = (dm: DispatchMap, name: string, current: StringSet): DispatchMap => { + /** @type {(dm: _DispatchMap, name: string, current: StringSet) => _DispatchMap} */ + const dispatchRule = (dm, name, current) => { if (name in dm) { return dm } const newCurrent = set(name)(current) const rule = ruleSet[name] if (typeof rule === 'number') { const range = rangeDecode(rule) const dispatch = dispatchOp.fromRange({tag: undefined, rules: []})(range) - const dr: DispatchRule = {emptyTag: undefined, rangeMap: dispatch} + /** @type {_DispatchRule} */ + const dr = {emptyTag: undefined, rangeMap: dispatch} return { ...dm, [name]: dr } } else if (rule instanceof Array) { - let emptyTag: EmptyTag = true - let result: Dispatch = [] + /** @type {EmptyTag} */ + let emptyTag = true + /** @type {_Dispatch} */ + let result = [] for (const item of rule) { if (contains(item)(newCurrent)) { result = result.map(x => [addRuleToDispatch(x[0], item), x[1]]) } else { dm = dispatchRule(dm, item, newCurrent) - const dr = dm[item]! + const dr = /** @type {_DispatchRule} */ (dm[item]) if (emptyTag === true) { result = result.map(x => [addRuleToDispatch(x[0], item), x[1]]) result = toArray(dispatchOp.merge(result)(dr.rangeMap)) @@ -140,28 +98,34 @@ export const dispatchMap = (ruleSet: RuleSet): DispatchMap => { } } } - const dr: DispatchRule = {emptyTag, rangeMap: result} + /** @type {_DispatchRule} */ + const dr = {emptyTag, rangeMap: result} return { ...dm, [name]: dr} } else { const entries = definedEntries(rule) - let result: Dispatch = [] - let emptyTag: EmptyTag = undefined + /** @type {_Dispatch} */ + let result = [] + /** @type {EmptyTag} */ + let emptyTag = undefined for (const [tag, item] of entries) { dm = dispatchRule(dm, item, newCurrent) - const dr = dm[item]! + const dr = /** @type {_DispatchRule} */ (dm[item]) if (nullMap[item] !== undefined) { emptyTag = tag } else { - const d: Dispatch = dr.rangeMap.map(x => [addTagToDispatch(x[0], tag), x[1]]) + /** @type {_Dispatch} */ + const d = dr.rangeMap.map(x => [addTagToDispatch(x[0], tag), x[1]]) result = toArray(dispatchOp.merge(result)(d)) } } - const dr: DispatchRule = {emptyTag, rangeMap: result} + /** @type {_DispatchRule} */ + const dr = {emptyTag, rangeMap: result} return { ...dm, [name]: dr} } } - let result: DispatchMap = {} + /** @type {_DispatchMap} */ + let result = {} for (const k in ruleSet) { result = dispatchRule(result, k, null) } @@ -171,25 +135,32 @@ export const dispatchMap = (ruleSet: RuleSet): DispatchMap => { /** * Creates an LL(1) parser from a functional grammar rule. + * + * @type {(fr: FRule) => Match} */ -export const parser = (fr: FRule): Match => { +export const parser = fr => { const data = toData(fr) return parserRuleSet(data[0]) } -const mrSuccess = (tag: AstTag, sequence: AstSequence, r: Remainder): MatchResult => +/** @type {(tag: AstTag, sequence: AstSequence, r: Remainder) => MatchResult} */ +const mrSuccess = (tag, sequence, r) => [{tag, sequence}, true, r] -const mrFail = (tag: AstTag, sequence: AstSequence, r: Remainder): MatchResult => +/** @type {(tag: AstTag, sequence: AstSequence, r: Remainder) => MatchResult} */ +const mrFail = (tag, sequence, r) => [{tag, sequence}, false, r] /** * Creates an LL(1) parser from an already materialized {@link RuleSet}. + * + * @type {(ruleSet: RuleSet) => Match} */ -export const parserRuleSet = (ruleSet: RuleSet): Match => { +export const parserRuleSet = ruleSet => { const map = dispatchMap(ruleSet) - const f: MatchRule = ({emptyTag, rangeMap}, cp): MatchResult => { + /** @type {MatchRule} */ + const f = ({emptyTag, rangeMap}, cp) => { if (cp.length === 0) { return mrSuccess(emptyTag, [], emptyTag === undefined ? null : cp) } @@ -200,12 +171,14 @@ export const parserRuleSet = (ruleSet: RuleSet): Match => { ? mrFail(emptyTag, [], cp) : mrSuccess(emptyTag, [], cp) } - let seq: AstSequence = [cp0] + /** @type {AstSequence} */ + let seq = [cp0] const [, ...restCp] = cp - let r: readonly number[] = restCp + /** @type {readonly number[]} */ + let r = restCp const {tag, rules} = dr for (const i of rules) { - const rule = typeof i === 'string' ? map[i]! : i + const rule = typeof i === 'string' ? /** @type {_DispatchRule} */ (map[i]) : i const res = f(rule, r) const [astRule, success, newR] = res if (success === false) { @@ -220,5 +193,5 @@ export const parserRuleSet = (ruleSet: RuleSet): Match => { return mrSuccess(tag, seq, r) } - return (name, cp): MatchResult => f(map[name]!, cp) + return (name, cp) => f(/** @type {_DispatchRule} */ (map[name]), cp) } diff --git a/fjs/bnf/ll1/proof.f.ts b/fjs/bnf/ll1/proof.f.ts index c0eddc90bf..d483aec937 100644 --- a/fjs/bnf/ll1/proof.f.ts +++ b/fjs/bnf/ll1/proof.f.ts @@ -2,8 +2,10 @@ import { stringToCodePointList } from '../../text/utf16/module.f.mjs' import { toArray } from '../../types/list/module.f.mjs' import { commaJoin0Plus, option, range, repeat0Plus, set } from '../module.f.mjs' import { deterministic } from '../testlib.f.ts' -import { type RuleSet, toData } from '../data/module.f.ts' -import { dispatchMap, type MatchResult, parser, parserRuleSet } from './module.f.ts' +import { toData } from '../data/module.f.mjs' +import type { RuleSet } from '../data/types.ts' +import { dispatchMap, parser, parserRuleSet } from './module.f.mjs' +import type { MatchResult } from './types.ts' import { assertEq } from '../../asserts/module.f.mjs' export const proof = { diff --git a/fjs/bnf/ll1/todo/stack-recursive-matching.md b/fjs/bnf/ll1/todo/stack-recursive-matching.md index 328c1ad74a..8c38019435 100644 --- a/fjs/bnf/ll1/todo/stack-recursive-matching.md +++ b/fjs/bnf/ll1/todo/stack-recursive-matching.md @@ -5,7 +5,7 @@ ### Problem -`fjs/bnf/ll1/module.f.ts`'s matcher `f` (inside `parserRuleSet`) recurses +`fjs/bnf/ll1/module.f.mjs`'s matcher `f` (inside `parserRuleSet`) recurses natively: after consuming a code point it walks the dispatched `rules` chain with one nested `f` call per rule. For a right-recursive rule — which is how `repeat0Plus` encodes repetition — every additional repetition adds another @@ -21,7 +21,7 @@ identical). This is the same bug that was fixed in the sibling descent backend in PR [#1303](https://github.com/functionalscript/functionalscript/pull/1303) (see -its CHANGELOG entry for the history): `fjs/bnf/descent/module.f.ts`'s matcher +its CHANGELOG entry for the history): `fjs/bnf/descent/module.f.mjs`'s matcher now runs as an explicit-stack machine and handles 100 KB+ inputs; the LL(1) matcher was not touched. Today nothing outside `fjs/bnf/ll1`'s own proofs consumes this parser (hence P3, not P1), but any future consumer with realistic input @@ -30,7 +30,7 @@ sizes will hit it. ### Proposal Port the descent backend's fix: rewrite `f` as an explicit-stack machine. -`fjs/bnf/descent/module.f.ts` is the template — its matcher keeps two +`fjs/bnf/descent/module.f.mjs` is the template — its matcher keeps two suspended-frame kinds on an immutable cons-cell stack and loops, either starting the current rule invocation or feeding the pending result into the innermost frame. The LL(1) version is simpler: `f` has only one recursion @@ -61,7 +61,7 @@ grammar size, not input size — it does not need to change. ### Related -- `fjs/bnf/descent/module.f.ts` — the ported fix to mirror (explicit frame +- `fjs/bnf/descent/module.f.mjs` — the ported fix to mirror (explicit frame stack; see the `longInput` proof group in its `proof.f.ts`), landed in PR [#1303](https://github.com/functionalscript/functionalscript/pull/1303), whose CHANGELOG entry records the history of the same bug in the descent diff --git a/fjs/bnf/ll1/types.ts b/fjs/bnf/ll1/types.ts new file mode 100644 index 0000000000..58300fae12 --- /dev/null +++ b/fjs/bnf/ll1/types.ts @@ -0,0 +1,73 @@ +/** + * Types for the LL(1) dispatch/matcher backend. + * + * @module + */ + +import type { CodePoint } from '../../text/utf16/types.ts' +import type { RangeMapArray } from '../../types/range_map/types.ts' +import type { StringMap } from '../../types/object/types.ts' +import type { EmptyTag } from '../data/types.ts' + +/** @internal */ +export type _DispatchRule = { + readonly emptyTag: EmptyTag, + readonly rangeMap: _Dispatch +} + +/** @internal */ +export type _Dispatch = RangeMapArray<_DispatchResult> + +/** @internal */ +export type _DispatchResult = _DispatchRuleCollection | null + +/** @internal */ +export type _DispatchRuleOrName = _DispatchRule | string + +/** @internal */ +export type _DispatchRuleCollection = { + readonly tag: string | undefined, + readonly rules: _DispatchRuleOrName[] +} + +/** @internal */ +export type _DispatchMap = StringMap<_DispatchRule> + +/** + * Represents a parsed AST rule, consisting of a rule name and its parsed sequence. + * + * @internal + */ +export type _AstRule = { + readonly tag: AstTag, + readonly sequence: AstSequence +} + +/** + * Represents a parsed AST sequence. + */ +export type AstSequence = readonly(_AstRule|CodePoint)[] + +export type AstTag = string|true|undefined + +/** + * Represents the remaining input after a match attempt, or `null` if no match is possible. + */ +export type Remainder = readonly CodePoint[] | null + +/** + * Parsing result of `parser` and `parserRuleSet`. + * + * Represents the result of a match operation, including the parsed AST rule and the remainder of the input. + */ +export type MatchResult = readonly[_AstRule, boolean, Remainder] + +/** + * LL(1) parser function for matching by rule name. + */ +export type Match = (name: string, s: readonly CodePoint[]) => MatchResult + +/** + * Internal match function signature used by compiled dispatch rules. + */ +export type MatchRule = (dr: _DispatchRule, s: readonly CodePoint[]) => MatchResult diff --git a/fjs/bnf/todo/207.md b/fjs/bnf/todo/207.md index b09181286e..d167267a52 100644 --- a/fjs/bnf/todo/207.md +++ b/fjs/bnf/todo/207.md @@ -59,7 +59,7 @@ The combinators (`option`, `repeat0Plus`, `repeat1Plus`, `join0Plus`, …) are just helpers that build `Variant`/`Sequence` trees, e.g. `option(x) = { some: x, none: [] }` and `repeat0Plus(x) = () => option([x, self])`. -`parser`/`descentParser` (`fjs/bnf/data/module.f.ts`) produce a generic AST: +`parser`/`descentParser` (`fjs/bnf/data/module.f.mjs`) produce a generic AST: ```ts type AstRule = { readonly tag: AstTag, readonly sequence: AstSequence } diff --git a/fjs/bnf/todo/46.md b/fjs/bnf/todo/46.md index b335275896..25c40da71a 100644 --- a/fjs/bnf/todo/46.md +++ b/fjs/bnf/todo/46.md @@ -7,7 +7,7 @@ Implement an LR(1) parser because LL(1) can't handle break lines in comments. The parser targets the current BNF: functional form in [../module.f.mjs](../module.f.mjs), serializable form in -[../data/module.f.ts](../data/module.f.ts). It needs an AST structure derived +[../data/module.f.mjs](../data/module.f.mjs). It needs an AST structure derived from that BNF definition — see [parser-structure](./parser-structure.md) for the AST shape. diff --git a/fjs/bnf/todo/665-bnf-data-fold-children.md b/fjs/bnf/todo/665-bnf-data-fold-children.md index 05d8a03114..d41a4f5111 100644 --- a/fjs/bnf/todo/665-bnf-data-fold-children.md +++ b/fjs/bnf/todo/665-bnf-data-fold-children.md @@ -5,14 +5,14 @@ ### Problem -`fjs/bnf/data/module.f.ts` defines two `NewRule` builders, `sequence` and +`fjs/bnf/data/module.f.mjs` defines two `NewRule` builders, `sequence` and `variant`, that walk a rule's children, register each child via `toDataAdd`, and thread the resulting `FRuleMap`/`RuleSet` while building a result. They share the same accumulation skeleton and differ only in (a) what they iterate and (b) how the result is shaped: ```ts -// fjs/bnf/data/module.f.ts:172 +// fjs/bnf/data/module.f.mjs:114 const sequence = (list: FSequence): NewRule => map => { let result: Sequence = [] let set = {} @@ -25,7 +25,7 @@ const sequence = (list: FSequence): NewRule => map => { return [map, set, result] } -// fjs/bnf/data/module.f.ts:184 +// fjs/bnf/data/module.f.mjs:129 const variant = (fr: FRule): NewRule => map => { let set: RuleSet = {} let rule: Variant = {} @@ -101,7 +101,7 @@ removing the four mutated `let`s. ### Tasks -- [ ] Add `foldChildren` (private) to `fjs/bnf/data/module.f.ts`. +- [ ] Add `foldChildren` (private) to `fjs/bnf/data/module.f.mjs`. - [ ] Rewrite `sequence` and `variant` as instantiations of it. - [ ] Confirm `fjs/bnf/data/proof.f.ts` coverage still exercises both result shapes (array and keyed) and the multi-child map-threading path. diff --git a/fjs/bnf/todo/667-bnf-repeat-flatten.md b/fjs/bnf/todo/667-bnf-repeat-flatten.md index 5b1b8dd1b9..d33209e7ea 100644 --- a/fjs/bnf/todo/667-bnf-repeat-flatten.md +++ b/fjs/bnf/todo/667-bnf-repeat-flatten.md @@ -106,5 +106,5 @@ matched items rather than the nested right-recursive cons structure. - [256-bit bigint BNF symbols](./bigint-symbols.md) — changes terminal/range representation assumptions used by the old dispatch example. - [BNF semantic actions](./207.md) — origin of the flat-list/action motivation. -- `fjs/bnf/data/module.f.ts` — owns the data `Rule` representation and dispatch. +- `fjs/bnf/data/module.f.mjs` — owns the data `Rule` representation and dispatch. - `fjs/bnf/module.f.mjs` — owns BNF combinators such as `repeat0Plus`. diff --git a/fjs/bnf/todo/669-bnf-data-shared-helpers.md b/fjs/bnf/todo/669-bnf-data-shared-helpers.md index cd24884ceb..15318dac17 100644 --- a/fjs/bnf/todo/669-bnf-data-shared-helpers.md +++ b/fjs/bnf/todo/669-bnf-data-shared-helpers.md @@ -5,7 +5,7 @@ ### Problem -`fjs/bnf/data/module.f.ts` carries a DRY / hoisting smell in its parser +`fjs/bnf/data/module.f.mjs` carries a DRY / hoisting smell in its parser machinery, not covered by the existing fold-children work in [i665-bnf-data-fold-children](todo.md) (that issue is about the `sequence` / `variant` AST-fold helpers, a different pair of @@ -13,7 +13,7 @@ functions). (A second smell used to be listed here — duplicated `emptyTagMapAdd` branches in `fjs/bnf/descent` — but that function was deleted and replaced by a single -shared `emptyTagMap` fixpoint in `fjs/bnf/data/module.f.ts` while fixing +shared `emptyTagMap` fixpoint in `fjs/bnf/data/module.f.mjs` while fixing nullable-analysis-shared, so it no longer applies.) #### 1. `mrSuccess` / `mrFail` match-result constructors diff --git a/fjs/bnf/todo/data-tosequence-reuse.md b/fjs/bnf/todo/data-tosequence-reuse.md index 0f04823d51..0398f5b60d 100644 --- a/fjs/bnf/todo/data-tosequence-reuse.md +++ b/fjs/bnf/todo/data-tosequence-reuse.md @@ -12,7 +12,7 @@ This TODO proposed preserving `bnf/data`'s generic `string` rule case and reusin The alphabet-specific BNF split intentionally removes that architecture instead: - `string` is removed from the generic `DataRule` / `Rule` representation; -- `fjs/bnf/data/module.f.ts` no longer interprets strings as Unicode code points; +- `fjs/bnf/data/module.f.mjs` no longer interprets strings as Unicode code points; - `toSequence` moves to `fjs/bnf/unicode/module.f.ts` as an alphabet-specific construction helper; - Unicode helpers lower strings to ordinary generic rules before they reach @@ -24,7 +24,7 @@ immediately removes, while implementing it afterward would no longer make sense. ### Historical proposal -The original proposal was to import `toSequence` in `fjs/bnf/data/module.f.ts`, +The original proposal was to import `toSequence` in `fjs/bnf/data/module.f.mjs`, replace the `'string'` case body with `sequence(toSequence(dr))`, and delete the local duplicate Unicode-conversion helpers/imports. diff --git a/fjs/bnf/todo/proof-recognizer-and-fixtures.md b/fjs/bnf/todo/proof-recognizer-and-fixtures.md index d9a7508b4e..d0052a4017 100644 --- a/fjs/bnf/todo/proof-recognizer-and-fixtures.md +++ b/fjs/bnf/todo/proof-recognizer-and-fixtures.md @@ -45,7 +45,7 @@ const expect = (s: string, success: boolean) => { The two backends read their results differently because their result *types* differ: descent returns the record `DescentMatchResult` -(`fjs/bnf/descent/module.f.ts:65-70` — `{ ast, success, idx, failure? }`), +(`fjs/bnf/descent/types.ts:52-57` — `{ ast, success, idx, failure? }`), while ll1's `MatchResult` is still a tuple. Any adapter has to speak both. `fjs/djs/tokenizer/proof.f.ts:27` is an eighth site in the descent shape, with @@ -181,5 +181,5 @@ explicit named override list for the rows where token-stream acceptance differs. long-input regression corpus. - [new-parser](./new-parser.md) — token-symbol alphabet needs its own recognizer adapter, but can share `Case` / `assertRecognizes`. -- `fjs/bnf/descent/module.f.ts` `DescentFailure` — failure diagnostics compose +- `fjs/bnf/descent/types.ts` `DescentFailure` — failure diagnostics compose through `Recognition.diagnostic`; do not collapse them to boolean. diff --git a/fjs/bnf/todo/rule-visitor.md b/fjs/bnf/todo/rule-visitor.md index 9ff9195f54..c6d503cd0a 100644 --- a/fjs/bnf/todo/rule-visitor.md +++ b/fjs/bnf/todo/rule-visitor.md @@ -30,7 +30,7 @@ union**, not against today's `number | string` implementation details. ### Proposal After the alphabet and terminal-representation migrations settle the generic -`Rule` union, add a visitor in `fjs/bnf/data/module.f.ts` (the module that owns the +`Rule` union, add a visitor in `fjs/bnf/data/module.f.mjs` (the module that owns the type), mirroring the proven `visit` pattern in `fjs/types/rtti/common`. Conceptually the visitor exposes the semantic rule cases: @@ -63,7 +63,7 @@ scheme. Each call site keeps its own recursion/accumulator structure. - [ ] Wait for the alphabet split and bigint terminal/range migration to settle the final generic `Rule` union and terminal representation. - [ ] Define `RuleVisitor` / `matchRule` against those final discriminants in - `fjs/bnf/data/module.f.ts`; do not depend on the obsolete raw-string rule or + `fjs/bnf/data/module.f.mjs`; do not depend on the obsolete raw-string rule or `typeof rule === 'number'` terminal test. - [ ] Rewrite the backend dispatch sites to use the shared visitor. - [ ] Keep any alphabet-specific lowering outside this generic visitor. diff --git a/fjs/bnf/todo/terminal-range-shared-type.md b/fjs/bnf/todo/terminal-range-shared-type.md index f11952732d..13275a351b 100644 --- a/fjs/bnf/todo/terminal-range-shared-type.md +++ b/fjs/bnf/todo/terminal-range-shared-type.md @@ -5,13 +5,13 @@ ### Problem -The same public type is declared in two modules. `fjs/bnf/module.f.mjs:23`: +The same public type is declared in two modules. `fjs/bnf/types.ts:30`: ```ts export type TerminalRange = number ``` -and `fjs/bnf/data/module.f.ts:25-27`: +and `fjs/bnf/data/types.ts:11-14`: ```ts /** @@ -29,10 +29,10 @@ changes from a plain `number`). ### Proposal -Define `TerminalRange` once in `fjs/bnf/module.f.mjs` (the module that owns the -range encode/decode primitives) and have `fjs/bnf/data/module.f.ts` import and -re-export it rather than redeclaring. Per `AGENTS.md`: "When a sibling module -already has the type you need, import it" instead of duplicating. +Define `TerminalRange` once in `fjs/bnf/types.ts` (the module that owns the +range encode/decode primitives' types) and have `fjs/bnf/data/types.ts` import +and re-export it rather than redeclaring. Per `AGENTS.md`: "When a sibling +module already has the type you need, import it" instead of duplicating. ### Tasks diff --git a/fjs/bnf/todo/unicode-rules.md b/fjs/bnf/todo/unicode-rules.md index aa7c1c250a..2eef1d2b0a 100644 --- a/fjs/bnf/todo/unicode-rules.md +++ b/fjs/bnf/todo/unicode-rules.md @@ -17,7 +17,7 @@ BNF machinery for different alphabets: bytes, Unicode code points, tokenizer output symbols, and potentially other intermediate symbol streams. The coupling is not limited to helper functions in `fjs/bnf/module.f.mjs`. -`DataRule` currently includes `string`, and `fjs/bnf/data/module.f.ts` recognizes +`DataRule` currently includes `string`, and `fjs/bnf/data/module.f.mjs` recognizes that case and converts the string with `stringToCodePointList`. Therefore the serializable/core BNF conversion path itself currently has Unicode semantics. @@ -51,7 +51,7 @@ core BNF. Remove `string` from the generic `DataRule` / `Rule` representation. Unicode helpers should translate strings into ordinary generic rules before the grammar -reaches `fjs/bnf/data`, so `fjs/bnf/data/module.f.ts` no longer imports +reaches `fjs/bnf/data`, so `fjs/bnf/data/module.f.mjs` no longer imports `stringToCodePointList` or performs a string-specific conversion. Keep generic combinators generic. If an existing combinator currently embeds @@ -117,9 +117,9 @@ new module boundary and final rule discriminants before implementation starts. `fjs/bnf/module.f.mjs`. - [ ] Remove Unicode/text imports from `fjs/bnf/module.f.mjs`. - [ ] Keep byte-container interpretation out of `fjs/bnf/module.f.mjs` and - `fjs/bnf/data/module.f.ts`. + `fjs/bnf/data/module.f.mjs`. - [ ] Remove `string` as a generic BNF `DataRule` / `Rule` case. -- [ ] Remove Unicode string expansion from `fjs/bnf/data/module.f.ts`. +- [ ] Remove Unicode string expansion from `fjs/bnf/data/module.f.mjs`. - [ ] Make any core combinators that currently embed string/Unicode syntax alphabet-agnostic; keep optional Unicode conveniences in `fjs/bnf/unicode/module.f.ts`. @@ -182,5 +182,5 @@ new module boundary and final rule discriminants before implementation starts. string expansion. - [`fjs/bnf/module.f.mjs`](../module.f.mjs) — currently mixes generic and Unicode rule construction. -- [`fjs/bnf/data/module.f.ts`](../data/module.f.ts) — currently expands string +- [`fjs/bnf/data/module.f.mjs`](../data/module.f.mjs) — currently expands string rules into Unicode code-point terminals. diff --git a/fjs/cas/cli/module.f.ts b/fjs/cas/cli/module.f.ts index fde829ab7d..3d3501340e 100644 --- a/fjs/cas/cli/module.f.ts +++ b/fjs/cas/cli/module.f.ts @@ -5,7 +5,7 @@ */ import { sha256 } from '../../crypto/sha2/module.f.mjs' import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.mjs' -import { forEachStep, pure, step } from '../../effects/module.f.ts' +import { forEachStep, pure, step } from '../../effects/module.f.mjs' import { errorExit, log, diff --git a/fjs/cas/evo/module.f.ts b/fjs/cas/evo/module.f.ts index 3b39fe796e..7a2f2a8ab3 100644 --- a/fjs/cas/evo/module.f.ts +++ b/fjs/cas/evo/module.f.ts @@ -42,7 +42,8 @@ * * @module */ -import { pure, foldStep, type Effect, type Operation } from '../../effects/module.f.ts' +import { pure, foldStep } from '../../effects/module.f.mjs' +import type { Effect, Operation } from '../../effects/types.ts' import { eff } from '../../effects/eff/module.f.ts' import { create, read, write, type Key, type MemOp } from '../../effects/memory/module.f.ts' import { collectRead, type Cas } from '../module.f.ts' @@ -52,7 +53,7 @@ import { tryUtf8 } from '../../text/module.f.mjs' import { decodeText, encodeText, dialect, checkReferences, isHash, type LockMap, type Revision } from '../../media/revision/module.f.ts' import type { Ok, Result } from '../../types/result/types.ts' import { ok, error } from '../../types/result/module.f.mjs' -import { nonEmpty, empty as elEmpty } from '../../effects/list/module.f.ts' +import { nonEmpty, empty as elEmpty } from '../../effects/list/module.f.mjs' import { at, definedEntries } from '../../types/object/module.f.mjs' import type { StringMap } from '../../types/object/types.ts' import { unwrap } from '../../types/nullable/module.f.mjs' diff --git a/fjs/cas/evo/proof.f.ts b/fjs/cas/evo/proof.f.ts index d06810337b..d815e1d94a 100644 --- a/fjs/cas/evo/proof.f.ts +++ b/fjs/cas/evo/proof.f.ts @@ -1,5 +1,5 @@ import { assert, assertEq } from '../../asserts/module.f.mjs' -import { pure } from '../../effects/module.f.ts' +import { pure } from '../../effects/module.f.mjs' import { fileCas, type Cas } from '../module.f.ts' import { sha256 } from '../../crypto/sha2/module.f.mjs' import { emptyState, virtual } from '../../effects/node/virtual/module.f.ts' @@ -9,7 +9,7 @@ import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.mjs' import { unwrap } from '../../types/nullable/module.f.mjs' import type { Ok } from '../../types/result/types.ts' import { ok, error } from '../../types/result/module.f.mjs' -import { nonEmpty, empty as elEmpty } from '../../effects/list/module.f.ts' +import { nonEmpty, empty as elEmpty } from '../../effects/list/module.f.mjs' import type { IoResult } from '../../effects/node/module.f.ts' import { tryUtf8 } from '../../text/module.f.mjs' import { dialect as revisionDialect } from '../../media/revision/module.f.ts' diff --git a/fjs/cas/module.f.ts b/fjs/cas/module.f.ts index 81b7b34176..9c8d58c962 100644 --- a/fjs/cas/module.f.ts +++ b/fjs/cas/module.f.ts @@ -9,7 +9,8 @@ import { join, normalize, parse } from '../path/module.f.mjs' import type { Vec } from '../types/bit_vec/types.ts' import { empty, length, maxLength, maxLengthBytes, msb, vec } from '../types/bit_vec/module.f.mjs' import { cBase32ToVec, vecToCBase32 } from '../basen/cbase32/module.f.mjs' -import { foldStep, forEachStep, history, historyStep, okStep, pure, step, type Effect, type Operation } from '../effects/module.f.ts' +import { foldStep, forEachStep, history, historyStep, okStep, pure, step } from '../effects/module.f.mjs' +import type { Effect, Operation } from '../effects/types.ts' import { eff } from '../effects/eff/module.f.ts' import { access, @@ -40,7 +41,8 @@ import { import { toOption } from '../types/nullable/module.f.mjs' import { error, ok, unwrap } from '../types/result/module.f.mjs' import { splitAt } from '../types/string/module.f.mjs' -import { nonEmpty, empty as elEmpty, type List } from '../effects/list/module.f.ts' +import { nonEmpty, empty as elEmpty } from '../effects/list/module.f.mjs' +import type { List } from '../effects/list/types.ts' const split2 = splitAt(2) diff --git a/fjs/cas/proof.f.ts b/fjs/cas/proof.f.ts index e287c45ff6..d1ccf6ab86 100644 --- a/fjs/cas/proof.f.ts +++ b/fjs/cas/proof.f.ts @@ -3,13 +3,15 @@ import { length, maxLength, msb, vec, vec8 } from '../types/bit_vec/module.f.mjs import { cBase32ToVec, vecToCBase32 } from '../basen/cbase32/module.f.mjs' import { computeSync, sha256 } from '../crypto/sha2/module.f.mjs' import { fileCas, casAddFile, collectRead, type FileCasOperation, casUpload } from './module.f.ts' -import { match, pure, runPure, step, type Effect } from '../effects/module.f.ts' +import { match, pure, runPure, step } from '../effects/module.f.mjs' +import type { Effect } from '../effects/types.ts' import { mkdir, writeFile, rm, readFile, type ReadFile, type WriteFile, type Rm, type Mkdir, type IoResult, access } from '../effects/node/module.f.ts' import type { Ok } from '../types/result/types.ts' import { error, ok } from '../types/result/module.f.mjs' import { emptyState, virtual } from '../effects/node/virtual/module.f.ts' import { join } from '../path/module.f.mjs' -import { nonEmpty, empty, type List } from '../effects/list/module.f.ts' +import { nonEmpty, empty } from '../effects/list/module.f.mjs' +import type { List } from '../effects/list/types.ts' import { assert, assertEq, assertNotNullish } from '../asserts/module.f.mjs' const testDir = './test-cas-cli' diff --git a/fjs/cas/todo/66o-read-streamfile-dedup.md b/fjs/cas/todo/66o-read-streamfile-dedup.md index 5c56f64162..9c06bb33b9 100644 --- a/fjs/cas/todo/66o-read-streamfile-dedup.md +++ b/fjs/cas/todo/66o-read-streamfile-dedup.md @@ -34,7 +34,7 @@ const loop = (offset: number): List> => ``` (`nonEmpty` / `elEmpty` are `nonEmpty` and `empty as elEmpty` from -`fjs/effects/list/module.f.ts`.) The only real difference is the declared effect +`fjs/effects/list/module.f.mjs`.) The only real difference is the declared effect type: `FileCasOperation` in `read` vs. `ReadBytes` in `streamFile`, with `ReadBytes ⊆ FileCasOperation`. So the EOF/error streaming invariant is maintained in two places that must stay in sync. diff --git a/fjs/cas/todo/write-closed-helpers.md b/fjs/cas/todo/write-closed-helpers.md index d2fed0dbd0..c7053120b4 100644 --- a/fjs/cas/todo/write-closed-helpers.md +++ b/fjs/cas/todo/write-closed-helpers.md @@ -63,5 +63,5 @@ by a one-line JSDoc claim, as above. No fragment extraction. - [fold-stream-combinator](../../effects/todo/fold-stream-combinator.md) — covers the chunk loop; this issue covers the remaining nested helpers. -- `okStep` (`fjs/effects/module.f.ts`) already applies to two steps inside +- `okStep` (`fjs/effects/module.f.mjs`) already applies to two steps inside `write` (`createExclusive`, and `casUpload` beside it). diff --git a/fjs/ci/module.f.ts b/fjs/ci/module.f.ts index f5e466f924..200bc94aea 100644 --- a/fjs/ci/module.f.ts +++ b/fjs/ci/module.f.ts @@ -3,7 +3,8 @@ * * @module */ -import { mapStep, step, type Effect } from '../effects/module.f.ts' +import { mapStep, step } from '../effects/module.f.mjs' +import type { Effect } from '../effects/types.ts' import { access, writeUtf8File, type NodeOp } from '../effects/node/module.f.ts' import { functionalscript, images } from './config/module.f.mjs' import { diff --git a/fjs/ci/nix/module.f.ts b/fjs/ci/nix/module.f.ts index c2741e46b0..d552edac6b 100644 --- a/fjs/ci/nix/module.f.ts +++ b/fjs/ci/nix/module.f.ts @@ -9,7 +9,8 @@ * * @module */ -import { forEachStep, mapStep, pure, step, type Effect } from '../../effects/module.f.ts' +import { forEachStep, mapStep, pure, step } from '../../effects/module.f.mjs' +import type { Effect } from '../../effects/types.ts' import { mkdir, writeUtf8File, type Mkdir, type WriteFile } from '../../effects/node/module.f.ts' import { nixToString } from '../../media/nix/module.f.mjs' import type { Expression } from '../../media/nix/types.ts' diff --git a/fjs/ci/nix/proof.f.ts b/fjs/ci/nix/proof.f.ts index ae10db51bc..9d065926aa 100644 --- a/fjs/ci/nix/proof.f.ts +++ b/fjs/ci/nix/proof.f.ts @@ -4,7 +4,7 @@ * @module */ import { assert, assertEq } from '../../asserts/module.f.mjs' -import { step } from '../../effects/module.f.ts' +import { step } from '../../effects/module.f.mjs' import { readUtf8File } from '../../effects/node/module.f.ts' import { emptyState, virtual } from '../../effects/node/virtual/module.f.ts' import { nixpkgs } from '../config/module.f.mjs' diff --git a/fjs/cli/module.f.ts b/fjs/cli/module.f.ts index 89135e3c04..28b5277450 100644 --- a/fjs/cli/module.f.ts +++ b/fjs/cli/module.f.ts @@ -1,5 +1,6 @@ import { errorExit, log, type NodeOp, type NodeProgramOptions, type Write } from '../effects/node/module.f.ts' -import { pure, step, type Effect } from '../effects/module.f.ts' +import { pure, step } from '../effects/module.f.mjs' +import type { Effect } from '../effects/types.ts' import { at, fromEntries } from '../types/object/module.f.mjs' type Handler = (options: NodeProgramOptions) => Effect diff --git a/fjs/cli/proof.f.ts b/fjs/cli/proof.f.ts index ce2c3b8f15..f46177a955 100644 --- a/fjs/cli/proof.f.ts +++ b/fjs/cli/proof.f.ts @@ -1,4 +1,4 @@ -import { pure } from '../effects/module.f.ts' +import { pure } from '../effects/module.f.mjs' import type { NodeOp, NodeProgramOptions } from '../effects/node/module.f.ts' import { defaultNodeProgramOptions, emptyState, virtual } from '../effects/node/virtual/module.f.ts' import { dispatch, type Commands } from './module.f.ts' @@ -52,7 +52,7 @@ export const proof = { const commands: Commands = [{ names: ['grab'], description: 'Capture args', - handler: ({ args }): import('../effects/module.f.ts').Effect => { + handler: ({ args }): import('../effects/types.ts').Effect => { captured.push(...args) return pure(0) }, diff --git a/fjs/dev/module.f.ts b/fjs/dev/module.f.ts index 49dc1f87d6..9bd54a2c40 100644 --- a/fjs/dev/module.f.ts +++ b/fjs/dev/module.f.ts @@ -16,7 +16,8 @@ import { import { cmp as strCmp } from '../types/string/module.f.mjs' import type { StringMap } from '../types/object/types.ts' import { unwrap } from '../types/result/module.f.mjs' -import { pure, step, type Effect } from '../effects/module.f.ts' +import { pure, step } from '../effects/module.f.mjs' +import type { Effect } from '../effects/types.ts' import { join, relativize, toPosix } from '../path/module.f.mjs' import { assert, assertEq } from '../asserts/module.f.mjs' import { emptyState, virtual, type Dir } from '../effects/node/virtual/module.f.ts' diff --git a/fjs/dev/update/module.f.ts b/fjs/dev/update/module.f.ts index e633ce1e4b..36291058e8 100644 --- a/fjs/dev/update/module.f.ts +++ b/fjs/dev/update/module.f.ts @@ -3,7 +3,8 @@ * * @module */ -import { history, historyStep, mapStep, step, type Effect } from '../../effects/module.f.ts' +import { history, historyStep, mapStep, step } from '../../effects/module.f.mjs' +import type { Effect } from '../../effects/types.ts' import { mkdir, type Mkdir, type NodeProgram, readUtf8File, type ReadFile, type WriteFile, writeUtf8File } from '../../effects/node/module.f.ts' import { unwrap } from '../../types/result/module.f.mjs' diff --git a/fjs/dev/update/proof.f.ts b/fjs/dev/update/proof.f.ts index 7c7ed77404..5c581a8d81 100644 --- a/fjs/dev/update/proof.f.ts +++ b/fjs/dev/update/proof.f.ts @@ -8,7 +8,7 @@ import { utf8 } from '../../text/module.f.mjs' import { readUtf8File } from '../../effects/node/module.f.ts' import { defaultNodeProgramOptions, emptyState, virtual } from '../../effects/node/virtual/module.f.ts' import { main, syncMcp } from './module.f.ts' -import { step } from '../../effects/module.f.ts' +import { step } from '../../effects/module.f.mjs' const mcp = '{"servers":{}}' as const const initial = { diff --git a/fjs/djs/module.f.ts b/fjs/djs/module.f.ts index 366d1c9896..04292f1971 100644 --- a/fjs/djs/module.f.ts +++ b/fjs/djs/module.f.ts @@ -7,7 +7,8 @@ import type { Primitive as JsonPrimitive } from '../media/json/module.f.ts' import { transpile } from './transpiler/module.f.ts' import { stringify, stringifyAsTree } from './serializer/module.f.ts' import { sort } from '../types/object/module.f.mjs' -import { type Effect, pure, step } from '../effects/module.f.ts' +import { pure, step } from '../effects/module.f.mjs' +import type { Effect } from '../effects/types.ts' import { writeUtf8File, type WriteFile, type ReadFile, diff --git a/fjs/djs/tokenizer/module.f.ts b/fjs/djs/tokenizer/module.f.ts index c4cacdbaea..de5b68b315 100644 --- a/fjs/djs/tokenizer/module.f.ts +++ b/fjs/djs/tokenizer/module.f.ts @@ -3,15 +3,15 @@ * * @module */ -import { - descentParser, - type AstRuleMeta, - type AstSequenceMeta, - type AstTag, - type CodePointMeta, - type DescentMatch, - type DescentMatchResult -} from "../../bnf/descent/module.f.ts" +import { descentParser } from "../../bnf/descent/module.f.mjs" +import type { + AstRuleMeta, + AstSequenceMeta, + AstTag, + CodePointMeta, + DescentMatch, + DescentMatchResult +} from "../../bnf/descent/types.ts" import { eof, none, @@ -234,7 +234,7 @@ const buildToken = (): Rule => { // The whole file's token stream as one right-recursive grammar rule. Safe at any input // length: descentParser matches on an explicit frame stack, not the JS call stack -// (see fjs/bnf/descent/module.f.ts). +// (see fjs/bnf/descent/module.f.mjs). export const jsGrammar = (): Rule => repeat0Plus(buildToken()) const stringify = stringifyAsTree(sort) diff --git a/fjs/djs/tokenizer/proof.f.ts b/fjs/djs/tokenizer/proof.f.ts index 73d938f2ad..6cf05fe2bc 100644 --- a/fjs/djs/tokenizer/proof.f.ts +++ b/fjs/djs/tokenizer/proof.f.ts @@ -1,4 +1,4 @@ -import { descentParser } from '../../bnf/descent/module.f.ts' +import { descentParser } from '../../bnf/descent/module.f.mjs' import { stringToCodePointList, stringToList } from '../../text/utf16/module.f.mjs' import { toArray } from '../../types/list/module.f.mjs' import { jsGrammar, tokenizeString, descentParserCpOnly, tokenizeJs, tokenize } from './module.f.ts' diff --git a/fjs/djs/transpiler/module.f.ts b/fjs/djs/transpiler/module.f.ts index 43572e2bf0..f6c9cbfd05 100644 --- a/fjs/djs/transpiler/module.f.ts +++ b/fjs/djs/transpiler/module.f.ts @@ -15,7 +15,8 @@ import { stringToList } from '../../text/utf16/module.f.mjs' import { concat as pathConcat } from '../../path/module.f.mjs' import { type ParseError, parseFromTokens } from '../parser/module.f.ts' import { run, type AstModule } from '../ast/module.f.ts' -import { type Effect, foldStep, pure, step } from '../../effects/module.f.ts' +import { foldStep, pure, step } from '../../effects/module.f.mjs' +import type { Effect } from '../../effects/types.ts' import { readUtf8File, type ReadFile } from '../../effects/node/module.f.ts' /** diff --git a/fjs/effects/eff/README.md b/fjs/effects/eff/README.md index 37a3c7f49d..77b0735368 100644 --- a/fjs/effects/eff/README.md +++ b/fjs/effects/eff/README.md @@ -1,7 +1,7 @@ # Eff — an experiment, not a settled design `Eff` is a fluent, method-chaining wrapper over the raw `Effect` from -[`../module.f.ts`](../module.f.ts). **It is under active investigation and its +[`../module.f.mjs`](../module.f.mjs). **It is under active investigation and its design is unstable.** Expect the shape to change between releases, and expect it to be a real possibility that it is removed entirely. diff --git a/fjs/effects/eff/module.f.ts b/fjs/effects/eff/module.f.ts index 39c5ed340b..470066607c 100644 --- a/fjs/effects/eff/module.f.ts +++ b/fjs/effects/eff/module.f.ts @@ -1,4 +1,5 @@ -import { history, historyStep, mapStep, pure, type Effect, type Operation } from '../module.f.ts' +import { history, historyStep, mapStep, pure } from '../module.f.mjs' +import type { Effect, Operation } from '../types.ts' /** * A fluent, method-chaining monad over a raw {@link Effect} that also diff --git a/fjs/effects/eff/proof.f.ts b/fjs/effects/eff/proof.f.ts index ad75f43f85..644aba9817 100644 --- a/fjs/effects/eff/proof.f.ts +++ b/fjs/effects/eff/proof.f.ts @@ -1,4 +1,4 @@ -import { do_, match, pure } from '../module.f.ts' +import { do_, match, pure } from '../module.f.mjs' import { assert, assertEq } from '../../asserts/module.f.mjs' import { assertPure } from '../proof.f.ts' import { eff } from './module.f.ts' diff --git a/fjs/effects/list/module.f.mjs b/fjs/effects/list/module.f.mjs new file mode 100644 index 0000000000..928613fcdb --- /dev/null +++ b/fjs/effects/list/module.f.mjs @@ -0,0 +1,34 @@ +/** + * Effectful cons-list construction. + * + * See `./types.ts` for the type-level API. + * + * @module + */ +import { pure } from "../module.f.mjs" +/** @import { Effect, Operation } from "../types.ts" */ +/** @import { List, Next } from "./types.ts" */ + +/** + * The empty `List`: a pure end-of-stream marker (`undefined`). + * + * The explicit `Effect>` return type lets the contextual type drive the + * check, so the recursive payload type-checks without a cast. Construct streams through + * these two combinators. + * + * Note: we use `Effect>` because TypeScript can't convert `pure(...)` to + * `List`. + * + * @type {() => Effect>} + */ +export const empty = () => + pure(undefined) + +/** + * Prepends `first` to a {@link List} `tail`, as a pure cons cell. `tail` is an + * ordinary argument, so it is built before the cell is — see {@link Next}. + * + * @type {(first: T, tail: List) => Effect>} + */ +export const nonEmpty = (first, tail) => + pure({ first, tail }) diff --git a/fjs/effects/list/module.f.ts b/fjs/effects/list/types.ts similarity index 50% rename from fjs/effects/list/module.f.ts rename to fjs/effects/list/types.ts index 1797a029df..9b90e471d7 100644 --- a/fjs/effects/list/module.f.ts +++ b/fjs/effects/list/types.ts @@ -1,4 +1,10 @@ -import { pure, type Effect, type Operation } from "../module.f.ts" +/** + * Types for the effectful cons-list. + * + * @module + */ + +import type { Effect, Operation } from '../types.ts' export type NonEmpty = { readonly first: T @@ -17,32 +23,10 @@ export type NonEmpty = { * the tail is not reached until a runner performs the command. * * `Effect>` is used directly in places where `List` cannot - * be written as a return type (see {@link empty}). + * be written as a return type (see `empty`). */ export type Next = NonEmpty | undefined export type List = Effect> - -/** - * The empty `List`: a pure end-of-stream marker (`undefined`). - * - * The explicit `Effect>` return type lets the contextual type drive the - * check, so the recursive payload type-checks without a cast. Construct streams through - * these two combinators. - * - * Note: we use `Effect>` because TypeScript can't convert `pure(...)` to - * `List`. - */ -export const empty = -(): Effect> => - pure(undefined) - -/** - * Prepends `first` to a {@link List} `tail`, as a pure cons cell. `tail` is an - * ordinary argument, so it is built before the cell is — see {@link Next}. - */ -export const nonEmpty = -(first: T, tail: List): Effect> => - pure({ first, tail }) diff --git a/fjs/effects/memory/module.f.ts b/fjs/effects/memory/module.f.ts index 1973d76db3..e322e91681 100644 --- a/fjs/effects/memory/module.f.ts +++ b/fjs/effects/memory/module.f.ts @@ -16,7 +16,8 @@ import type { Phantom } from '../../types/phantom/types.ts' import type { Nominal } from '../../types/nominal/types.ts' import { asBase as nominalAsBase, asNominal as nominalAsNominal } from '../../types/nominal/module.f.mjs' -import { do_, type Effect } from '../module.f.ts' +import { do_ } from '../module.f.mjs' +import type { Effect } from '../types.ts' /** Nominal brand version for memory keys. */ type MemKeyHash = '3f114fa6036a8da026b827f0c3e6d901f5e81ad9a320e431ccce31451892d286' diff --git a/fjs/effects/memory/proof.f.ts b/fjs/effects/memory/proof.f.ts index 786263e0b0..2473c6211f 100644 --- a/fjs/effects/memory/proof.f.ts +++ b/fjs/effects/memory/proof.f.ts @@ -1,6 +1,6 @@ import { assert, assertEq } from '../../asserts/module.f.mjs' import { run, type MemOperationMap } from '../mock/module.f.ts' -import { pure, step } from '../module.f.ts' +import { pure, step } from '../module.f.mjs' import { asBase, asNominal, create, read, write, diff --git a/fjs/effects/mock/module.f.ts b/fjs/effects/mock/module.f.ts index 55bf3b27ad..c15b94ef85 100644 --- a/fjs/effects/mock/module.f.ts +++ b/fjs/effects/mock/module.f.ts @@ -3,7 +3,8 @@ * * @module */ -import { match, type Effect, type Operation, type Pr } from "../module.f.ts" +import { match } from "../module.f.mjs" +import type { Effect, Operation, Pr } from "../types.ts" /** * A synchronous, state-threading operation map. An entry takes the command's diff --git a/fjs/effects/module.f.ts b/fjs/effects/module.f.mjs similarity index 65% rename from fjs/effects/module.f.ts rename to fjs/effects/module.f.mjs index 248f2c35b3..dff9346915 100644 --- a/fjs/effects/module.f.ts +++ b/fjs/effects/module.f.mjs @@ -4,7 +4,7 @@ * An `Effect` **is** the raw value — a `Pure` thunk (`() => T`) or a `Do` * node (`{ command, payload, continuation }`). It is plain data with no methods. * Composition is provided externally by {@link step}. The optional - * method-chaining wrapper lives in `fjs/effects/eff/module.f.ts`. + * method-chaining wrapper lives in `fjs/effects/eff/module.f.mjs`. * * **Three functions discriminate `Pure` from `Do`** — {@link step}, * {@link match}, and {@link runPure} — plus the node proof in @@ -36,7 +36,7 @@ * **Do not nest steps.** Bind each intermediate effect to its own name, so a * sequence reads top-to-bottom in evaluation order: * - * ```ts + * ```js * // avoid — reads inside-out, and gains a level of indentation per link * step(a, x => step(f(x), y => step(g(y), z => h(z)))) * @@ -57,7 +57,7 @@ * runs first. The closing `)` may sit on its own line or trail the last * argument: * - * ```ts + * ```js * return step( * collectRead(cas.read(hash)), * ([tag, value]) => pure(tag === 'error' ? null : decodeRevisionVec(value))) @@ -68,7 +68,7 @@ * enclosing scope. {@link historyStep} carries the value forward instead, so * the chain stays flat: * - * ```ts + * ```js * // avoid — nested only so `h` can still see `x` * step(a, x => step(f(x), y => h(x, y))) * @@ -92,108 +92,22 @@ * {@link historyStep} the flat form would be unavailable the moment a later * link needed an earlier link's value. * + * See `./types.ts` for the type-level API. + * * @module */ import { assert } from '../asserts/module.f.mjs' -import type { List } from '../types/list/types.ts' +/** @import { List } from '../types/list/types.ts' */ import { fold } from '../types/list/module.f.mjs' import { at } from '../types/object/module.f.mjs' -import type { Option } from '../types/option/types.ts' -import type { Result } from '../types/result/types.ts' - -export type Operation = - readonly[string, (..._: readonly never[]) => unknown] - -/** - * An `Effect` is the raw value: a {@link Pure} thunk that yields `T`, or a - * {@link Do} node describing a command to perform. It is plain data — compose - * effects with the external {@link step}, which is eager wherever the head is - * `Pure`. - */ -export type Effect = - Pure | Do - -/** - * A pure effect: an *already-computed* `T` behind a thunk. - * - * The thunk is a **discriminator, not a suspension**. `Effect` is a union with - * no tag field, so telling its two cases apart needs a runtime test, and - * `typeof e === 'function'` is it — wrapping the value in a function is what - * makes that test work. Deferral is not what the thunk is for. A `Pure` never - * holds work that has yet to happen; everything that *does* something is a - * {@link Do} node, and only a runner performs those. - * - * Two rules follow, and the rest of the module leans on both: - * - * - **The thunk must be pure and total.** Work hidden behind it is an effect - * that no runner ever sees and no {@link OperationMap} can interpret or mock. - * - **It may be called more than once.** Nothing memoizes it. The same effect - * can be decoded repeatedly — `Eff` re-forces the effect it wraps on each - * `.step` — and under the first rule that costs nothing and changes nothing. - * - * A `lazy` constructor (`(t: () => T): Effect => t`) once existed - * to advertise the thunk as a suspension. It was the identity function, and it - * promised a deferral this representation does not keep; it has been removed. - * Reintroducing it would reintroduce the contradiction, not fix one. - */ -export type Pure = - () => T - -export type Pr = - O extends readonly[K, (...args: infer P) => infer R] ? readonly[P, R] : never +/** @import { Option } from '../types/option/types.ts' */ +/** @import { Result } from '../types/result/types.ts' */ +/** @import { Fold } from '../types/function/operator/types.ts' */ +/** @import { Cont, Do, Effect, F, History, MatchResult, Operation, OperationMap, Param, Pr, Pure, Return, ToAsyncOperationMap } from './types.ts' */ -/** - * A `Do` node's continuation: given the command's output, produce the rest of - * the effect. - * - * The `out O` annotation asserts a covariance TypeScript cannot derive through - * the conditional `Pr` type: the command's output sits in the *contravariant* - * parameter position, so a bare function type would be measured contravariant - * in `O`, but the effect system only ever *widens* `O` (grows the op-set), never - * narrows it. - * - * **It is sound.** The `command` tag pins exactly which command's output the - * continuation receives, and every interpreter dispatches on the tag first - * ({@link match} → runner), so a `write` node's continuation is only ever - * called with `void`; the op-set can grow without any continuation ever being - * handed the wrong output. `out` enables only the widening direction - * (`Effect` <: `Effect`), never the unsound narrowing. Anyone changing - * the continuation representation must re-check this argument before keeping the - * annotation. - */ -export type Cont = - (_: Pr[1]) => Effect - -/** - * A `Do` node: the command to perform, its payload, and the continuation to - * resume with the command's output. Its runtime value is exactly this record, - * and every reader destructures it by name — - * `const { command, payload, continuation } = e`. - * - * It must be an object rather than a tuple, and that is not a style choice: - * only object / function / mapped-type aliases may carry a variance annotation - * (`TS2637` forbids `out` on a tuple), and the raw `Effect` union must be - * covariant in `O` end to end. `command` and `payload` are indexed/conditional - * types over `O` that TypeScript will not widen generically on their own — - * annotating only {@link Cont} is not enough — so the whole node carries - * `out O`. The same tag-dispatch soundness argument that justifies `Cont`'s - * `out O` applies here (see {@link Cont}); widening only ever grows the op-set. - * - * The fields were once numeric (`0` / `1` / `2`) over a real `[cmd, param, - * cont]` array, which is where the positional reads and the `Decoded` record - * that wrapped them came from. Nothing needed the positions: the constraint - * above is satisfied by any object type, so the numeric keys were paying a - * tuple's price without being a tuple. Named fields make the node - * self-describing at every read and leave no layout to memorize. - */ -export type Do = { - readonly command: O[0] - readonly payload: Pr[0] - readonly continuation: Cont -} - -export const pure = (v: T): Effect => () => v +/** @type {(v: T) => Effect} */ +export const pure = v => () => v /** * Composes effects: run `e`, then continue with `f` applied to its result. @@ -221,11 +135,10 @@ export const pure = (v: T): Effect => () => v * without performing it yet has to keep the ingredients and defer the `step` * itself — `Eff` does exactly this, holding its history tuple as a thunk (`h`) * precisely because composing it eagerly is the one thing it cannot take back. + * + * @type {(e: Effect, f: (t: T) => Effect) => Effect} */ -export const step = ( - e: Effect, - f: (t: T) => Effect -): Effect => +export const step = (e, f) => typeof e === 'function' ? f(e()) : { ...e, continuation: x => step(e.continuation(x), f) } @@ -249,28 +162,11 @@ export const step = ( * () => v)` already reads clearly, and it keeps `v`'s evaluation inside the * continuation where `step` puts it, rather than moving it to where the * composition is written. - */ -export const mapStep = ( - e: Effect, - f: (t: T) => R -): Effect => - step(e, t => pure(f(t))) - -/** - * An effect whose result is a **history tuple**: the values a chain has bound so - * far, newest first. `History` is three links deep, with - * `A` bound earliest. * - * This is a transparent alias for {@link Effect}. It adds the tuple bound and - * nothing else, so any tuple-valued effect satisfies it whether or not - * {@link history} produced it — it names the convention at the signatures that - * rely on it rather than enforcing it. - * - * Heterogeneous by design: each element has its own type, so this is not a - * `List` and nothing that folds or maps a list applies to it. + * @type {(e: Effect, f: (t: T) => R) => Effect} */ -export type History = - Effect +export const mapStep = (e, f) => + step(e, t => pure(f(t))) /** * Like {@link step}, but keeps the values instead of discarding them: runs `e` @@ -282,7 +178,7 @@ export type History = * a later link has no way to reach an earlier one. `historyStep` carries every * earlier value forward, and the next destructuring names the parts: * - * ```ts + * ```js * const b = historyStep(history(a), decodeRevisionBlob(cas)) * const c = step(b, ([revision, hash]) => ...) * ``` @@ -292,7 +188,7 @@ export type History = * history and returns one, so it composes with itself to any depth; only the * entry point needs {@link history}: * - * ```ts + * ```js * const h0 = history(readHash(cas)) * const h1 = historyStep(h0, hash => decodeRevisionBlob(cas)(hash)) * const h2 = historyStep(h1, (revision, hash) => ...) @@ -304,19 +200,17 @@ export type History = * rather than a traversal, but a long chain makes the positions hard to count. * When that starts to hurt, collapse it into a record of named fields * (`pure({ hash, revision } as const)`) and start a fresh history from there. + * + * `Readonly

` on `f`'s rest parameter is load-bearing: inferring `P` from a + * bare rest parameter yields a *mutable*, labelled tuple (`[next: string]`), + * which then rejects the `readonly` tuples every history is built from. + * + * @type {( + * e: History, + * f: (...p: Readonly

) => Effect + * ) => History} */ -export const historyStep = < - O extends Operation, - P extends readonly unknown[], - Q extends Operation, - R ->( - e: History, - // `Readonly

` is load-bearing: inferring `P` from a bare rest parameter - // yields a *mutable*, labelled tuple (`[next: string]`), which then rejects - // the `readonly` tuples every history is built from. - f: (...p: Readonly

) => Effect -): History => +export const historyStep = (e, f) => step(e, param => step(f(...param), result => pure([result, ...param]))) /** @@ -329,18 +223,16 @@ export const historyStep = < * chains stop composing: such a step nests its predecessor's tuple instead of * flattening it, so link two would have to be spelled differently from link * three. + * + * @type {(e: Effect) => History} */ -export const history = (e: Effect): History => +export const history = e => step(e, v => pure([v])) -export type Param = F[0] - -export type Return = F[1] - -export const do_ = - (command: O[0]) => - (...payload: Param): Effect> => - ({ command, payload, continuation: pure }) +/** + * @type {(command: O[0]) => (...payload: Param) => Effect>} + */ +export const do_ = command => (...payload) => ({ command, payload, continuation: pure }) /** * Sequentially threads a state value through an effect for each item produced by @@ -379,35 +271,42 @@ export const do_ = * `forEachStep` — takes its effect first and breaks one argument per line when * it wraps (see this module's header): every such call is a statement list, and * each line is one statement in execution order. + * + * @template {Operation} O + * @template T + * @template {Operation} Q + * @template S + * @param {Effect>} items + * @param {S} init + * @param {(item: T) => (state: S) => Effect} f + * @returns {Effect} */ -export const foldStep = ( - items: Effect>, - init: S, - f: (item: T) => (state: S) => Effect -): Effect => - step(items, fold>(item => acc => step(acc, f(item)))(pure(init))) +export const foldStep = (items, init, f) => { + /** @type {Fold>} */ + const op = item => acc => step(acc, f(item)) + return step(items, fold(op)(pure(init))) +} /** * Sequentially runs `f(item)` for each item produced by `items`, discarding * intermediate results. The `void` accumulator sibling of {@link foldStep}, and * a step variant on the same grounds. + * + * @type {(items: Effect>, f: (item: T) => Effect) => Effect} */ -export const forEachStep = ( - items: Effect>, - f: (item: T) => Effect -): Effect => - foldStep(items, undefined, (item: T) => () => f(item)) +export const forEachStep = (items, f) => + foldStep(items, undefined, item => () => f(item)) /** * A step adapter for the `error` short-circuit: `error` → pass it through * unchanged as `pure`, `ok` → continue with `f`. Collapses the hand-written * `r[0] === 'error' ? pure(r) : f(r[1])` check that recurs at every site * chaining `Effect>` steps. + * + * @type {(f: (value: T) => Effect>) => (r: Result) => Effect>} */ -export const okStep = - (f: (value: T) => Effect>) => - (r: Result): Effect> => - r[0] === 'error' ? pure(r) : f(r[1]) +export const okStep = f => r => + r[0] === 'error' ? pure(r) : f(r[1]) /** * Runs an effect that reaches its value without performing a command: `[t]` for @@ -429,23 +328,12 @@ export const okStep = * not the reverse — a continuation's result is always the wider type and would * be rejected. `Do` is uninhabited besides, which would make the empty * case unreachable without a cast. + * + * @type {(e: Effect) => Option} */ -export const runPure = (e: Effect): Option => +export const runPure = e => typeof e === 'function' ? [e()] : [] -/** - * An operation map whose entries take a command's payload and return some - * output `R`. Generalizes `ToAsyncOperationMap` (`R = Promise<…>`) and the - * curried `MemOperationMap` (`R = (state) => [state, …]`). - */ -export type OperationMap = { - readonly [K in O[0]]: (...payload: Pr[0]) => R -} - -export type MatchResult = - | readonly['done', T] - | readonly['cont', R, Do['continuation']] - /** * Decodes an effect's next step and dispatches its command to `map`, * returning either the final result or the operation's output `R` paired @@ -472,21 +360,23 @@ export type MatchResult = * (`assert`) rather than widening {@link MatchResult} with a variant no * type-correct caller could ever observe — a runner cannot resume a command it * has no handler for, so there is nothing for a recovery branch to do. + * + * @template {Operation} O + * @template R + * @param {OperationMap} map */ -export const match = - (map: OperationMap) => - (e: Effect): MatchResult => { +export const match = map => + /** + * @template {O} O1 + * @template T + * @param {Effect} e + * @returns {MatchResult} + */ + e => { if (typeof e === 'function') { return ['done', e()] } const { command, payload, continuation } = e - const handler = at(command)[O[0]]>(map) + const handler = /** @type {(...payload: readonly unknown[]) => R} */ + (at(command)(/** @type {any} */ (map))) assert(handler !== null, command) return ['cont', handler(...payload), continuation] } - -export type ToAsyncOperationMap = { - readonly [K in O[0]]: (...payload: Pr[0]) => Promise[1]> -} - -export type F = Pr - -export type Func = (..._: Param) => Effect> diff --git a/fjs/effects/module.ts b/fjs/effects/module.ts index 903346e112..94ce3573dd 100644 --- a/fjs/effects/module.ts +++ b/fjs/effects/module.ts @@ -1,4 +1,5 @@ -import { match, type Effect, type Operation, type ToAsyncOperationMap } from "./module.f.ts" +import { match } from "./module.f.mjs" +import type { Effect, Operation, ToAsyncOperationMap } from "./types.ts" export const asyncRun = (map: ToAsyncOperationMap) => diff --git a/fjs/effects/node/memory/module.ts b/fjs/effects/node/memory/module.ts index c8fb5f2935..908555fd8e 100644 --- a/fjs/effects/node/memory/module.ts +++ b/fjs/effects/node/memory/module.ts @@ -6,7 +6,7 @@ import { randomUUID } from 'node:crypto' import { asyncRun } from '../../module.ts' -import type { Effect, ToAsyncOperationMap } from '../../module.f.ts' +import type { Effect, ToAsyncOperationMap } from '../../types.ts' import { asBase, asNominal, type Key, type MemOp } from '../../memory/module.f.ts' export type MemoryOperationMap = ToAsyncOperationMap diff --git a/fjs/effects/node/memory/proof.ts b/fjs/effects/node/memory/proof.ts index 821b861b5a..a58b7be895 100644 --- a/fjs/effects/node/memory/proof.ts +++ b/fjs/effects/node/memory/proof.ts @@ -12,7 +12,7 @@ import { } from '../../memory/module.f.ts' import { memoryOperationMap, run } from './module.ts' import { assert, assertEq } from '../../../asserts/module.f.mjs' -import { step } from '../../module.f.ts' +import { step } from '../../module.f.mjs' export const proof = { nodeInterpreter: async () => { diff --git a/fjs/effects/node/module.f.ts b/fjs/effects/node/module.f.ts index 1bd06842ca..fab77417f4 100644 --- a/fjs/effects/node/module.f.ts +++ b/fjs/effects/node/module.f.ts @@ -20,8 +20,9 @@ import type { Nominal } from '../../types/nominal/types.ts' import type { Result } from '../../types/result/types.ts' import { ok, error as resultError, mapOk } from '../../types/result/module.f.mjs' import type { StringMap } from '../../types/object/types.ts' -import { type Effect, type Func, type Operation, type ToAsyncOperationMap, do_, mapStep, okStep, pure, step } from '../module.f.ts' -import type { List } from '../list/module.f.ts' +import { do_, mapStep, okStep, pure, step } from '../module.f.mjs' +import type { Effect, Func, Operation, ToAsyncOperationMap } from '../types.ts' +import type { List } from '../list/types.ts' export type IoResult = Result diff --git a/fjs/effects/node/module.ts b/fjs/effects/node/module.ts index ea9da3926d..91b242d746 100644 --- a/fjs/effects/node/module.ts +++ b/fjs/effects/node/module.ts @@ -22,7 +22,7 @@ import { once } from 'node:events' import * as testContext from 'node:test' import { concat, normalize, toPosix } from '../../path/module.f.mjs' -import { type Effect } from '../module.f.ts' +import type { Effect } from '../types.ts' import { asyncRun } from '../module.ts' import { memoryOperationMap } from './memory/module.ts' import { diff --git a/fjs/effects/node/proof.f.ts b/fjs/effects/node/proof.f.ts index bd1480de48..5b00891727 100644 --- a/fjs/effects/node/proof.f.ts +++ b/fjs/effects/node/proof.f.ts @@ -1,10 +1,10 @@ import type { Vec } from "../../types/bit_vec/types.ts" import { empty, isVec, uint, vec, vec8 } from "../../types/bit_vec/module.f.mjs" import { utf8, utf8ToString } from "../../text/module.f.mjs" -import { match, pure, step } from "../module.f.ts" +import { match, pure, step } from "../module.f.mjs" import { both, fetch, mkdir, now, readdir, readFile, readUtf8File, rm, sandbox, writeFile, writeUtf8File, rename, readBytes, randomInt, writeFromStream, usesInlineTestContext, versionLessThan, type IoResult, type ReadFile } from "./module.f.ts" import { create as memCreate, read as memRead, write as memWrite } from "../memory/module.f.ts" -import { empty as listEmpty, nonEmpty as listNonEmpty } from "../list/module.f.ts" +import { empty as listEmpty, nonEmpty as listNonEmpty } from "../list/module.f.mjs" import { emptyState, virtual, type Dir } from "./virtual/module.f.ts" import { assert, assertEq, assertNotNullish } from '../../asserts/module.f.mjs' import { ok } from '../../types/result/module.f.mjs' diff --git a/fjs/effects/node/todo/ornotfound-combinator.md b/fjs/effects/node/todo/ornotfound-combinator.md index 86f49c0b06..93a5e54cac 100644 --- a/fjs/effects/node/todo/ornotfound-combinator.md +++ b/fjs/effects/node/todo/ornotfound-combinator.md @@ -19,7 +19,7 @@ another site appears. ### Proposal A **step adapter**: a continuation factory passed to `.step`, not a wrapper -taking the effect — the shape `okStep` (`fjs/effects/module.f.ts`) already +taking the effect — the shape `okStep` (`fjs/effects/module.f.mjs`) already uses for the two-way ok/error case. The wrapper shape proposed earlier — `orNotFound(effect)(notFound)(onOk)` — recreates the nesting problem the moment two policies chain @@ -53,5 +53,5 @@ list: () => access(storePrefix).step(orNotFound([])(() => ### Related -- `okStep` (`fjs/effects/module.f.ts`) — the step-adapter convention this +- `okStep` (`fjs/effects/module.f.mjs`) — the step-adapter convention this follows; the two-way sibling of this three-way policy. diff --git a/fjs/effects/proof.f.ts b/fjs/effects/proof.f.ts index 0529102593..131d574a4c 100644 --- a/fjs/effects/proof.f.ts +++ b/fjs/effects/proof.f.ts @@ -1,4 +1,5 @@ -import { step, do_, foldStep, forEachStep, mapStep, match, okStep, history, pure, runPure, type Effect, type Operation, historyStep } from './module.f.ts' +import { step, do_, foldStep, forEachStep, mapStep, match, okStep, history, pure, runPure, historyStep } from './module.f.mjs' +import type { Effect, Operation } from './types.ts' import { error, ok } from '../types/result/module.f.mjs' import { assert, assertEq } from '../asserts/module.f.mjs' diff --git a/fjs/effects/todo/allvoid-combinator.md b/fjs/effects/todo/allvoid-combinator.md index 6841deeb3a..96ede79edf 100644 --- a/fjs/effects/todo/allvoid-combinator.md +++ b/fjs/effects/todo/allvoid-combinator.md @@ -41,7 +41,7 @@ methods, so `.step` is reachable only through the `Eff` wrapper (`fjs/effects/eff/module.f.ts`). An earlier draft of this issue quoted these sites as `all(...).step(...)` — that form does not exist and would not compile. -`fjs/effects/module.f.ts` already ships `forEachStep` (the *sequential* void +`fjs/effects/module.f.mjs` already ships `forEachStep` (the *sequential* void combinator, line 90), and [allreduce-combinator](./allreduce-combinator.md) covers the parallel *reduce* variant — but the parallel *void* sibling is missing, so every call site re-spells the whole wrap-step-unwrap dance. @@ -50,7 +50,7 @@ missing, so every call site re-spells the whole wrap-step-unwrap dance. Add the void sibling in `fjs/effects/node/module.f.ts`, next to `all` / `All` / `both`. It cannot live next to `forEachStep` in the core -`fjs/effects/module.f.ts`: `all`/`All` are defined in the node module, +`fjs/effects/module.f.mjs`: `all`/`All` are defined in the node module, which already imports the core module — placing `allVoid` in core would invert that dependency. (`fjs/emergent_testing` already imports `all` from the node module, so the call sites need no new import path.) @@ -95,4 +95,4 @@ duplicating the `all(...map)` core — whichever reads better. - [allreduce-combinator](./allreduce-combinator.md) — the aggregating sibling; `allVoid` discards. -- `fjs/effects/module.f.ts:90` — `forEachStep`, the sequential sibling. +- `fjs/effects/module.f.mjs:297` — `forEachStep`, the sequential sibling. diff --git a/fjs/effects/todo/effect-list-fold.md b/fjs/effects/todo/effect-list-fold.md index 1d30c48a41..c1c6dd0193 100644 --- a/fjs/effects/todo/effect-list-fold.md +++ b/fjs/effects/todo/effect-list-fold.md @@ -11,7 +11,7 @@ element exists in memory before the first `f` runs, and the fold cannot begin until the last element has been produced. That is not the shape the codebase's real sequences have. -`fjs/effects/list/module.f.ts` already defines the streaming one: +`fjs/effects/list/module.f.mjs` already defines the streaming one: ```ts export type List = Effect> @@ -26,7 +26,7 @@ combinators speak the wrong one. Three consequences: in the store lives in memory at once, purely because `foldStep` cannot consume anything else. -**The layering is inverted.** `fjs/effects/module.f.ts` — the core effect module +**The layering is inverted.** `fjs/effects/module.f.mjs` — the core effect module — imports `fjs/types/list` (line 95) for `fold` and `List`, and *nothing else in that file uses either*. The two fold combinators are the module's only dependency on the strict list type. @@ -39,13 +39,13 @@ places. That is `foldStep` over a stream, plus a short-circuit. ### Proposal **1. Rename `List` → `EffectList`** in -`fjs/effects/list/module.f.ts`. It collides with `fjs/types/list`'s `List`, +`fjs/effects/list/module.f.mjs`. It collides with `fjs/types/list`'s `List`, and the eight importers currently alias around the clash — `elEmpty` in `fjs/cas`, `fjs/cas/evo`, `fjs/mcp`; `emptyList` in `fjs/media/type/proof`. The rename has value independent of the rest of this issue. -**2. Move `foldStep` / `forEachStep`** out of `fjs/effects/module.f.ts` and into -`fjs/effects/list/module.f.ts`, retyped over `EffectList`: +**2. Move `foldStep` / `forEachStep`** out of `fjs/effects/module.f.mjs` and into +`fjs/effects/list/module.f.mjs`, retyped over `EffectList`: ```ts export const foldStep = ( @@ -64,7 +64,7 @@ Keep the step-variant shape — effect first, one argument per line when the cal wraps. That is not a style preference: a step variant is this module's `do` notation, so the argument list is a statement list in execution order and the effect comes first because it happens first. The rationale currently lives on -`foldStep`'s JSDoc and in the `fjs/effects/module.f.ts` header; carry both over. +`foldStep`'s JSDoc and in the `fjs/effects/module.f.mjs` header; carry both over. **3. Add a strict-list converter.** All six current call sites hold a strict list, so the move needs a way in: @@ -76,9 +76,9 @@ export const fromList = (items: List): EffectList` → `EffectList` in `fjs/effects/list/module.f.ts`; +- [ ] Rename `List` → `EffectList` in `fjs/effects/list/module.f.mjs`; update the eight importers and drop the `elEmpty` / `emptyList` aliases that existed only to dodge the name clash. -- [ ] Add `fromList` to `fjs/effects/list/module.f.ts`. +- [ ] Add `fromList` to `fjs/effects/list/module.f.mjs`. - [ ] Move `foldStep` / `forEachStep` there, retyped over `EffectList`, carrying their JSDoc and the step-variant rationale. - [ ] Remove the now-unused `fjs/types/list` import from - `fjs/effects/module.f.ts`. + `fjs/effects/module.f.mjs`. - [ ] Migrate the six call sites; the four strict ones go through `fromList`. - [ ] Create `fjs/effects/list/proof.f.ts` with full coverage. - [ ] Re-scope or close [fold-stream-combinator](./fold-stream-combinator.md). @@ -154,5 +154,5 @@ follow-up in `fjs/cas` (see *Related*), not part of this issue. it is specified over `List` and will want the same treatment. - [write-closed-helpers](../../cas/todo/write-closed-helpers.md) — already blocked by `fold-stream-combinator`, so transitively affected. -- `fjs/effects/module.f.ts` header — the step-variant / `do`-notation rationale +- `fjs/effects/module.f.mjs` header — the step-variant / `do`-notation rationale that fixes the argument order. diff --git a/fjs/effects/todo/fold-stream-combinator.md b/fjs/effects/todo/fold-stream-combinator.md index addb4f11f1..738a753649 100644 --- a/fjs/effects/todo/fold-stream-combinator.md +++ b/fjs/effects/todo/fold-stream-combinator.md @@ -53,13 +53,13 @@ in sync. ### Proposal -Add a `foldStream` combinator to `fjs/effects/list/module.f.ts` whose step +Add a `foldStream` combinator to `fjs/effects/list/module.f.mjs` whose step returns an `Effect`, so it subsumes both the pure folds (step = `pure(...)`) and the effectful writers. **Layering note:** `IoResult` cannot appear in this module's signature — it is exported by `fjs/effects/node/module.f.ts`, which already imports -`List` from `fjs/effects/list/module.f.ts`, so importing it back would be +`List` from `fjs/effects/list/module.f.mjs`, so importing it back would be a cycle. But `IoResult` is just an alias for `Result` from `fjs/types/result` (a types-layer module `effects/list` can import freely), so write the signature in terms of `Result` — call sites that hold @@ -101,7 +101,7 @@ consumers first; the writers follow only if the shape stays clean. ### Tasks -- [ ] Add `foldStream` to `fjs/effects/list/module.f.ts` with proof coverage. +- [ ] Add `foldStream` to `fjs/effects/list/module.f.mjs` with proof coverage. - [ ] Convert `detectStream` (`fjs/media/type`) and `collectRead` (`fjs/cas`). - [ ] Convert `writeLoop` (`fjs/effects/node`) if the effectful step fits without contortion; otherwise document why in this issue and keep it. @@ -118,7 +118,7 @@ consumers first; the writers follow only if the shape stays clean. abstraction; `foldStream` is the consumer-side generalization. - [allreduce-combinator](./allreduce-combinator.md) — sibling combinator for parallel effects. -- `okStep` (`fjs/effects/module.f.ts`) — the step-adapter helper shape; this +- `okStep` (`fjs/effects/module.f.mjs`) — the step-adapter helper shape; this combinator's per-chunk step is a Kleisli function of the same shape. - [write-closed-helpers](../../cas/todo/write-closed-helpers.md) — hoists `fileCas.write`'s remaining nested helpers; its loop conversion depends on diff --git a/fjs/effects/todo/map-step-combinator.md b/fjs/effects/todo/map-step-combinator.md index 886716201a..e0b8a47e7f 100644 --- a/fjs/effects/todo/map-step-combinator.md +++ b/fjs/effects/todo/map-step-combinator.md @@ -3,7 +3,7 @@ **Priority:** P3 **Status:** open -> **The APIs have landed.** `mapStep` is in `fjs/effects/module.f.ts` and +> **The APIs have landed.** `mapStep` is in `fjs/effects/module.f.mjs` and > `Eff.map` in `fjs/effects/eff/module.f.ts`, each with proof coverage and with > its first real consumers converted in the same change — `readUtf8File`, > `awaitIfPromise` and `errorExit` (`fjs/effects/node/module.f.ts`), @@ -13,7 +13,7 @@ ### Problem -`fjs/effects/module.f.ts` ships `pure` (return) and `step` (bind), plus the +`fjs/effects/module.f.mjs` ships `pure` (return) and `step` (bind), plus the derived combinators `historyStep`, `foldStep`, `forEachStep`, the `okStep` adapter — and now `mapStep`, the functor `map`: "run the effect, then apply a pure function to its result". Before it existed, every call site re-derived it @@ -124,6 +124,6 @@ conversion as one 14-module diff. projection (`() => undefined`). - [fold-stream-combinator](./fold-stream-combinator.md) — its pure consumers (`detectStream`, `collectRead`) end in `pure(ok(...))` projections. -- `fjs/effects/module.f.ts` — `mapStep`, `step`, `historyStep`, `foldStep`, +- `fjs/effects/module.f.mjs` — `mapStep`, `step`, `historyStep`, `foldStep`, `forEachStep`, `okStep`; the "do not nest steps" rule in the module header. diff --git a/fjs/effects/types.ts b/fjs/effects/types.ts new file mode 100644 index 0000000000..5fd68d59c6 --- /dev/null +++ b/fjs/effects/types.ts @@ -0,0 +1,137 @@ +/** + * Types for the core effect system. + * + * @module + */ + +export type Operation = + readonly[string, (..._: readonly never[]) => unknown] + +/** + * An `Effect` is the raw value: a {@link Pure} thunk that yields `T`, or a + * {@link Do} node describing a command to perform. It is plain data — compose + * effects with the external `step`, which is eager wherever the head is + * `Pure`. + */ +export type Effect = + Pure | Do + +/** + * A pure effect: an *already-computed* `T` behind a thunk. + * + * The thunk is a **discriminator, not a suspension**. `Effect` is a union with + * no tag field, so telling its two cases apart needs a runtime test, and + * `typeof e === 'function'` is it — wrapping the value in a function is what + * makes that test work. Deferral is not what the thunk is for. A `Pure` never + * holds work that has yet to happen; everything that *does* something is a + * {@link Do} node, and only a runner performs those. + * + * Two rules follow, and the rest of the module leans on both: + * + * - **The thunk must be pure and total.** Work hidden behind it is an effect + * that no runner ever sees and no `OperationMap` can interpret or mock. + * - **It may be called more than once.** Nothing memoizes it. The same effect + * can be decoded repeatedly — `Eff` re-forces the effect it wraps on each + * `.step` — and under the first rule that costs nothing and changes nothing. + * + * A `lazy` constructor (`(t: () => T): Effect => t`) once existed + * to advertise the thunk as a suspension. It was the identity function, and it + * promised a deferral this representation does not keep; it has been removed. + * Reintroducing it would reintroduce the contradiction, not fix one. + */ +export type Pure = + () => T + +export type Pr = + O extends readonly[K, (...args: infer P) => infer R] ? readonly[P, R] : never + +/** + * A `Do` node's continuation: given the command's output, produce the rest of + * the effect. + * + * The `out O` annotation asserts a covariance TypeScript cannot derive through + * the conditional `Pr` type: the command's output sits in the *contravariant* + * parameter position, so a bare function type would be measured contravariant + * in `O`, but the effect system only ever *widens* `O` (grows the op-set), never + * narrows it. + * + * **It is sound.** The `command` tag pins exactly which command's output the + * continuation receives, and every interpreter dispatches on the tag first + * (`match` → runner), so a `write` node's continuation is only ever + * called with `void`; the op-set can grow without any continuation ever being + * handed the wrong output. `out` enables only the widening direction + * (`Effect` <: `Effect`), never the unsound narrowing. Anyone changing + * the continuation representation must re-check this argument before keeping the + * annotation. + */ +export type Cont = + (_: Pr[1]) => Effect + +/** + * A `Do` node: the command to perform, its payload, and the continuation to + * resume with the command's output. Its runtime value is exactly this record, + * and every reader destructures it by name — + * `const { command, payload, continuation } = e`. + * + * It must be an object rather than a tuple, and that is not a style choice: + * only object / function / mapped-type aliases may carry a variance annotation + * (`TS2637` forbids `out` on a tuple), and the raw `Effect` union must be + * covariant in `O` end to end. `command` and `payload` are indexed/conditional + * types over `O` that TypeScript will not widen generically on their own — + * annotating only {@link Cont} is not enough — so the whole node carries + * `out O`. The same tag-dispatch soundness argument that justifies `Cont`'s + * `out O` applies here (see {@link Cont}); widening only ever grows the op-set. + * + * The fields were once numeric (`0` / `1` / `2`) over a real `[cmd, param, + * cont]` array, which is where the positional reads and the `Decoded` record + * that wrapped them came from. Nothing needed the positions: the constraint + * above is satisfied by any object type, so the numeric keys were paying a + * tuple's price without being a tuple. Named fields make the node + * self-describing at every read and leave no layout to memorize. + */ +export type Do = { + readonly command: O[0] + readonly payload: Pr[0] + readonly continuation: Cont +} + +/** + * An effect whose result is a **history tuple**: the values a chain has bound so + * far, newest first. `History` is three links deep, with + * `A` bound earliest. + * + * This is a transparent alias for `Effect`. It adds the tuple bound and + * nothing else, so any tuple-valued effect satisfies it whether or not + * `history` produced it — it names the convention at the signatures that + * rely on it rather than enforcing it. + * + * Heterogeneous by design: each element has its own type, so this is not a + * `List` and nothing that folds or maps a list applies to it. + */ +export type History = + Effect + +export type Param = F[0] + +export type Return = F[1] + +/** + * An operation map whose entries take a command's payload and return some + * output `R`. Generalizes `ToAsyncOperationMap` (`R = Promise<…>`) and the + * curried `MemOperationMap` (`R = (state) => [state, …]`). + */ +export type OperationMap = { + readonly [K in O[0]]: (...payload: Pr[0]) => R +} + +export type MatchResult = + | readonly['done', T] + | readonly['cont', R, Do['continuation']] + +export type ToAsyncOperationMap = { + readonly [K in O[0]]: (...payload: Pr[0]) => Promise[1]> +} + +export type F = Pr + +export type Func = (..._: Param) => Effect> diff --git a/fjs/emergent_testing/module.f.ts b/fjs/emergent_testing/module.f.ts index c736faeb8d..c9f4e136ff 100644 --- a/fjs/emergent_testing/module.f.ts +++ b/fjs/emergent_testing/module.f.ts @@ -29,7 +29,8 @@ import { type Write, type WriteConsoles } from '../effects/node/module.f.ts' -import { history, historyStep, pure, step, type Effect, type Operation } from '../effects/module.f.ts' +import { history, historyStep, pure, step } from '../effects/module.f.mjs' +import type { Effect, Operation } from '../effects/types.ts' import { eff } from '../effects/eff/module.f.ts' import { loadModuleMap, shouldLoad, type LoadModuleOperations, type ModuleMap } from '../dev/module.f.ts' import { invert } from '../types/result/module.f.mjs' diff --git a/fjs/emergent_testing/proof.f.ts b/fjs/emergent_testing/proof.f.ts index 9879ab3773..d96c70383c 100644 --- a/fjs/emergent_testing/proof.f.ts +++ b/fjs/emergent_testing/proof.f.ts @@ -1,4 +1,4 @@ -import type { Effect } from '../effects/module.f.ts' +import type { Effect } from '../effects/types.ts' import { log, type NodeProgramOptions, type Sandbox, type Write } from '../effects/node/module.f.ts' import { defaultNodeProgramOptions, emptyState, type JsModule } from '../effects/node/virtual/module.f.ts' import { virtual } from '../effects/node/virtual/module.f.ts' diff --git a/fjs/mcp/cas/module.f.ts b/fjs/mcp/cas/module.f.ts index d82ba05352..1b44436ee8 100644 --- a/fjs/mcp/cas/module.f.ts +++ b/fjs/mcp/cas/module.f.ts @@ -103,7 +103,8 @@ */ import { string, option, or, boolean } from '../../types/rtti/module.f.mjs' import { stringify } from '../../media/json/module.f.ts' -import { pure, step, type Effect } from '../../effects/module.f.ts' +import { pure, step } from '../../effects/module.f.mjs' +import type { Effect } from '../../effects/types.ts' import { type MemOp } from '../../effects/memory/module.f.ts' import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.mjs' import { decode as base64Decode, encode as base64Encode } from '../../basen/base64/module.f.mjs' @@ -123,7 +124,7 @@ import { collectRead, fileCas, type FileCasOperation } from '../../cas/module.f. import { fromVec } from '../../text/utf8/module.f.mjs' import { identity } from '../../types/function/module.f.mjs' import { sha256 } from '../../crypto/sha2/module.f.mjs' -import { nonEmpty, empty as elEmpty } from '../../effects/list/module.f.ts' +import { nonEmpty, empty as elEmpty } from '../../effects/list/module.f.mjs' import { syncRevision, type Cache } from '../../cas/evo/module.f.ts' import type { Key } from '../../effects/memory/module.f.ts' diff --git a/fjs/mcp/evo/module.f.ts b/fjs/mcp/evo/module.f.ts index f227e4104a..fac572e5b4 100644 --- a/fjs/mcp/evo/module.f.ts +++ b/fjs/mcp/evo/module.f.ts @@ -44,7 +44,8 @@ * @module */ import { string, option, array } from '../../types/rtti/module.f.mjs' -import { pure, step, type Effect, type Operation } from '../../effects/module.f.ts' +import { pure, step } from '../../effects/module.f.mjs' +import type { Effect, Operation } from '../../effects/types.ts' import { type MemOp } from '../../effects/memory/module.f.ts' import { toolEntry, errorResult, okResult, diff --git a/fjs/mcp/evo/proof.f.ts b/fjs/mcp/evo/proof.f.ts index b73b281bf1..9c4dae4b43 100644 --- a/fjs/mcp/evo/proof.f.ts +++ b/fjs/mcp/evo/proof.f.ts @@ -7,7 +7,7 @@ import { vecToCBase32 } from '../../basen/cbase32/module.f.mjs' import { initEvo, evo, type Evo } from '../../cas/evo/module.f.ts' import { evoToolRegistry } from './module.f.ts' import type { ToolEntry, ToolsCallResult } from '../../protocol/mcp/module.f.ts' -import type { Operation } from '../../effects/module.f.ts' +import type { Operation } from '../../effects/types.ts' import { parse as parseJson } from '../../media/json/module.f.ts' import { array, string as rttiString } from '../../types/rtti/module.f.mjs' import { parse as rttiParse } from '../../types/rtti/parse/module.f.mjs' diff --git a/fjs/mcp/module.f.ts b/fjs/mcp/module.f.ts index 7da97c002a..7e5b439406 100644 --- a/fjs/mcp/module.f.ts +++ b/fjs/mcp/module.f.ts @@ -26,7 +26,8 @@ * * @module */ -import { step, type Effect } from '../effects/module.f.ts' +import { step } from '../effects/module.f.mjs' +import type { Effect } from '../effects/types.ts' import { create, type MemOp } from '../effects/memory/module.f.ts' import { type Read, type Write } from '../effects/node/module.f.ts' import { stdioTransport } from '../protocol/mcp/stdio/module.f.ts' diff --git a/fjs/mcp/proof.f.ts b/fjs/mcp/proof.f.ts index d00e2f4a5e..a9b9ef99dc 100644 --- a/fjs/mcp/proof.f.ts +++ b/fjs/mcp/proof.f.ts @@ -1,5 +1,6 @@ import { assert, assertEq } from '../asserts/module.f.mjs' -import { pure, step, type Effect, type Operation } from '../effects/module.f.ts' +import { pure, step } from '../effects/module.f.mjs' +import type { Effect, Operation } from '../effects/types.ts' import { create } from '../effects/memory/module.f.ts' import { parse as parseJson, type Unknown } from '../media/json/module.f.ts' import { number as rttiNumber, option, string as rttiString } from '../types/rtti/module.f.mjs' @@ -13,7 +14,8 @@ import { utf8 } from '../text/module.f.mjs' import { fileCas, type FileCasOperation } from '../cas/module.f.ts' import { dialect as revisionDialect, mediaType as revisionMediaType } from '../media/revision/module.f.ts' import { sha256 } from '../crypto/sha2/module.f.mjs' -import { nonEmpty, empty as elEmpty, type List } from '../effects/list/module.f.ts' +import { nonEmpty, empty as elEmpty } from '../effects/list/module.f.mjs' +import type { List } from '../effects/list/types.ts' import { mcpStep, uninitializedState, type McpSessionState, type ToolsCallResult, } from '../protocol/mcp/module.f.ts' diff --git a/fjs/media/type/module.f.ts b/fjs/media/type/module.f.ts index 193bdafcfa..2604033c99 100644 --- a/fjs/media/type/module.f.ts +++ b/fjs/media/type/module.f.ts @@ -36,8 +36,9 @@ import type { Vec } from '../../types/bit_vec/types.ts' import { msb, fromSentinel, length, u8List } from '../../types/bit_vec/module.f.mjs' import { iterable } from '../../types/list/module.f.mjs' import type { Nullable } from '../../types/nullable/types.ts' -import { pure, step, type Effect, type Operation } from '../../effects/module.f.ts' -import type { List } from '../../effects/list/module.f.ts' +import { pure, step } from '../../effects/module.f.mjs' +import type { Effect, Operation } from '../../effects/types.ts' +import type { List } from '../../effects/list/types.ts' import type { IoResult } from '../../effects/node/module.f.ts' import { ok, error } from '../../types/result/module.f.mjs' import { isValidCodePoint, isTextCodePoint } from '../../text/code_point/module.f.mjs' diff --git a/fjs/media/type/proof.f.ts b/fjs/media/type/proof.f.ts index e4d227eb0c..1ce779f1bb 100644 --- a/fjs/media/type/proof.f.ts +++ b/fjs/media/type/proof.f.ts @@ -1,8 +1,9 @@ import { assert, assertEq } from '../../asserts/module.f.mjs' import type { Vec } from '../../types/bit_vec/types.ts' import { msb, u8ListToVec, vec8, repeat, empty } from '../../types/bit_vec/module.f.mjs' -import { runPure } from '../../effects/module.f.ts' -import { nonEmpty, empty as emptyList, type List } from '../../effects/list/module.f.ts' +import { runPure } from '../../effects/module.f.mjs' +import { nonEmpty, empty as emptyList } from '../../effects/list/module.f.mjs' +import type { List } from '../../effects/list/types.ts' import type { Result } from '../../types/result/types.ts' import { ok } from '../../types/result/module.f.mjs' import { detect, detectStream, detectVec, type DetectMeta } from './module.f.ts' diff --git a/fjs/module.f.ts b/fjs/module.f.ts index 9b6ce0d2a4..51ca2e34ae 100644 --- a/fjs/module.f.ts +++ b/fjs/module.f.ts @@ -10,7 +10,7 @@ import { main as ciMain } from './ci/module.f.ts' import { import_, type NodeOp, type NodeProgram } from './effects/node/module.f.ts' import { dispatch, type Commands } from './cli/module.f.ts' import { casMcpServer } from './mcp/module.f.ts' -import { pure, step } from './effects/module.f.ts' +import { pure, step } from './effects/module.f.mjs' import { unwrap } from './types/result/module.f.mjs' const commands: Commands = [ diff --git a/fjs/proof.f.ts b/fjs/proof.f.ts index c76e195ad6..016f036e6b 100644 --- a/fjs/proof.f.ts +++ b/fjs/proof.f.ts @@ -1,5 +1,5 @@ import { assert, assertEq } from './asserts/module.f.mjs' -import { pure } from './effects/module.f.ts' +import { pure } from './effects/module.f.mjs' import type { NodeProgram, NodeProgramOptions } from './effects/node/module.f.ts' import { defaultNodeProgramOptions, emptyState, virtual, type Dir } from './effects/node/virtual/module.f.ts' import { main } from './module.f.ts' diff --git a/fjs/protocol/mcp/module.f.ts b/fjs/protocol/mcp/module.f.ts index ca0d83d9be..60f433f441 100644 --- a/fjs/protocol/mcp/module.f.ts +++ b/fjs/protocol/mcp/module.f.ts @@ -16,7 +16,8 @@ import { boolean, string, option, array, record, or } from '../../types/rtti/module.f.mjs' import { unknown, type Unknown } from '../../media/json/module.f.ts' import type { Ts } from '../../types/rtti/ts/types.ts' -import { pure, type Operation, type Effect, step } from '../../effects/module.f.ts' +import { pure, step } from '../../effects/module.f.mjs' +import type { Operation, Effect } from '../../effects/types.ts' import { read, write, type Key, type MemOp } from '../../effects/memory/module.f.ts' import { decodeRequest, diff --git a/fjs/protocol/mcp/proof.f.ts b/fjs/protocol/mcp/proof.f.ts index dac6a344f1..c1295c752a 100644 --- a/fjs/protocol/mcp/proof.f.ts +++ b/fjs/protocol/mcp/proof.f.ts @@ -1,7 +1,7 @@ import { assert, assertEq } from '../../asserts/module.f.mjs' -import { pure, step, type Operation } from '../../effects/module.f.ts' +import { pure, step } from '../../effects/module.f.mjs' import { eff } from '../../effects/eff/module.f.ts' -import type { Effect } from '../../effects/module.f.ts' +import type { Effect, Operation } from '../../effects/types.ts' import { run, type MemOperationMap } from '../../effects/mock/module.f.ts' import { asBase, asNominal, create, read, type Key, type MemOp } from '../../effects/memory/module.f.ts' import type { Unknown } from '../../media/json/module.f.ts' diff --git a/fjs/protocol/mcp/stdio/module.f.ts b/fjs/protocol/mcp/stdio/module.f.ts index 3b347e23b2..6f74a885f3 100644 --- a/fjs/protocol/mcp/stdio/module.f.ts +++ b/fjs/protocol/mcp/stdio/module.f.ts @@ -29,7 +29,8 @@ * * @module */ -import { pure, step, type Effect, type Operation } from '../../../effects/module.f.ts' +import { pure, step } from '../../../effects/module.f.mjs' +import type { Effect, Operation } from '../../../effects/types.ts' import { readLine, write, type IoResult, type Read, type Write } from '../../../effects/node/module.f.ts' import { tryUtf8 } from '../../../text/module.f.mjs' import { parse, stringify, type Unknown } from '../../../media/json/module.f.ts' diff --git a/fjs/protocol/mcp/stdio/proof.f.ts b/fjs/protocol/mcp/stdio/proof.f.ts index af97df6c97..ea7f1d8700 100644 --- a/fjs/protocol/mcp/stdio/proof.f.ts +++ b/fjs/protocol/mcp/stdio/proof.f.ts @@ -1,5 +1,6 @@ import { assertEq } from '../../../asserts/module.f.mjs' -import { pure, type Effect } from '../../../effects/module.f.ts' +import { pure } from '../../../effects/module.f.mjs' +import type { Effect } from '../../../effects/types.ts' import { emptyState, virtual, type State } from '../../../effects/node/virtual/module.f.ts' import type { Unknown } from '../../../media/json/module.f.ts' import { stringify } from '../../../media/json/module.f.ts' diff --git a/fjs/text/sgr/module.f.ts b/fjs/text/sgr/module.f.ts index 8c14e23fac..31d5c30671 100644 --- a/fjs/text/sgr/module.f.ts +++ b/fjs/text/sgr/module.f.ts @@ -9,7 +9,7 @@ // https://en.wikipedia.org/wiki/ANSI_escape_code#C0_control_codes import { write, type Write, type WriteConsoles, type NodeProgramOptions } from '../../effects/node/module.f.ts' -import { type Effect } from '../../effects/module.f.ts' +import type { Effect } from '../../effects/types.ts' import { utf8 } from "../module.f.mjs" export const backspace: string = '\x08' diff --git a/fjs/website/module.f.ts b/fjs/website/module.f.ts index 02e1453b1b..af3436e6f0 100644 --- a/fjs/website/module.f.ts +++ b/fjs/website/module.f.ts @@ -5,7 +5,8 @@ */ import { htmlUtf8 } from '../media/html/module.f.mjs' import { writeFile, type WriteFile } from '../effects/node/module.f.ts' -import { pure, step, type Effect } from '../effects/module.f.ts' +import { pure, step } from '../effects/module.f.mjs' +import type { Effect } from '../effects/types.ts' import type { Vec } from '../types/bit_vec/types.ts' const html: Vec = htmlUtf8()(