diff --git a/AGENTS.md b/AGENTS.md index 9fe210800..6e39d7390 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -625,7 +625,7 @@ whenever that type changes. #### `StringMap` / `RequiredMap` / `OptionalMap` for string-keyed records -Use the record types from `fjs/types/object/module.f.ts` for all string-keyed +Use the record types from `fjs/types/object/types.ts` for all string-keyed record types. The key set picks the type: - **Open key set:** `StringMap` is `{ readonly[k in string]?: T }` — any @@ -653,7 +653,7 @@ a property of aliasing, not of any one definition — writing the record as a mapped type rather than a conditional one does not lift it. When iterating all defined entries of a `StringMap`, use `definedEntries` -from `fjs/types/object/module.f.ts` instead of `Object.entries`; use +from `fjs/types/object/module.f.mjs` instead of `Object.entries`; use `definedValues` instead of `Object.values`. #### `flatMap` over a filtering type predicate diff --git a/CHANGELOG.md b/CHANGELOG.md index 69bea2846..ec468701a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,19 @@ history. ## Unreleased +- **BREAKING CHANGES:** `fjs/types/object`, `fjs/sul/level/literal`, + `fjs/sul/id`, `fjs/sul/level/hash`, `fjs/types/string_set`, and the + `module.f.ts` of `fjs/types/sorted_set` and `fjs/types/byte_set` + migrate to `.f.mjs`, splitting their public types into a sibling + `types.ts` — importers must use the `.f.mjs` specifier for runtime + values and the `types.ts` specifier for types + [#1484](https://github.com/functionalscript/functionalscript/pull/1484) +- **BREAKING CHANGES:** `fjs/crypto/hmac`, `fjs/path`, and + `fjs/media/json/serializer` migrate from authored TypeScript + (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`) under the stage-1 + TypeScript-to-mjs migration — importers must use the `.f.mjs` + specifier + [#1484](https://github.com/functionalscript/functionalscript/pull/1484) - **BREAKING CHANGES:** every public type exported by an authored `.f.mjs` module moves to a sibling `types.ts` (31 modules, including `asserts`, `types/list`, `types/bit_vec`, `types/result`, `text`, `crypto/sha2`) — diff --git a/fjs/bnf/data/module.f.ts b/fjs/bnf/data/module.f.ts index 773562e06..c7693813d 100644 --- a/fjs/bnf/data/module.f.ts +++ b/fjs/bnf/data/module.f.ts @@ -18,7 +18,8 @@ import { type Rule as FRule, type Sequence as FSequence, } from '../module.f.ts' -import { definedEntries, type StringMap } from '../../types/object/module.f.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. diff --git a/fjs/bnf/data/proof.f.ts b/fjs/bnf/data/proof.f.ts index 0d36604d9..78e1d0f28 100644 --- a/fjs/bnf/data/proof.f.ts +++ b/fjs/bnf/data/proof.f.ts @@ -1,6 +1,6 @@ import { stringify } from '../../media/json/module.f.ts' import { identity } from '../../types/function/module.f.mjs' -import { sort } from '../../types/object/module.f.ts' +import { sort } from '../../types/object/module.f.mjs' import { oneEncode, option, range, rangeDecode, repeat0Plus, set } from '../module.f.ts' import { classic, deterministic } from '../testlib.f.ts' import { emptyTagMap, type RuleSet, toData } from './module.f.ts' diff --git a/fjs/bnf/descent/module.f.ts b/fjs/bnf/descent/module.f.ts index 85d38a2e1..c0c770aee 100644 --- a/fjs/bnf/descent/module.f.ts +++ b/fjs/bnf/descent/module.f.ts @@ -16,7 +16,7 @@ import type { CodePoint } from '../../text/utf16/types.ts' import { rangeDecode, type TerminalRange } from '../module.f.ts' import { contains as rangeContains } from '../../types/range/module.f.mjs' -import { definedEntries } from '../../types/object/module.f.ts' +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 '../module.f.ts' diff --git a/fjs/bnf/ll1/module.f.ts b/fjs/bnf/ll1/module.f.ts index f2436d759..85b653374 100644 --- a/fjs/bnf/ll1/module.f.ts +++ b/fjs/bnf/ll1/module.f.ts @@ -15,9 +15,11 @@ 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 { contains, set, type StringSet } from '../../types/string_set/module.f.ts' +import { contains, set } from '../../types/string_set/module.f.mjs' +import type { StringSet } from '../../types/string_set/types.ts' import { rangeDecode } from '../module.f.ts' -import { definedEntries, type StringMap } from '../../types/object/module.f.ts' +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 '../module.f.ts' diff --git a/fjs/bnf/module.f.ts b/fjs/bnf/module.f.ts index a80d2489a..b45296f77 100644 --- a/fjs/bnf/module.f.ts +++ b/fjs/bnf/module.f.ts @@ -10,7 +10,8 @@ import type { Tuple } from '../types/array/types.ts' import { codePointListToString, stringToCodePointList } from '../text/utf16/module.f.mjs' -import { definedValues, type StringMap } from '../types/object/module.f.ts' +import { definedValues } from '../types/object/module.f.mjs' +import type { StringMap } from '../types/object/types.ts' import { isTuple } from '../types/array/module.f.mjs' import { map, toArray, repeat as listRepeat } from '../types/list/module.f.mjs' import { contains } from '../types/range/module.f.mjs' diff --git a/fjs/cas/evo/module.f.ts b/fjs/cas/evo/module.f.ts index e80d5b83f..3b39fe796 100644 --- a/fjs/cas/evo/module.f.ts +++ b/fjs/cas/evo/module.f.ts @@ -53,7 +53,8 @@ import { decodeText, encodeText, dialect, checkReferences, isHash, type LockMap, 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 { at, definedEntries, type StringMap } from '../../types/object/module.f.ts' +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' import type { Vec } from '../../types/bit_vec/types.ts' import { isNotFound, type IoResult } from '../../effects/node/module.f.ts' diff --git a/fjs/cas/module.f.ts b/fjs/cas/module.f.ts index 11433ea51..81b7b3417 100644 --- a/fjs/cas/module.f.ts +++ b/fjs/cas/module.f.ts @@ -5,7 +5,7 @@ */ import type { Sha2, State as Sha2State } from '../crypto/sha2/types.ts' import { sha256 } from '../crypto/sha2/module.f.mjs' -import { join, normalize, parse } from '../path/module.f.ts' +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' diff --git a/fjs/cas/proof.f.ts b/fjs/cas/proof.f.ts index 026282639..e287c45ff 100644 --- a/fjs/cas/proof.f.ts +++ b/fjs/cas/proof.f.ts @@ -8,7 +8,7 @@ import { mkdir, writeFile, rm, readFile, type ReadFile, type WriteFile, type Rm, 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.ts' +import { join } from '../path/module.f.mjs' import { nonEmpty, empty, type List } from '../effects/list/module.f.ts' import { assert, assertEq, assertNotNullish } from '../asserts/module.f.mjs' diff --git a/fjs/cas/todo/shard-dir-helper.md b/fjs/cas/todo/shard-dir-helper.md index 568cec2bf..a61238c9a 100644 --- a/fjs/cas/todo/shard-dir-helper.md +++ b/fjs/cas/todo/shard-dir-helper.md @@ -23,7 +23,7 @@ how a key becomes segments, and `publish` separately assumes "the last segment is the file name". A layout change (e.g. a different shard depth) would have to be caught in both places, and the `mkdir(dstDir, { recursive: true })` that follows depends on the fragile re-parse. -`fjs/path/module.f.ts` also has no `dirname`/`parent` helper, so the +`fjs/path/module.f.mjs` also has no `dirname`/`parent` helper, so the open-coded `parse(x).slice(0, -1)` is the only way to express this today. ### Proposal @@ -49,7 +49,7 @@ export const toPath = (key: Vec): string => { `publish` then uses `const { dir } = shard(hash)` and `mkdir(join(path, dir), { recursive: true })` — no re-parse, no `slice`. -Alternative considered: a general `dirname` in `fjs/path/module.f.ts`; the +Alternative considered: a general `dirname` in `fjs/path/module.f.mjs`; the `shard` form is preferred because the CAS write path should not know how a shard path decomposes, and no second `dirname` consumer exists yet. diff --git a/fjs/ci/proof.f.ts b/fjs/ci/proof.f.ts index 852bf8b19..2918b26ca 100644 --- a/fjs/ci/proof.f.ts +++ b/fjs/ci/proof.f.ts @@ -10,7 +10,7 @@ import type { State } from '../effects/node/virtual/module.f.ts' import { emptyState, virtual, type Dir } from '../effects/node/virtual/module.f.ts' import { parse as jsonParse } from '../media/json/module.f.ts' import { unwrap } from '../types/result/module.f.mjs' -import { definedValues } from '../types/object/module.f.ts' +import { definedValues } from '../types/object/module.f.mjs' const hasRun = (cmd: string) => (gha: GitHubAction): boolean => definedValues(gha.jobs).some(job => job.steps.some(step => step.run?.includes(cmd))) diff --git a/fjs/cli/module.f.ts b/fjs/cli/module.f.ts index 70d5e73e7..89135e3c0 100644 --- a/fjs/cli/module.f.ts +++ b/fjs/cli/module.f.ts @@ -1,6 +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 { at, fromEntries } from '../types/object/module.f.ts' +import { at, fromEntries } from '../types/object/module.f.mjs' type Handler = (options: NodeProgramOptions) => Effect diff --git a/fjs/crypto/hmac/module.f.ts b/fjs/crypto/hmac/module.f.mjs similarity index 74% rename from fjs/crypto/hmac/module.f.ts rename to fjs/crypto/hmac/module.f.mjs index dc2e51cf3..5fa9462e8 100644 --- a/fjs/crypto/hmac/module.f.ts +++ b/fjs/crypto/hmac/module.f.mjs @@ -9,7 +9,7 @@ * @example * * ```ts - * import { vec } from '../../types/bit_vec/module.f.ts' + * import { vec } from '../../types/bit_vec/module.f.mjs' * import { msbUtf8 } from '../../text/module.f.mjs' * import { sha256 } from '../sha2/module.f.mjs' * @@ -18,9 +18,9 @@ * ``` */ -import type { Vec, Reduce } from '../../types/bit_vec/types.ts' +/** @import { Vec, Reduce } from '../../types/bit_vec/types.ts' */ import { length, msb, vec, vec8, repeat } from '../../types/bit_vec/module.f.mjs' -import type { Sha2 } from '../sha2/types.ts' +/** @import { Sha2 } from '../sha2/types.ts' */ import { computeSync } from '../sha2/module.f.mjs' const { concat, xor } = msb @@ -38,11 +38,11 @@ const iPad = vec8(0x36n) /** * Generates an HMAC (Hash-based Message Authentication Code) using the specified hash function. * - * @param hashFunc - The hash function implementation to use. - * @returns - A function that takes a key and returns another function + * @param {Sha2} hashFunc - The hash function implementation to use. + * @returns {Reduce} A function that takes a key and returns another function * that takes a message and computes the HMAC. */ -export const hmac = (hashFunc: Sha2): Reduce => { +export const hmac = hashFunc => { const { blockLength } = hashFunc const p = repeat(blockLength >> 3n) const ip = p(iPad) @@ -52,9 +52,10 @@ export const hmac = (hashFunc: Sha2): Reduce => { const k1 = length(k) > blockLength ? c([k]) : k const k2 = concat(k1)(vec(blockLength - length(k1))(0n)) const xk2 = xor(k2) - const f = (p: Vec) => { + /** @type {(p: Vec) => (msg: Vec) => Vec} */ + const f = p => { const x = xk2(p) - return (msg: Vec) => c([x, msg]) + return msg => c([x, msg]) } const fip = f(ip) const fop = f(op) diff --git a/fjs/crypto/hmac/proof.f.ts b/fjs/crypto/hmac/proof.f.mjs similarity index 98% rename from fjs/crypto/hmac/proof.f.ts rename to fjs/crypto/hmac/proof.f.mjs index 93d7d3d9c..edd7d8e54 100644 --- a/fjs/crypto/hmac/proof.f.ts +++ b/fjs/crypto/hmac/proof.f.mjs @@ -2,7 +2,7 @@ import { assertEq } from '../../asserts/module.f.mjs' import { utf8 } from '../../text/module.f.mjs' import { uint, vec } from '../../types/bit_vec/module.f.mjs' import { sha256, sha384, sha512 } from '../sha2/module.f.mjs' -import { hmac } from './module.f.ts' +import { hmac } from './module.f.mjs' export const proof = { example: () => { diff --git a/fjs/crypto/sign/module.f.ts b/fjs/crypto/sign/module.f.ts index 4abb1e064..304145e74 100644 --- a/fjs/crypto/sign/module.f.ts +++ b/fjs/crypto/sign/module.f.ts @@ -8,7 +8,7 @@ import type { Tuple } from '../../types/array/types.ts' import { bitLength, divUp8, roundUp8 } from '../../types/bigint/module.f.mjs' import type { Vec } from '../../types/bit_vec/types.ts' import { empty, length, msb, repeat, unpack, vec, vec8 } from '../../types/bit_vec/module.f.mjs' -import { hmac } from '../hmac/module.f.ts' +import { hmac } from '../hmac/module.f.mjs' import type { Curve } from '../secp/types.ts' import type { Sha2 } from '../sha2/types.ts' import { computeSync } from '../sha2/module.f.mjs' diff --git a/fjs/crypto/sign/proof.f.ts b/fjs/crypto/sign/proof.f.ts index ba26f5c1e..a54d0eed1 100644 --- a/fjs/crypto/sign/proof.f.ts +++ b/fjs/crypto/sign/proof.f.ts @@ -2,7 +2,7 @@ import { utf8 } from "../../text/module.f.mjs" import type { Tuple } from "../../types/array/types.ts" import type { Vec } from "../../types/bit_vec/types.ts" import { empty, msb, repeat, vec, vec8 } from "../../types/bit_vec/module.f.mjs" -import { hmac } from "../hmac/module.f.ts" +import { hmac } from "../hmac/module.f.mjs" import type { Curve } from "../secp/types.ts" import { secp192r1, secp256r1, secp384r1, secp521r1 } from "../secp/module.f.mjs" import type { Sha2 } from "../sha2/types.ts" diff --git a/fjs/dev/module.f.ts b/fjs/dev/module.f.ts index 80ea5265b..49dc1f87d 100644 --- a/fjs/dev/module.f.ts +++ b/fjs/dev/module.f.ts @@ -14,10 +14,10 @@ import { type Readdir } from '../effects/node/module.f.ts' import { cmp as strCmp } from '../types/string/module.f.mjs' -import type { StringMap } from '../types/object/module.f.ts' +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 { join, relativize, toPosix } from '../path/module.f.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/todo/dev-allfiles-flatmap.md b/fjs/dev/todo/dev-allfiles-flatmap.md index 686f39cff..8c77715a3 100644 --- a/fjs/dev/todo/dev-allfiles-flatmap.md +++ b/fjs/dev/todo/dev-allfiles-flatmap.md @@ -32,7 +32,7 @@ zero or one effect, which is exactly the `flatMap` shape. The repeated `result = [...result, x]` is also O(n²) in entry count. Secondary: line 82 destructures `const { fromEntries } = Object` while -`AGENTS.md` mandates the typed helpers from `fjs/types/object/module.f.ts` +`AGENTS.md` mandates the typed helpers from `fjs/types/object/module.f.mjs` for string-keyed maps (`fjs/cli` already imports from there). ### Proposal @@ -50,14 +50,14 @@ readdir(p, {}).step(d => all(...unwrap(d).flatMap(i => { No `let`, no `continue`, no accumulator reassignment; the 0-or-1 mapping per entry is explicit. Also replace the `Object.fromEntries` destructuring with -the appropriate helper from `fjs/types/object/module.f.ts` (or file a +the appropriate helper from `fjs/types/object/module.f.mjs` (or file a follow-up if no typed `fromEntries` exists there yet). ### Tasks - [ ] Rewrite the `load` body with `flatMap` as above. - [ ] Replace `const { fromEntries } = Object` with the typed equivalent - from `fjs/types/object/module.f.ts`, adding one there if missing (it + from `fjs/types/object/module.f.mjs`, adding one there if missing (it has consumers waiting: this module and future map-builders). - [ ] Run `npx tsc` and `fjs t`. diff --git a/fjs/djs/ast/module.f.ts b/fjs/djs/ast/module.f.ts index 1c4b26e06..d54904822 100644 --- a/fjs/djs/ast/module.f.ts +++ b/fjs/djs/ast/module.f.ts @@ -7,7 +7,7 @@ import type { Primitive, Array, Unknown } from '../module.f.ts' import type { List } from '../../types/list/types.ts' import { concat, fold, last, map, take, toArray } from '../../types/list/module.f.mjs' import type { Entry } from '../../types/ordered_map/types.ts' -import { fromEntries } from '../../types/object/module.f.ts' +import { fromEntries } from '../../types/object/module.f.mjs' const { entries } = Object diff --git a/fjs/djs/ast/proof.f.ts b/fjs/djs/ast/proof.f.ts index 0fea209c8..90835fc8b 100644 --- a/fjs/djs/ast/proof.f.ts +++ b/fjs/djs/ast/proof.f.ts @@ -1,4 +1,4 @@ -import { sort } from '../../types/object/module.f.ts' +import { sort } from '../../types/object/module.f.mjs' import { run } from './module.f.ts' import { stringifyAsTree } from '../serializer/module.f.ts' import { assertEq } from '../../asserts/module.f.mjs' diff --git a/fjs/djs/module.f.ts b/fjs/djs/module.f.ts index b86933cb9..366d1c989 100644 --- a/fjs/djs/module.f.ts +++ b/fjs/djs/module.f.ts @@ -6,7 +6,7 @@ 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.ts' +import { sort } from '../types/object/module.f.mjs' import { type Effect, pure, step } from '../effects/module.f.ts' import { writeUtf8File, diff --git a/fjs/djs/parser/module.f.ts b/fjs/djs/parser/module.f.ts index 08d8a09a7..c49725126 100644 --- a/fjs/djs/parser/module.f.ts +++ b/fjs/djs/parser/module.f.ts @@ -11,7 +11,7 @@ import type { Fold } from '../../types/function/operator/types.ts' import type { DjsToken, DjsTokenWithMetadata } from '../tokenizer/module.f.ts' import { setReplace, at } from '../../types/ordered_map/module.f.mjs' import type { OrderedMap } from '../../types/ordered_map/types.ts' -import { fromMap } from '../../types/object/module.f.ts' +import { fromMap } from '../../types/object/module.f.mjs' import type { AstArray, AstConst, AstModule, AstModuleRef } from '../ast/module.f.ts' import type { TokenMetadata } from '../../js/tokenizer/module.f.ts' import { assertEq } from '../../asserts/module.f.mjs' diff --git a/fjs/djs/parser/proof.f.ts b/fjs/djs/parser/proof.f.ts index 497271eaa..0203a455e 100644 --- a/fjs/djs/parser/proof.f.ts +++ b/fjs/djs/parser/proof.f.ts @@ -1,7 +1,7 @@ import { parseFromTokens } from './module.f.ts' import { tokenize, type DjsTokenWithMetadata } from '../tokenizer/module.f.ts' import { toArray } from '../../types/list/module.f.mjs' -import { sort } from '../../types/object/module.f.ts' +import { sort } from '../../types/object/module.f.mjs' import { stringToList } from '../../text/utf16/module.f.mjs' import { stringifyAsTree } from '../serializer/module.f.ts' import { stringify } from '../../media/json/module.f.ts' diff --git a/fjs/djs/serializer/module.f.ts b/fjs/djs/serializer/module.f.ts index 4ff61cbfd..5243c93a2 100644 --- a/fjs/djs/serializer/module.f.ts +++ b/fjs/djs/serializer/module.f.ts @@ -5,7 +5,7 @@ */ import type { Unknown, Object } from '../module.f.ts' import type { Fold } from '../../types/function/operator/types.ts' -import type { Entry as ObjectEntry } from '../../types/object/module.f.ts' +import type { Entry as ObjectEntry } from '../../types/object/types.ts' import { fold } from '../../types/list/module.f.mjs' import { concat } from '../../types/string/module.f.mjs' import type { List } from '../../types/list/types.ts' @@ -13,7 +13,7 @@ import { flat, flatMap, map, concat as listConcat } from '../../types/list/modul const { entries } = Object import { compose, fn } from '../../types/function/module.f.mjs' import { serialize as bigintSerialize } from '../../types/bigint/module.f.mjs' -import { objectWrap, arrayWrap, stringSerialize, numberSerialize, nullSerialize, boolSerialize } from '../../media/json/serializer/module.f.ts' +import { objectWrap, arrayWrap, stringSerialize, numberSerialize, nullSerialize, boolSerialize } from '../../media/json/serializer/module.f.mjs' const colon = [':'] diff --git a/fjs/djs/serializer/proof.f.ts b/fjs/djs/serializer/proof.f.ts index 6aef0b743..d56b1154e 100644 --- a/fjs/djs/serializer/proof.f.ts +++ b/fjs/djs/serializer/proof.f.ts @@ -1,5 +1,5 @@ import { countRefs, stringify, stringifyAsTree } from './module.f.ts' -import { sort } from '../../types/object/module.f.ts' +import { sort } from '../../types/object/module.f.mjs' import { identity } from '../../types/function/module.f.mjs' import { setProperty } from '../../media/json/module.f.ts' import { assertEq } from '../../asserts/module.f.mjs' diff --git a/fjs/djs/todo/157.md b/fjs/djs/todo/157.md index f193340c0..05a309b5f 100644 --- a/fjs/djs/todo/157.md +++ b/fjs/djs/todo/157.md @@ -107,7 +107,7 @@ Each defines the identical closure cluster: `propertySerialize` the recursive `f` switching on `typeof`, and `arraySerialize = compose(map(f))(arrayWrap)`. The serializer already imports its primitives (`stringSerialize`, `objectWrap`, `arrayWrap`, …) from -`fjs/media/json/serializer/module.f.ts`, so the leaves are shared; only the walker was +`fjs/media/json/serializer/module.f.mjs`, so the leaves are shared; only the walker was copied. The deltas: diff --git a/fjs/djs/todo/663-json-djs-tree-type.md b/fjs/djs/todo/663-json-djs-tree-type.md index 84d41689a..02634e2d9 100644 --- a/fjs/djs/todo/663-json-djs-tree-type.md +++ b/fjs/djs/todo/663-json-djs-tree-type.md @@ -111,5 +111,5 @@ serialization behavior. - [197](./197.md) — extracts traversal over the same `Unknown` shape. - `fjs/media/json/module.f.ts` — current JSON recursive type aliases. - `fjs/djs/module.f.ts` — current DJS recursive type aliases. -- `fjs/media/json/serializer/module.f.ts` — currently has no separate recursive +- `fjs/media/json/serializer/module.f.mjs` — currently has no separate recursive generic value aliases and therefore is not part of this migration. diff --git a/fjs/djs/tokenizer/module.f.ts b/fjs/djs/tokenizer/module.f.ts index 35bd44bee..f85912534 100644 --- a/fjs/djs/tokenizer/module.f.ts +++ b/fjs/djs/tokenizer/module.f.ts @@ -59,7 +59,7 @@ import { contains } from "../../types/range/module.f.mjs" import type { List } from "../../types/list/types.ts" import { concat, empty, filter, flat, flatMap, fold, map, stateScan, toArray } from "../../types/list/module.f.mjs" import { stringifyAsTree } from "../serializer/module.f.ts" -import { sort } from "../../types/object/module.f.ts" +import { sort } from "../../types/object/module.f.mjs" import type { Unknown } from "../module.f.ts" // Builds the single-token grammar that jsGrammar's whole-file `tokens` rule repeats. diff --git a/fjs/djs/tokenizer/proof.f.ts b/fjs/djs/tokenizer/proof.f.ts index f3cee4b54..73d938f2a 100644 --- a/fjs/djs/tokenizer/proof.f.ts +++ b/fjs/djs/tokenizer/proof.f.ts @@ -4,7 +4,7 @@ import { toArray } from '../../types/list/module.f.mjs' import { jsGrammar, tokenizeString, descentParserCpOnly, tokenizeJs, tokenize } from './module.f.ts' import { assert, assertEq } from '../../asserts/module.f.mjs' import { stringifyAsTree } from '../serializer/module.f.ts' -import { sort } from '../../types/object/module.f.ts' +import { sort } from '../../types/object/module.f.mjs' import type { Unknown } from '../module.f.ts' // DjsTokenWithMetadata carries bigint fields (bf, bigint value) that JSON.stringify can't diff --git a/fjs/djs/transpiler/module.f.ts b/fjs/djs/transpiler/module.f.ts index d3fc00cd1..43572e2bf 100644 --- a/fjs/djs/transpiler/module.f.ts +++ b/fjs/djs/transpiler/module.f.ts @@ -12,7 +12,7 @@ import { tokenize } from '../tokenizer/module.f.ts' import { setReplace, at } from '../../types/ordered_map/module.f.mjs' import type { OrderedMap } from '../../types/ordered_map/types.ts' import { stringToList } from '../../text/utf16/module.f.mjs' -import { concat as pathConcat } from '../../path/module.f.ts' +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' diff --git a/fjs/djs/transpiler/proof.f.ts b/fjs/djs/transpiler/proof.f.ts index c4a7db914..a23269358 100644 --- a/fjs/djs/transpiler/proof.f.ts +++ b/fjs/djs/transpiler/proof.f.ts @@ -1,4 +1,4 @@ -import { sort } from '../../types/object/module.f.ts' +import { sort } from '../../types/object/module.f.mjs' import { transpile } from './module.f.ts' import { stringifyAsTree } from '../serializer/module.f.ts' import { virtual, emptyState, type Dir } from '../../effects/node/virtual/module.f.ts' diff --git a/fjs/effects/module.f.ts b/fjs/effects/module.f.ts index 5ddece098..248f2c35b 100644 --- a/fjs/effects/module.f.ts +++ b/fjs/effects/module.f.ts @@ -98,7 +98,7 @@ import { assert } from '../asserts/module.f.mjs' import type { List } from '../types/list/types.ts' import { fold } from '../types/list/module.f.mjs' -import { at } from '../types/object/module.f.ts' +import { at } from '../types/object/module.f.mjs' import type { Option } from '../types/option/types.ts' import type { Result } from '../types/result/types.ts' diff --git a/fjs/effects/node/module.f.ts b/fjs/effects/node/module.f.ts index cbf3e280a..1bd06842c 100644 --- a/fjs/effects/node/module.f.ts +++ b/fjs/effects/node/module.f.ts @@ -19,7 +19,7 @@ import type { MemOp } from '../memory/module.f.ts' 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/module.f.ts' +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' diff --git a/fjs/effects/node/module.ts b/fjs/effects/node/module.ts index 7d7512688..ea9da3926 100644 --- a/fjs/effects/node/module.ts +++ b/fjs/effects/node/module.ts @@ -21,7 +21,7 @@ import process from 'node:process' import { once } from 'node:events' import * as testContext from 'node:test' -import { concat, normalize, toPosix } from '../../path/module.f.ts' +import { concat, normalize, toPosix } from '../../path/module.f.mjs' import { type Effect } from '../module.f.ts' import { asyncRun } from '../module.ts' import { memoryOperationMap } from './memory/module.ts' @@ -43,7 +43,7 @@ import type { Result } from '../../types/result/types.ts' import { error, ok } from '../../types/result/module.f.mjs' import { asyncTryCatch } from '../../types/result/module.ts' import { fromVec, listToVec, toVec } from '../../types/uint8array/module.f.mjs' -import type { StringMap } from '../../types/object/module.f.ts' +import type { StringMap } from '../../types/object/types.ts' import { maxLengthBytes } from '../../types/bit_vec/module.f.mjs' type Server = { diff --git a/fjs/effects/node/virtual/module.f.ts b/fjs/effects/node/virtual/module.f.ts index 1e1b921a5..5bc512e81 100644 --- a/fjs/effects/node/virtual/module.f.ts +++ b/fjs/effects/node/virtual/module.f.ts @@ -4,7 +4,7 @@ * @module */ import { todo } from '../../../asserts/module.f.mjs' -import { isProperPrefix, join, parse } from '../../../path/module.f.ts' +import { isProperPrefix, join, parse } from '../../../path/module.f.mjs' import { utf8ToString } from '../../../text/module.f.mjs' import type { Vec } from '../../../types/bit_vec/types.ts' import { empty, length, maxLengthBytes, msb, vec } from '../../../types/bit_vec/module.f.mjs' diff --git a/fjs/effects/todo/node-module-layering.md b/fjs/effects/todo/node-module-layering.md index f161372dc..e417bb7d8 100644 --- a/fjs/effects/todo/node-module-layering.md +++ b/fjs/effects/todo/node-module-layering.md @@ -133,7 +133,7 @@ Judgement calls worth deciding explicitly rather than by accident: ```ts // fjs/effects/console/module.f.ts - import type { RequiredMap } from '../../types/object/module.f.ts' + import type { RequiredMap } from '../../types/object/types.ts' export type Std = RequiredMap diff --git a/fjs/emergent_testing/module.f.ts b/fjs/emergent_testing/module.f.ts index 59069c690..c736faeb8 100644 --- a/fjs/emergent_testing/module.f.ts +++ b/fjs/emergent_testing/module.f.ts @@ -33,7 +33,7 @@ import { history, historyStep, pure, step, type Effect, type Operation } from '. 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' -import { definedEntries } from '../types/object/module.f.ts' +import { definedEntries } from '../types/object/module.f.mjs' type TestState = { diff --git a/fjs/emergent_testing/todo/65y-proof-asserteq-adoption.md b/fjs/emergent_testing/todo/65y-proof-asserteq-adoption.md index 03a8f8b8e..a004cb853 100644 --- a/fjs/emergent_testing/todo/65y-proof-asserteq-adoption.md +++ b/fjs/emergent_testing/todo/65y-proof-asserteq-adoption.md @@ -28,8 +28,8 @@ Counts in the current tree: - ~1,623 `if (...) { throw ... }` lines across `fjs/**/proof.f.ts` — the dominant assertion style. - Only 4 files import `assertEq`: `fjs/sul/proof.f.ts`, - `fjs/sul/level/hash/proof.f.ts`, `fjs/sul/id/proof.f.ts`, - `fjs/sul/id/module.f.ts`. + `fjs/sul/level/hash/proof.f.mjs`, `fjs/sul/id/proof.f.mjs`, + `fjs/sul/id/module.f.mjs`. - ~40 of the manual sites throw bare string literals (`throw 'error'`, `throw 'Error'`) that carry no actual context — worse than `assertEq`'s `[a, b]` pair on failure. @@ -137,8 +137,8 @@ it's by far the most common and the lowest-judgement case. load → light, cheap checks only). `assertEq` is the helper that makes that tier ergonomic. - `fjs/dev/module.f.ts:36–39` — definitions of `assert` / `assertEq`. -- `fjs/sul/id/module.f.ts:19`, `fjs/sul/id/proof.f.ts:1`, - `fjs/sul/proof.f.ts:1`, `fjs/sul/level/hash/proof.f.ts:1` — the four +- `fjs/sul/id/module.f.mjs:19`, `fjs/sul/id/proof.f.mjs:1`, + `fjs/sul/proof.f.ts:1`, `fjs/sul/level/hash/proof.f.mjs:1` — the four existing consumers, demonstrating the desired call-site shape. - [i194](todo.md), [i65X-async-test-functions](./README.md) — parallel work on the test framework's effect surface. The helper diff --git a/fjs/fsc/todo/66c-emit-literals-via-owner-modules.md b/fjs/fsc/todo/66c-emit-literals-via-owner-modules.md index e7d426652..b56b42c18 100644 --- a/fjs/fsc/todo/66c-emit-literals-via-owner-modules.md +++ b/fjs/fsc/todo/66c-emit-literals-via-owner-modules.md @@ -55,7 +55,7 @@ string syntax, and `fjs/media/json/serializer` already concentrates it: it alias built-in privately and wraps it as `stringSerialize`. ```ts -// fjs/media/json/serializer/module.f.ts:28 +// fjs/media/json/serializer/module.f.mjs:28 const jsonStringify = JSON.stringify // :33 export const stringSerialize: (_: string) => List = input => [jsonStringify(input)] diff --git a/fjs/fsm/module.f.ts b/fjs/fsm/module.f.ts index 0541c795c..093bdcc40 100644 --- a/fjs/fsm/module.f.ts +++ b/fjs/fsm/module.f.ts @@ -5,9 +5,11 @@ */ import type { List } from '../types/list/types.ts' import { equal, isEmpty, fold, toArray, scan, foldScan, empty as emptyList } from '../types/list/module.f.mjs' -import type { StringMap } from '../types/object/module.f.ts' -import { toRangeMap, union as byteSetUnion, one, empty, range, type ByteSet } from '../types/byte_set/module.f.ts' -import { intersect, type SortedSet, union as sortedSetUnion } from '../types/sorted_set/module.f.ts' +import type { StringMap } from '../types/object/types.ts' +import { toRangeMap, union as byteSetUnion, one, empty, range } from '../types/byte_set/module.f.mjs' +import type { ByteSet } from '../types/byte_set/types.ts' +import { intersect, union as sortedSetUnion } from '../types/sorted_set/module.f.mjs' +import type { SortedSet } from '../types/sorted_set/types.ts' import type { RangeMap, Properties, RangeMapArray, Entry } from '../types/range_map/types.ts' import { merge, get as rangeMapGet } from '../types/range_map/module.f.mjs' import type { Fold, Scan } from '../types/function/operator/types.ts' diff --git a/fjs/fsm/proof.f.ts b/fjs/fsm/proof.f.ts index 6ebea5539..b132716c0 100644 --- a/fjs/fsm/proof.f.ts +++ b/fjs/fsm/proof.f.ts @@ -1,6 +1,6 @@ import { dfa, run, toRange, toUnion, type Grammar } from './module.f.ts' -import { union } from '../types/byte_set/module.f.ts' -import { sort, fromEntries } from '../types/object/module.f.ts' +import { union } from '../types/byte_set/module.f.mjs' +import { sort, fromEntries } from '../types/object/module.f.mjs' import { stringify } from '../media/json/module.f.ts' import { identity } from '../types/function/module.f.mjs' import { toArray } from '../types/list/module.f.mjs' diff --git a/fjs/js/tokenizer/proof.f.ts b/fjs/js/tokenizer/proof.f.ts index d48d12279..b00a2e99f 100644 --- a/fjs/js/tokenizer/proof.f.ts +++ b/fjs/js/tokenizer/proof.f.ts @@ -1,7 +1,7 @@ import { tokenize, type JsToken, type JsTokenWithMetadata } from './module.f.ts' import { map, toArray } from '../../types/list/module.f.mjs' import { stringifyAsTree } from '../../djs/serializer/module.f.ts' -import { sort } from '../../types/object/module.f.ts' +import { sort } from '../../types/object/module.f.mjs' import { stringToList } from '../../text/utf16/module.f.mjs' const tokenizeString diff --git a/fjs/media/html/module.f.ts b/fjs/media/html/module.f.ts index 42b6f6307..5ea31aee3 100644 --- a/fjs/media/html/module.f.ts +++ b/fjs/media/html/module.f.ts @@ -7,7 +7,8 @@ import type { List } from '../../types/list/types.ts' import { map, flatMap, flat, concat as listConcat } from '../../types/list/module.f.mjs' import { concat, concat as stringConcat } from '../../types/string/module.f.mjs' -import { definedEntries, type Entry, type StringMap } from '../../types/object/module.f.ts' +import { definedEntries } from '../../types/object/module.f.mjs' +import type { Entry, StringMap } from '../../types/object/types.ts' import { compose } from '../../types/function/module.f.mjs' import { stringToList } from '../../text/utf16/module.f.mjs' import { includes } from '../../types/array/module.f.mjs' diff --git a/fjs/media/json/module.f.ts b/fjs/media/json/module.f.ts index ea5adfcd9..227e87af8 100644 --- a/fjs/media/json/module.f.ts +++ b/fjs/media/json/module.f.ts @@ -18,9 +18,10 @@ import { stringToList } from '../../text/utf16/module.f.mjs' import type { Result } from '../../types/result/types.ts' import { parse as parseTokens } from './parser/module.f.ts' import { tokenize } from './tokenizer/module.f.ts' -import { at, definedEntries, type Entry as ObjectEntry } from '../../types/object/module.f.ts' +import { at, definedEntries } from '../../types/object/module.f.mjs' +import type { Entry as ObjectEntry } from '../../types/object/types.ts' import { compose, fn } from '../../types/function/module.f.mjs' -import { objectWrap, arrayWrap, stringSerialize, numberSerialize, nullSerialize, boolSerialize } from './serializer/module.f.ts' +import { objectWrap, arrayWrap, stringSerialize, numberSerialize, nullSerialize, boolSerialize } from './serializer/module.f.mjs' import { boolean as rttiBoolean, number as rttiNumber, string as rttiString, or, record, array as rttiArray } from '../../types/rtti/module.f.ts' import type { Ts } from '../../types/rtti/ts/module.f.ts' import type { Assert } from '../../asserts/types.ts' diff --git a/fjs/media/json/parser/module.f.ts b/fjs/media/json/parser/module.f.ts index 0a9f238fd..c260a95ac 100644 --- a/fjs/media/json/parser/module.f.ts +++ b/fjs/media/json/parser/module.f.ts @@ -12,7 +12,7 @@ import { type JsonToken } from '../tokenizer/module.f.ts' import { setReplace } from '../../../types/ordered_map/module.f.mjs' import type { OrderedMap } from '../../../types/ordered_map/types.ts' import { type Unknown } from '../module.f.ts' -import { fromMap } from '../../../types/object/module.f.ts' +import { fromMap } from '../../../types/object/module.f.mjs' import { assertEq } from '../../../asserts/module.f.mjs' type JsonObject = { diff --git a/fjs/media/json/parser/proof.f.ts b/fjs/media/json/parser/proof.f.ts index 54c4c171c..8a7dbb470 100644 --- a/fjs/media/json/parser/proof.f.ts +++ b/fjs/media/json/parser/proof.f.ts @@ -2,7 +2,7 @@ import { parse } from './module.f.ts' import { tokenize, type JsonToken } from '../tokenizer/module.f.ts' import { toArray } from '../../../types/list/module.f.mjs' import { stringify as jsonStringify } from '../module.f.ts' -import { sort } from '../../../types/object/module.f.ts' +import { sort } from '../../../types/object/module.f.mjs' import { stringToList } from '../../../text/utf16/module.f.mjs' import { assertEq } from '../../../asserts/module.f.mjs' diff --git a/fjs/media/json/proof.f.ts b/fjs/media/json/proof.f.ts index b541bcad7..21336e1b9 100644 --- a/fjs/media/json/proof.f.ts +++ b/fjs/media/json/proof.f.ts @@ -1,5 +1,5 @@ import { parse, setProperty, stringify } from './module.f.ts' -import { sort } from '../../types/object/module.f.ts' +import { sort } from '../../types/object/module.f.mjs' import { identity } from '../../types/function/module.f.mjs' import { assertEq } from '../../asserts/module.f.mjs' diff --git a/fjs/media/json/serializer/module.f.ts b/fjs/media/json/serializer/module.f.mjs similarity index 74% rename from fjs/media/json/serializer/module.f.ts rename to fjs/media/json/serializer/module.f.mjs index c87ae0659..a9b4e102a 100644 --- a/fjs/media/json/serializer/module.f.ts +++ b/fjs/media/json/serializer/module.f.mjs @@ -7,11 +7,11 @@ * * @module */ -import type { List } from '../../../types/list/types.ts' +/** @import { List } from '../../../types/list/types.ts' */ import { flat, map, reduce, empty } from '../../../types/list/module.f.mjs' -import type { Reduce } from '../../../types/function/operator/types.ts' +/** @import { Reduce } from '../../../types/function/operator/types.ts' */ import { concat } from '../../../types/string/module.f.mjs' -import type { CodePoint } from '../../../text/utf16/types.ts' +/** @import { CodePoint } from '../../../text/utf16/types.ts' */ import { codePointToString, stringToCodePointList } from '../../../text/utf16/module.f.mjs' import { errorMask } from '../../../text/code_point/module.f.mjs' import { @@ -35,7 +35,7 @@ const { fromCharCode } = String * The code points JSON gives a two-character escape. Every other code point * below `space` has no short form and goes through `unicodeEscape` instead. */ -const escapeTable = { +const escapeTable = /** @type {const} */ ({ [backspace]: '\\b', [ht]: '\\t', [lf]: '\\n', @@ -43,15 +43,18 @@ const escapeTable = { [cr]: '\\r', [quotationMark]: '\\"', [reverseSolidus]: '\\\\', -} as const +}) -const hexDigit = (value: number): string => +/** @type {(value: number) => string} */ +const hexDigit = value => fromCharCode(value < 10 ? digit0 + value : latinSmallLetterA + value - 10) /** * `\uXXXX` with lowercase hex digits, matching ECMAScript's `UnicodeEscape`. + * + * @type {(unit: number) => string} */ -const unicodeEscape = (unit: number): string => +const unicodeEscape = unit => `\\u${hexDigit(unit >> 12 & 0xf)}${hexDigit(unit >> 8 & 0xf)}${hexDigit(unit >> 4 & 0xf)}${hexDigit(unit & 0xf)}` /** @@ -59,25 +62,29 @@ const unicodeEscape = (unit: number): string => * unpaired surrogate, which well-formed JSON stringification (ES2019) emits as * its `\uXXXX` escape rather than as a code unit; everything else is either a * named escape, a `\u00XX` control escape, or the character itself. + * + * @type {(codePoint: CodePoint) => string} */ -const escapeCodePoint = (codePoint: CodePoint): string => +const escapeCodePoint = codePoint => (codePoint & errorMask) !== 0 ? unicodeEscape(codePoint & 0xffff) - : escapeTable[codePoint] + : escapeTable[/** @type {keyof typeof escapeTable} */ (codePoint)] ?? (codePoint < space ? unicodeEscape(codePoint) : codePointToString(codePoint)) /** * Serializes a string as a JSON string literal. + * + * @type {(_: string) => List} */ export const stringSerialize - : (_: string) => List = input => [`"${concat(map(escapeCodePoint)(stringToCodePointList(input)))}"`] /** * Serializes a number as a JSON number literal. + * + * @type {(_: number) => List} */ export const numberSerialize - : (_: number) => List = input => [jsonStringify(input)] /** @@ -89,35 +96,36 @@ const trueSerialize = ['true'] const falseSerialize = ['false'] +/** @type {(_: boolean) => List} */ export const boolSerialize - : (_: boolean) => List = value => value ? trueSerialize : falseSerialize const comma = [','] +/** @type {Reduce>} */ const joinOp - : Reduce> = b => prior => flat([prior, comma, b]) +/** @type {(input: List>) => List} */ const join - : (input: List>) => List = reduce(joinOp)(empty) +/** @type {(open: string) => (close: string) => (input: List>) => List} */ const wrap - : (open: string) => (close: string) => (input: List>) => List = open => close => { const seqOpen = [open] const seqClose = [close] return input => flat([seqOpen, join(input), seqClose]) } +/** @type {(input: List>) => List} */ export const objectWrap - : (input: List>) => List = wrap('{')('}') /** * Wraps serialized entries into a JSON array. + * + * @type {(input: List>) => List} */ export const arrayWrap - : (input: List>) => List = wrap('[')(']') diff --git a/fjs/media/json/serializer/proof.f.ts b/fjs/media/json/serializer/proof.f.mjs similarity index 96% rename from fjs/media/json/serializer/proof.f.ts rename to fjs/media/json/serializer/proof.f.mjs index 52751da23..73f878a6f 100644 --- a/fjs/media/json/serializer/proof.f.ts +++ b/fjs/media/json/serializer/proof.f.mjs @@ -1,4 +1,4 @@ -import { arrayWrap, boolSerialize, numberSerialize, objectWrap, stringSerialize } from './module.f.ts' +import { arrayWrap, boolSerialize, numberSerialize, objectWrap, stringSerialize } from './module.f.mjs' import * as list from '../../../types/list/module.f.mjs' import { concat } from '../../../types/string/module.f.mjs' import { assertEq } from '../../../asserts/module.f.mjs' @@ -8,7 +8,8 @@ const { toArray } = list // The expected literals below are what the host's `JSON.stringify` produces for // the same input; `stringSerialize` has to reproduce them exactly, so any // divergence in the FunctionalScript escaping shows up here as a failure. -const serialized = (input: string): string => concat(stringSerialize(input)) +/** @type {(input: string) => string} */ +const serialized = input => concat(stringSerialize(input)) export const proof = { arrayWrap: [ diff --git a/fjs/media/json/todo/number-edge-cases.md b/fjs/media/json/todo/number-edge-cases.md index b668e5e26..3d32a6944 100644 --- a/fjs/media/json/todo/number-edge-cases.md +++ b/fjs/media/json/todo/number-edge-cases.md @@ -199,5 +199,5 @@ See [native JSON compatibility](./native-json-compatibility.md). - [Native JSON compatibility](./native-json-compatibility.md) — P5 follow-up; does not block this investigation. - [`fjs/media/json/module.f.ts`](../module.f.ts) — current ordinary JSON surface. -- [`fjs/media/json/serializer/module.f.ts`](../serializer/module.f.ts) — current +- [`fjs/media/json/serializer/module.f.mjs`](../serializer/module.f.mjs) — current primitive serialization implementation to replace/self-host. diff --git a/fjs/media/json/todo/remove-native-json.md b/fjs/media/json/todo/remove-native-json.md index 90dd71fee..9a295fe9a 100644 --- a/fjs/media/json/todo/remove-native-json.md +++ b/fjs/media/json/todo/remove-native-json.md @@ -12,7 +12,7 @@ the total, `Result`-returning `parse`. The writing half is still mostly the host's: - **115 call sites call `JSON.stringify` directly** — and one of them is - `fjs/media/json/serializer/module.f.ts`, so the FunctionalScript serializer + `fjs/media/json/serializer/module.f.mjs`, so the FunctionalScript serializer itself still bottoms out in the host. `numberSerialize` is `JSON.stringify` with a different name, and `fjs/djs/serializer/module.f.ts:15` imports it, so *every number* this repository serializes — JSON and DJS alike — is still @@ -38,8 +38,8 @@ Three reasons to finish the job: | Shape | Sites | Where | Replacement | | --- | --- | --- | --- | -| **Leaf serializer** | 1 | `fjs/media/json/serializer/module.f.ts` | FunctionalScript number formatting — blocks everything below | -| Expected-output comparison | 73 | `fjs/bnf/ll1/proof.f.ts` (27), `fjs/bnf/descent/proof.f.ts` (22), `fjs/media/json/serializer/proof.f.ts` (10), `fjs/djs/tokenizer/proof.f.ts:795-829` (8), `fjs/bnf/data/proof.f.ts` (4), `fjs/media/revision/proof.f.ts:138`, `fjs/cas/evo/proof.f.ts:57` | `stringify(identity)` | +| **Leaf serializer** | 1 | `fjs/media/json/serializer/module.f.mjs` | FunctionalScript number formatting — blocks everything below | +| Expected-output comparison | 73 | `fjs/bnf/ll1/proof.f.ts` (27), `fjs/bnf/descent/proof.f.ts` (22), `fjs/media/json/serializer/proof.f.mjs` (10), `fjs/djs/tokenizer/proof.f.ts:795-829` (8), `fjs/bnf/data/proof.f.ts` (4), `fjs/media/revision/proof.f.ts:138`, `fjs/cas/evo/proof.f.ts:57` | `stringify(identity)` | | Assertion messages | 33 | `fjs/djs/tokenizer/proof.f.ts` (31), `fjs/types/rtti/ts/proof.f.ts:8,12` (2) | pass the value, or `fjs/djs`'s `stringify` | | Source-text quoting | 5 | `fjs/emergent_testing/module.f.ts:305,322,335`, `fjs/types/ts/module.f.mjs:36,48` | `stringSerialize` — already designed in `66c-emit-literals-via-owner-modules.md` | | JSON line framing | 2 | `fjs/emergent_testing/proof.f.ts:42`, `fjs/mcp/proof.f.ts:128` | `stringify(identity)` | @@ -138,7 +138,7 @@ Consider a guard so it does not come back — the cheapest is a proof in ### Related -- [`fjs/media/json/serializer/module.f.ts`](../serializer/module.f.ts) — the +- [`fjs/media/json/serializer/module.f.mjs`](../serializer/module.f.mjs) — the leaf `JSON.stringify` phases 1 and 2 replace; only `numberSerialize` is left. - [`fjs/text/utf16/module.f.ts`](../../../text/utf16/module.f.ts) — where the escaping reads code points, and where phase 1 added `codePointToString`. diff --git a/fjs/media/json/todo/serializer-shared-atoms.md b/fjs/media/json/todo/serializer-shared-atoms.md index 5bee23f82..86b7b6538 100644 --- a/fjs/media/json/todo/serializer-shared-atoms.md +++ b/fjs/media/json/todo/serializer-shared-atoms.md @@ -6,7 +6,7 @@ ### Problem `fjs/djs/serializer/module.f.ts` already imports the serializer atoms it -shares with JSON from `fjs/media/json/serializer/module.f.ts` (line 15: +shares with JSON from `fjs/media/json/serializer/module.f.mjs` (line 15: `objectWrap, arrayWrap, stringSerialize, numberSerialize, nullSerialize, boolSerialize`) — but two pieces of the same property-serialization vocabulary were left behind and re-declared instead: @@ -39,7 +39,7 @@ rather than duplicating. Two real consumers exist for both atoms. ### Proposal -In `fjs/media/json/serializer/module.f.ts`, next to `objectWrap`/`arrayWrap`: +In `fjs/media/json/serializer/module.f.mjs`, next to `objectWrap`/`arrayWrap`: ```ts export const colon: List = [':'] diff --git a/fjs/media/json/tokenizer/proof.f.ts b/fjs/media/json/tokenizer/proof.f.ts index b1c332f8e..b9a463d5d 100644 --- a/fjs/media/json/tokenizer/proof.f.ts +++ b/fjs/media/json/tokenizer/proof.f.ts @@ -1,7 +1,7 @@ import { tokenize, type JsonToken } from './module.f.ts' import { toArray } from '../../../types/list/module.f.mjs' import { stringifyAsTree } from '../../../djs/serializer/module.f.ts' -import { sort } from '../../../types/object/module.f.ts' +import { sort } from '../../../types/object/module.f.mjs' import { stringToList } from '../../../text/utf16/module.f.mjs' const tokenizeString diff --git a/fjs/media/nix/todo/serializer-validation-split.md b/fjs/media/nix/todo/serializer-validation-split.md index 5c18731f2..4baed715c 100644 --- a/fjs/media/nix/todo/serializer-validation-split.md +++ b/fjs/media/nix/todo/serializer-validation-split.md @@ -77,7 +77,7 @@ the way out. `joinChunks(chunks, separator)` (`:154`) re-invent what the other two serializers in `fjs/media/` already do with `fjs/types/list`: -- `fjs/media/json/serializer/module.f.ts:39-52` — `join` (a `reduce` with a +- `fjs/media/json/serializer/module.f.mjs:39-52` — `join` (a `reduce` with a separator) and `wrap(open)(close)`, both over `List>`; - `fjs/media/html/module.f.ts` — `flatMap`/`flat`/`map` over `List` end to end. @@ -224,7 +224,7 @@ build `List` with `fjs/types/list`'s `flat`/`flatMap`/`map` as `fjs/media/html` does. Do **not** plan on importing `join`/`wrap` from -`fjs/media/json/serializer/module.f.ts`. They are private constants (`:39-53`) +`fjs/media/json/serializer/module.f.mjs`. They are private constants (`:39-53`) — only `objectWrap`/`arrayWrap` are exported — and, more decisively, `join` hardcodes its separator to `comma` (`:38`, `:40-42`), while Nix separates bindings with `'\n'` plus an indent. Sharing them would mean parameterizing @@ -286,7 +286,7 @@ where it lands. - [serializer-shared-atoms](../../json/todo/serializer-shared-atoms.md) — shares `colon` / `MapEntries` between the json and djs serializers; the same "one owner for a serializer atom" question, disjoint atoms. -- `fjs/media/json/serializer/module.f.ts` — `join` / `wrap` over +- `fjs/media/json/serializer/module.f.mjs` — `join` / `wrap` over `List>`, the existing chunk-joining vocabulary. - `fjs/types/nullable/module.f.mjs` — the `null`-for-absence convention and the `fromUndefined` boundary rule. diff --git a/fjs/media/revision/module.f.ts b/fjs/media/revision/module.f.ts index f5b0c19e0..a0edbbd8a 100644 --- a/fjs/media/revision/module.f.ts +++ b/fjs/media/revision/module.f.ts @@ -22,7 +22,7 @@ import { cBase32ToVec } from '../../basen/cbase32/module.f.mjs' import type { Result } from '../../types/result/types.ts' import { error, ok } from '../../types/result/module.f.mjs' import { dialectEntry, type DialectEntry } from '../module.f.ts' -import { definedEntries, sort } from '../../types/object/module.f.ts' +import { definedEntries, sort } from '../../types/object/module.f.mjs' import { stringify } from '../json/module.f.ts' /** diff --git a/fjs/media/todo/revision-lock-map.md b/fjs/media/todo/revision-lock-map.md index 04d807d5f..5586b3130 100644 --- a/fjs/media/todo/revision-lock-map.md +++ b/fjs/media/todo/revision-lock-map.md @@ -52,7 +52,7 @@ entries: ```ts import { stringify } from '../json/module.f.ts' -import { sort } from '../../types/object/module.f.ts' +import { sort } from '../../types/object/module.f.mjs' const toJson = stringify(sort) ``` diff --git a/fjs/path/module.f.ts b/fjs/path/module.f.mjs similarity index 73% rename from fjs/path/module.f.ts rename to fjs/path/module.f.mjs index d48ce5542..4787ed16c 100644 --- a/fjs/path/module.f.ts +++ b/fjs/path/module.f.mjs @@ -3,13 +3,13 @@ * * @module */ -import type { Fold, Reduce, Unary } from '../types/function/operator/types.ts' -import type { List } from '../types/list/types.ts' +/** @import { Fold, Reduce, Unary } from '../types/function/operator/types.ts' */ +/** @import { List } from '../types/list/types.ts' */ import { fold, last, take, length, concat as listConcat, toArray } from '../types/list/module.f.mjs' import { join as listJoin, concat as stringConcat } from '../types/string/module.f.mjs' -const foldNormalizeOp: Fold> -= input => state => { +/** @type {Fold>} */ +const foldNormalizeOp = input => state => { switch(input) { case '': case '.': { return state } case '..': { @@ -25,8 +25,10 @@ const foldNormalizeOp: Fold> /** * Converts Windows separators (`\`) to POSIX separators (`/`). + * + * @type {(path: string) => string} */ -export const toPosix = (path: string): string => path.replaceAll('\\', '/') +export const toPosix = path => path.replaceAll('\\', '/') /** * Splits a path into normalized segments. @@ -34,26 +36,30 @@ export const toPosix = (path: string): string => path.replaceAll('\\', '/') * Empty (`""`) and current-directory (`"."`) segments are removed, parent-directory * (`".."`) segments collapse the previous segment when possible, and Windows * separators are converted to POSIX separators. + * + * @type {(path: string) => readonly string[]} */ -export const parse = (path: string): readonly string[] => { +export const parse = path => { const split = toPosix(path).split('/') return toArray(fold(foldNormalizeOp)([])(split)) } /** * Normalizes a path string by parsing and rejoining it with POSIX separators. + * + * @type {Unary} */ -export const normalize: Unary -= path => { +export const normalize = path => { const foldResult = parse(path) return listJoin('/')(foldResult) } /** * Concatenates two path fragments and returns a normalized path. + * + * @type {Reduce} */ -export const concat: Reduce -= a => b => { +export const concat = a => b => { const s = stringConcat([a, '/', b]) return normalize(s) } @@ -64,21 +70,27 @@ export const concat: Reduce * so absolute roots and `.`/`..` segments are preserved verbatim. Use this * for building paths from already-clean segments (directory walks, store * layouts); use {@link concat} when normalization is desired. + * + * @type {(...list: readonly string[]) => string} */ -export const join = (...list: readonly string[]): string => list.join('/') +export const join = (...list) => list.join('/') /** * Returns `path` relative to `base` with a `./` prefix, or `path` unchanged * if it does not start with `base` or `base` is empty. * E.g. `relativize('/repo', '/repo/fs/a.ts')` → `'./fs/a.ts'`. + * + * @type {(base: string, path: string) => string} */ -export const relativize = (base: string, path: string): string => +export const relativize = (base, path) => base !== '' && path.startsWith(base) ? `.${path.slice(base.length)}` : path /** * Returns `true` when `prefix` is a strict ancestor of `path` in segment space: * every segment of `prefix` matches the corresponding segment of `path`, and * `path` has at least one additional segment. + * + * @type {(prefix: readonly string[], path: readonly string[]) => boolean} */ -export const isProperPrefix = (prefix: readonly string[], path: readonly string[]): boolean => +export const isProperPrefix = (prefix, path) => prefix.length < path.length && prefix.every((seg, i) => seg === path[i]) diff --git a/fjs/path/proof.f.ts b/fjs/path/proof.f.mjs similarity index 98% rename from fjs/path/proof.f.ts rename to fjs/path/proof.f.mjs index 77379c113..bbb1884ed 100644 --- a/fjs/path/proof.f.ts +++ b/fjs/path/proof.f.mjs @@ -1,4 +1,4 @@ -import { concat, isProperPrefix, join, normalize, relativize, toPosix } from "./module.f.ts" +import { concat, isProperPrefix, join, normalize, relativize, toPosix } from "./module.f.mjs" import { assertEq } from '../asserts/module.f.mjs' const normalizeTest = [ diff --git a/fjs/protocol/mcp/stdio/module.f.ts b/fjs/protocol/mcp/stdio/module.f.ts index b2a2d7a6c..3b347e23b 100644 --- a/fjs/protocol/mcp/stdio/module.f.ts +++ b/fjs/protocol/mcp/stdio/module.f.ts @@ -33,7 +33,7 @@ import { pure, step, type Effect, type Operation } from '../../../effects/module 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' -import { sort } from '../../../types/object/module.f.ts' +import { sort } from '../../../types/object/module.f.mjs' import { internalError, jsonrpc, parseError, type Response } from '../../json_rpc/module.f.ts' import { error, ok } from '../../../types/result/module.f.mjs' diff --git a/fjs/protocol/mcp/stdio/proof.f.ts b/fjs/protocol/mcp/stdio/proof.f.ts index 80229c047..af97df6c9 100644 --- a/fjs/protocol/mcp/stdio/proof.f.ts +++ b/fjs/protocol/mcp/stdio/proof.f.ts @@ -6,7 +6,7 @@ import { stringify } from '../../../media/json/module.f.ts' import { utf8 } from '../../../text/module.f.mjs' import { fromVec } from '../../../types/uint8array/module.f.mjs' import { maxLengthBytes } from '../../../types/bit_vec/module.f.mjs' -import { sort } from '../../../types/object/module.f.ts' +import { sort } from '../../../types/object/module.f.mjs' import { internalError, jsonrpc, parseError, type Id, type Response } from '../../json_rpc/module.f.ts' import { stdioTransport, type Step } from './module.f.ts' diff --git a/fjs/sul/README.md b/fjs/sul/README.md index 3459d5fb8..81a46eed7 100644 --- a/fjs/sul/README.md +++ b/fjs/sul/README.md @@ -23,7 +23,7 @@ The top-level encoder accepts a stream of bits and produces a single [`Id`](id/R ```ts import { encode, emptyEncodeState } from './module.f.ts' -import type { Add } from './level/hash/module.f.ts' +import type { Add } from './level/hash/types.ts' const enc = encode(add) // add: Add — called on every merge let state = emptyEncodeState(storage) diff --git a/fjs/sul/id/module.f.ts b/fjs/sul/id/module.f.mjs similarity index 74% rename from fjs/sul/id/module.f.ts rename to fjs/sul/id/module.f.mjs index 4bc0cb2c8..5586faca2 100644 --- a/fjs/sul/id/module.f.ts +++ b/fjs/sul/id/module.f.mjs @@ -2,29 +2,24 @@ * 256-bit content-addressed identifiers for SUL values. * Every identifier is one of three variants: a level-3 literal (inline), a raw bit vector (inline), * or a SHA2-based hash, selected automatically by `compress` based on size and input type. + * See `./types.ts` for the `Id` type. * * @module */ import { toArray } from '../../types/list/module.f.mjs' -import type { Vec } from '../../types/bit_vec/types.ts' +/** @import { Vec } from '../../types/bit_vec/types.ts' */ import { length, msb, uint, uintChunkList, unpack, vec } from '../../types/bit_vec/module.f.mjs' import { assertEq } from '../../asserts/module.f.mjs' import { utf8 } from '../../text/module.f.mjs' -import type { Point2D } from '../../crypto/secp/types.ts' +/** @import { Point2D } from '../../crypto/secp/types.ts' */ import { secp256r1 } from '../../crypto/secp/module.f.mjs' -import type { V8 } from '../../crypto/sha2/types.ts' +/** @import { V8 } from '../../crypto/sha2/types.ts' */ import { base32 } from '../../crypto/sha2/module.f.mjs' -import { literal3ToVec } from '../level/literal/module.f.ts' +import { literal3ToVec } from '../level/literal/module.f.mjs' import { log2 } from '../../types/bigint/module.f.mjs' -import type { Nominal } from '../../types/nominal/types.ts' import { asBase, asNominal } from '../../types/nominal/module.f.mjs' - -/** A 256-bit SUL identifier. One of three variants: level-3 literal, raw bit vector, or SHA2 hash. */ -export type Id = Nominal< - 'sul/id', - '6f5f6da053a6ac70e9687d42b7a09e925c3be21027f55beb2cba3040bf3d5b71', - bigint> +/** @import { Id } from './types.ts' */ // 32 bytes = 256 bits. // @@ -36,7 +31,8 @@ const utf8IvSeed = utf8(ivSeed) const c = secp256r1 -const ivUint: bigint = (c.mul(uint(utf8IvSeed))(c.g) as Point2D)[0] +/** @type {bigint} */ +const ivUint = /** @type {Point2D} */ (c.mul(uint(utf8IvSeed))(c.g))[0] // 64 hex = 256 bits = 32 bytes: assertEq( @@ -46,14 +42,16 @@ assertEq( 0x325d5666_573eb118_f32191de_20d17f64_33392ba3_291ae46c_1474a5ed_a5383f25n ) -const iv = toArray(uintChunkList(msb)(32n)({ length: 256n, uint: ivUint })) as V8 +const iv = /** @type {V8} */ (toArray(uintChunkList(msb)(32n)({ length: 256n, uint: ivUint }))) assertEq(iv.length, 8) /** * Note: no need to add a prefix. + * + * @type {(v: bigint) => Id} */ -export const level3Id: (v: bigint) => Id = +export const level3Id = asNominal const rawPrefixOffset = 0xFEn @@ -67,24 +65,26 @@ assertEq( 0x40000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000n ) +// 253 +const rawLenMax = 0xFDn + /** * Note: length(symbol) <= 253n * - * @param symbol - * @returns + * @param {Vec} symbol + * @returns {Id} */ -export const rawId = (symbol: Vec): Id => { +export const rawId = symbol => { const { length, uint } = unpack(symbol) return asNominal(rawPrefix | uint | (1n << length)) } -// 253 -const rawLenMax = 0xFDn - -export const isRaw = (v: Id): boolean => +/** @type {(v: Id) => boolean} */ +export const isRaw = v => asBase(v) >> rawPrefixOffset === 1n -const toRaw = (a: Id): Vec => { +/** @type {(a: Id) => Vec} */ +const toRaw = a => { if (!isRaw(a)) { return literal3ToVec(asBase(a)) } @@ -104,17 +104,18 @@ assertEq( 0x80000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000n ) -export const isHash = (v: Id): boolean => +/** @type {(v: Id) => boolean} */ +export const isHash = v => asBase(v) >> hashPrefixOffset === 1n /** * Note: we don't need to remove the prefix bits from the hash because * the prefix equals the prefix mask (`1n << 0xFFn`). * - * @param symbol - * @returns + * @param {bigint} hash + * @returns {Id} */ -export const hashId = (hash: bigint): Id => +export const hashId = hash => asNominal(hashPrefix | hash) const hash2 = base32.compress(iv) @@ -123,10 +124,12 @@ const vecX20 = vec(0x20n) const { concat, listToVec } = msb -const hashMerge = (a: Id, b: Id): Id => +/** @type {(a: Id, b: Id) => Id} */ +const hashMerge = (a, b) => hashId(uint(listToVec(hash2((asBase(a) << 0x100n) | asBase(b)).map(vecX20)))) -export const compress = (a: Id, b: Id): Id => { +/** @type {(a: Id, b: Id) => Id} */ +export const compress = (a, b) => { if (isHash(a) || isHash(b)) { return hashMerge(a, b) } diff --git a/fjs/sul/id/proof.f.ts b/fjs/sul/id/proof.f.mjs similarity index 99% rename from fjs/sul/id/proof.f.ts rename to fjs/sul/id/proof.f.mjs index 4fc219201..66e58e577 100644 --- a/fjs/sul/id/proof.f.ts +++ b/fjs/sul/id/proof.f.mjs @@ -1,7 +1,7 @@ import { assert, assertEq } from '../../asserts/module.f.mjs' import { mask } from '../../types/bigint/module.f.mjs' import { vec } from '../../types/bit_vec/module.f.mjs' -import { compress, hashId, isHash, isRaw, level3Id, rawId } from './module.f.ts' +import { compress, hashId, isHash, isRaw, level3Id, rawId } from './module.f.mjs' import { asBase } from '../../types/nominal/module.f.mjs' // literal3ToVec bit patterns for symbols used below: diff --git a/fjs/sul/id/types.ts b/fjs/sul/id/types.ts new file mode 100644 index 000000000..8dc4f68f6 --- /dev/null +++ b/fjs/sul/id/types.ts @@ -0,0 +1,13 @@ +/** + * Type-level API for SUL identifiers. + * + * @module + */ + +import type { Nominal } from '../../types/nominal/types.ts' + +/** A 256-bit SUL identifier. One of three variants: level-3 literal, raw bit vector, or SHA2 hash. */ +export type Id = Nominal< + 'sul/id', + '6f5f6da053a6ac70e9687d42b7a09e925c3be21027f55beb2cba3040bf3d5b71', + bigint> diff --git a/fjs/sul/level/hash/module.f.mjs b/fjs/sul/level/hash/module.f.mjs new file mode 100644 index 000000000..fbf61694b --- /dev/null +++ b/fjs/sul/level/hash/module.f.mjs @@ -0,0 +1,61 @@ +/** + * Streaming encoder for SUL hash-level symbols using a Patricia trie and 256-bit content-addressed hashes. + * See `./types.ts` for the `Add`/`EncodeState` type-level API. + * + * @module + */ + +/** @import { Create } from '../../../types/patricia_trie/types.ts' */ +import { emptyState, patriciaTrie } from '../../../types/patricia_trie/module.f.mjs' +import { compress } from '../../id/module.f.mjs' +/** @import { Id } from '../../id/types.ts' */ +import { asBase } from '../../../types/nominal/module.f.mjs' +/** @import { Add, EncodeState } from './types.ts' */ + +/** + * Returns a streaming encoder for hash-level symbols. + * + * The returned step function processes one level-3 symbol at a time. + * It returns `undefined` while the strictly-decreasing prefix is still + * being accumulated. When the terminating symbol `t` arrives (`t >= last`), + * the word `[s0 > ... > sk, t]` is finalised: + * + * 1. The Patricia trie of `[s0, ..., sk]` is collapsed to a root hash. + * 2. The root is merged with `t` via `compress`, producing the output symbol. + * 3. State is reset to an empty stack (storage is preserved). + * + * `add` is called once for every `compress` call. `isSymbol` is `false` for + * Patricia trie internal merges and `true` for the terminal `compress(root, t)`. + */ +export const encode = + /** + * @template S + * @param {Add} add + * @returns {(symbol: Id, state: EncodeState) => readonly [Id | undefined, EncodeState]} + */ + add => { + /** @type {(isSymbol: boolean) => Create} */ + const create = isSymbol => (a, b, s) => { + const m = compress(a, b) + return [m, add(a, b, m, isSymbol, s)] + } + const { push, end } = patriciaTrie(create(false)) + const rootCreate = create(true) + return (symbol, state) => { + const [, stack] = state + const last = stack.at(-1) + if (last === undefined || last[0] > asBase(symbol)) { + return [undefined, push([asBase(symbol), symbol], state)] + } + const [root1, storage1] = end(state) + const [root2, storage2] = rootCreate(/** @type {Id} */ (root1), symbol, storage1) + return [root2, [storage2, []]] + } + } + +/** + * Returns the initial encoding state wrapping `storage`. + * + * @type {(storage: S) => EncodeState} + */ +export const emptyEncodeState = emptyState diff --git a/fjs/sul/level/hash/module.f.ts b/fjs/sul/level/hash/module.f.ts deleted file mode 100644 index 55be612d2..000000000 --- a/fjs/sul/level/hash/module.f.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Streaming encoder for SUL hash-level symbols using a Patricia trie and 256-bit content-addressed hashes. - * - * @module - */ - -import type { Create, State } from '../../../types/patricia_trie/types.ts' -import { emptyState, patriciaTrie } from '../../../types/patricia_trie/module.f.mjs' -import { compress, type Id } from '../../id/module.f.ts' -import { asBase } from '../../../types/nominal/module.f.mjs' - -/** - * Called once per merge during encoding. `merged = compress(left, right)`. - * `isSymbol` is `true` for the terminal `compress(root, t)` that produces the - * word-level output symbol, and `false` for Patricia trie internal merges. - * Implementations record the triple in a content-addressed store. - */ -export type Add = (left: Id, right: Id, merged: Id, isSymbol: boolean, storage: S) => S - -/** - * Streaming state for hash-level encoding. - * Wraps the Patricia trie state that accumulates the strictly-decreasing - * prefix of the word currently being encoded. - */ -export type EncodeState = State - -/** - * Returns a streaming encoder for hash-level symbols. - * - * The returned step function processes one level-3 symbol at a time. - * It returns `undefined` while the strictly-decreasing prefix is still - * being accumulated. When the terminating symbol `t` arrives (`t >= last`), - * the word `[s0 > ... > sk, t]` is finalised: - * - * 1. The Patricia trie of `[s0, ..., sk]` is collapsed to a root hash. - * 2. The root is merged with `t` via `compress`, producing the output symbol. - * 3. State is reset to an empty stack (storage is preserved). - * - * `add` is called once for every `compress` call. `isSymbol` is `false` for - * Patricia trie internal merges and `true` for the terminal `compress(root, t)`. - */ -export const encode = - (add: Add): (symbol: Id, state: EncodeState) => readonly[Id|undefined, EncodeState] => -{ - const create = (isSymbol: boolean): Create => (a, b, s) => { - const m = compress(a, b) - return [m, add(a, b, m, isSymbol, s)] - } - const { push, end } = patriciaTrie(create(false)) - const rootCreate = create(true) - return (symbol, state) => { - const [, stack] = state - const last = stack.at(-1) - if (last === undefined || last[0] > asBase(symbol)) { - return [undefined, push([asBase(symbol), symbol], state)] - } - const [root1, storage1] = end(state) - const [root2, storage2] = rootCreate(root1!, symbol, storage1) - return [root2, [storage2, []]] - } -} - -/** Returns the initial encoding state wrapping `storage`. */ -export const emptyEncodeState: (storage: S) => EncodeState = emptyState diff --git a/fjs/sul/level/hash/proof.f.ts b/fjs/sul/level/hash/proof.f.mjs similarity index 83% rename from fjs/sul/level/hash/proof.f.ts rename to fjs/sul/level/hash/proof.f.mjs index 4d6261b2d..8edd33819 100644 --- a/fjs/sul/level/hash/proof.f.ts +++ b/fjs/sul/level/hash/proof.f.mjs @@ -1,15 +1,20 @@ import { assert, assertEq } from '../../../asserts/module.f.mjs' -import { compress, level3Id, type Id } from '../../id/module.f.ts' -import { emptyEncodeState, encode, type EncodeState } from './module.f.ts' +import { compress, level3Id } from '../../id/module.f.mjs' +/** @import { Id } from '../../id/types.ts' */ +import { emptyEncodeState, encode } from './module.f.mjs' +/** @import { EncodeState } from './types.ts' */ -type NodeList = readonly [Id, Id, Id, boolean][] +/** @typedef {readonly (readonly [Id, Id, Id, boolean])[]} _NodeList */ -const add = (l: Id, r: Id, m: Id, isSymbol: boolean, s: NodeList): NodeList => [...s, [l, r, m, isSymbol]] +/** @type {(l: Id, r: Id, m: Id, isSymbol: boolean, s: _NodeList) => _NodeList} */ +const add = (l, r, m, isSymbol, s) => [...s, [l, r, m, isSymbol]] const enc = encode(add) -const initial: EncodeState = emptyEncodeState([]) +/** @type {EncodeState<_NodeList>} */ +const initial = emptyEncodeState([]) // Run a complete valid word from a clean state; throws if no output is produced. -const runWord = (symbols: readonly Id[]): readonly [Id, NodeList] => { +/** @type {(symbols: readonly Id[]) => readonly [Id, _NodeList]} */ +const runWord = symbols => { let state = initial for (const s of symbols) { const [out, newState] = enc(s, state) @@ -20,7 +25,8 @@ const runWord = (symbols: readonly Id[]): readonly [Id, NodeList] => { } // Every stored triple must satisfy m === compress(l, r). -const verifyStorage = (storage: NodeList) => { +/** @type {(storage: _NodeList) => void} */ +const verifyStorage = storage => { for (const [l, r, m] of storage) { assertEq(m, compress(l, r)) } } @@ -71,7 +77,7 @@ export const proof = { // Output equals the merged value in the last add call output_is_last_add: () => { const [out, storage] = runWord([s1, s0, s1]) - assertEq(out, storage.at(-1)![2]) + assertEq(out, /** @type {_NodeList[number]} */ (storage.at(-1))[2]) }, // Stack is empty after flush; storage is preserved diff --git a/fjs/sul/level/hash/types.ts b/fjs/sul/level/hash/types.ts new file mode 100644 index 000000000..a780d86ac --- /dev/null +++ b/fjs/sul/level/hash/types.ts @@ -0,0 +1,23 @@ +/** + * Type-level API for hash-level SUL encoding. + * + * @module + */ + +import type { State } from '../../../types/patricia_trie/types.ts' +import type { Id } from '../../id/types.ts' + +/** + * Called once per merge during encoding. `merged = compress(left, right)`. + * `isSymbol` is `true` for the terminal `compress(root, t)` that produces the + * word-level output symbol, and `false` for Patricia trie internal merges. + * Implementations record the triple in a content-addressed store. + */ +export type Add = (left: Id, right: Id, merged: Id, isSymbol: boolean, storage: S) => S + +/** + * Streaming state for hash-level encoding. + * Wraps the Patricia trie state that accumulates the strictly-decreasing + * prefix of the word currently being encoded. + */ +export type EncodeState = State diff --git a/fjs/sul/level/literal/module.f.ts b/fjs/sul/level/literal/module.f.mjs similarity index 52% rename from fjs/sul/level/literal/module.f.ts rename to fjs/sul/level/literal/module.f.mjs index 4f2395100..48355bb86 100644 --- a/fjs/sul/level/literal/module.f.ts +++ b/fjs/sul/level/literal/module.f.mjs @@ -1,51 +1,33 @@ /** * Bijective encoding between words of level-k symbols and single symbols of level k+1, - * for the first three literal SUL levels. + * for the first three literal SUL levels. See `./types.ts` for the + * `Word`/`EncodeState`/`Level`/`PipelineState`/`LiteralToVec` type-level API. * * @module */ import { log2 } from '../../../types/bigint/module.f.mjs' -import type { Vec } from '../../../types/bit_vec/types.ts' import { msb, vec } from '../../../types/bit_vec/module.f.mjs' -import type { Func } from '../../../types/function/types.ts' -import type { Equal, StateScan } from '../../../types/function/operator/types.ts' +/** @import { Equal, StateScan } from '../../../types/function/operator/types.ts' */ import { strictEqual } from '../../../types/function/operator/module.f.mjs' -import type { List } from '../../../types/list/types.ts' +/** @import { List } from '../../../types/list/types.ts' */ import { equal, map } from '../../../types/list/module.f.mjs' import { join } from '../../../types/string/module.f.mjs' +/** @import { EncodeState, Level, LiteralToVec, PipelineState } from './types.ts' */ -export const symbolToString = (s: bigint): string => s.toString(16) +/** @type {(s: bigint) => string} */ +export const symbolToString = s => s.toString(16) -export type Word = readonly bigint[] - -export const wordToString = (word: List): string => +/** @type {(word: List) => string} */ +export const wordToString = word => join(',')(map(symbolToString)(word)) -export const wordEqual: Equal> = equal(strictEqual) - -/** - * Streaming encoder state: `[last, part]`. - * - `last` — the most recently consumed input symbol, or `undefined` before any symbol is seen. - * - `part` — accumulated index offset from the decreasing prefix consumed so far. - */ -export type EncodeState = readonly[bigint|undefined, bigint] +/** @type {Equal>} */ +export const wordEqual = equal(strictEqual) /** Initial encoder state: no symbols seen, zero offset. */ -export const emptyEncodeState: EncodeState = [undefined, 0n] - -/** - * A literal SUL level with finite alphabet `[0, n)`. - */ -export type Level = { - /** Number of valid words whose first symbol is ≤ `i`. */ - readonly sum: (i: bigint) => bigint - /** Inverse of {@link Level.encode}: restores the complete word from a symbol. */ - readonly decode: (i: bigint) => List - /** Streaming encoder: processes one input symbol at a time, emitting an output symbol only - * when the terminating symbol `t >= last` arrives. */ - readonly encode: StateScan -} +/** @type {EncodeState} */ +export const emptyEncodeState = [undefined, 0n] /** * Creates a {@link Level} for alphabet size `n = 2^e + 1`. @@ -58,9 +40,10 @@ export type Level = { * | `2` | `5` | * | `7` | `0x81` | * - * @param e `log2(n - 1)` + * @param {bigint} e `log2(n - 1)` + * @returns {Level} */ -export const level = (e: bigint): Level => { +export const level = e => { // m = n - 1 const m = 1n << e const n = m + 1n @@ -69,8 +52,9 @@ export const level = (e: bigint): Level => { // m2 = 2 * m const m2 = m << 1n const e1 = e + 1n - const sum = (i: bigint) => (m2 << i) + i - k - const decode = (i: bigint): List => () => { + const sum = (/** @type {bigint} */ i) => (m2 << i) + i - k + /** @type {(i: bigint) => List} */ + const decode = i => () => { const r = log2((i + k) >> e1) const s0 = sum(r) > i ? r : r + 1n const s1 = i - sum(s0) + n @@ -92,17 +76,17 @@ const l1 = level(0n) const l2 = level(2n) const l3 = level(7n) -/** Combined encoder state for the three-level literal pipeline (L1 → L2 → L3). */ -export type PipelineState = readonly [EncodeState, EncodeState, EncodeState] - /** Initial state for the three-level literal pipeline. */ -export const emptyPipelineState: PipelineState = [emptyEncodeState, emptyEncodeState, emptyEncodeState] +/** @type {PipelineState} */ +export const emptyPipelineState = [emptyEncodeState, emptyEncodeState, emptyEncodeState] /** * Advances the three-level literal pipeline by one bit. * Returns a level-3 symbol whenever the pipeline emits, otherwise `undefined`. + * + * @type {StateScan} */ -export const pipelineStep: StateScan = +export const pipelineStep = (bit, [l1s, l2s, l3s]) => { const [l1Out, newL1s] = l1.encode(bit, l1s) if (l1Out === undefined) return [undefined, [newL1s, l2s, l3s]] @@ -114,21 +98,23 @@ export const pipelineStep: StateScan const vec1 = vec(1n) -export type LiteralToVec = Func - const { listToVec } = msb -const literalToVec = (prior: LiteralToVec, e: bigint): LiteralToVec => { +/** @type {(prior: LiteralToVec, e: bigint) => LiteralToVec} */ +const literalToVec = (prior, e) => { const m = map(prior) const { decode } = level(e) return literal => listToVec(m(decode(literal))) } /** Decodes a level-1 symbol to its canonical MSB bit vector. */ -export const literal1ToVec: LiteralToVec = literalToVec(vec1, 0n) +/** @type {LiteralToVec} */ +export const literal1ToVec = literalToVec(vec1, 0n) /** Decodes a level-2 symbol to its canonical MSB bit vector (via level-1 decoding). */ -export const literal2ToVec: LiteralToVec = literalToVec(literal1ToVec, 2n) +/** @type {LiteralToVec} */ +export const literal2ToVec = literalToVec(literal1ToVec, 2n) /** Decodes a level-3 symbol to its canonical MSB bit vector (via level-2 and level-1 decoding). */ -export const literal3ToVec: LiteralToVec = literalToVec(literal2ToVec, 7n) +/** @type {LiteralToVec} */ +export const literal3ToVec = literalToVec(literal2ToVec, 7n) diff --git a/fjs/sul/level/literal/proof.f.ts b/fjs/sul/level/literal/proof.f.mjs similarity index 98% rename from fjs/sul/level/literal/proof.f.ts rename to fjs/sul/level/literal/proof.f.mjs index 753405664..085429966 100644 --- a/fjs/sul/level/literal/proof.f.ts +++ b/fjs/sul/level/literal/proof.f.mjs @@ -1,4 +1,4 @@ -import type { Vec } from '../../../types/bit_vec/types.ts' +/** @import { Vec } from '../../../types/bit_vec/types.ts' */ import { chunkList, msb, vec } from '../../../types/bit_vec/module.f.mjs' import { assert, assertEq } from '../../../asserts/module.f.mjs' import { map, stateScan, toArray } from '../../../types/list/module.f.mjs' @@ -13,18 +13,18 @@ import { symbolToString, wordEqual, wordToString -} from './module.f.ts' +} from './module.f.mjs' -const tests = (n: bigint) => { +const tests = (/** @type {bigint} */ n) => { const { sum, decode, encode: push } = level(n) return { - c: (i: bigint, s: bigint) => { + c: (/** @type {bigint} */ i, /** @type {bigint} */ s) => { const result = sum(i) if (result !== s) { throw new Error(`Assertion failed for n=${n}, i=${i}, s=${symbolToString(s)}, got ${symbolToString(result)}`); } }, - n: (word: readonly bigint[], expected: bigint) => { + n: (/** @type {readonly bigint[]} */ word, /** @type {bigint} */ expected) => { // encode const decoded = decode(expected) if (!wordEqual(decoded)(word)) { @@ -39,14 +39,14 @@ const tests = (n: bigint) => { } } -const l = (f: (literal: bigint) => Vec) => (l: bigint, e: Vec) => { +const l = (/** @type {(literal: bigint) => Vec} */ f) => (/** @type {bigint} */ l, /** @type {Vec} */ e) => { const result = f(l) assertEq(result, e, [result, e]) } -const vecToBits = (v: Vec) => map((b: Vec) => msb.front(1n)(b))(chunkList(msb)(1n)(v)) +const vecToBits = (/** @type {Vec} */ v) => map((/** @type {Vec} */ b) => msb.front(1n)(b))(chunkList(msb)(1n)(v)) -const w = (symbol: bigint, expected: Vec) => { +const w = (/** @type {bigint} */ symbol, /** @type {Vec} */ expected) => { const result = literal3ToVec(symbol) assertEq(result, expected, [result, expected]) const a = toArray(stateScan(pipelineStep)(emptyPipelineState)(vecToBits(expected))) diff --git a/fjs/sul/level/literal/types.ts b/fjs/sul/level/literal/types.ts new file mode 100644 index 000000000..f011fb448 --- /dev/null +++ b/fjs/sul/level/literal/types.ts @@ -0,0 +1,37 @@ +/** + * Type-level API for the literal SUL level encoding. + * + * @module + */ + +import type { Vec } from '../../../types/bit_vec/types.ts' +import type { Func } from '../../../types/function/types.ts' +import type { StateScan } from '../../../types/function/operator/types.ts' +import type { List } from '../../../types/list/types.ts' + +export type Word = readonly bigint[] + +/** + * Streaming encoder state: `[last, part]`. + * - `last` — the most recently consumed input symbol, or `undefined` before any symbol is seen. + * - `part` — accumulated index offset from the decreasing prefix consumed so far. + */ +export type EncodeState = readonly [bigint | undefined, bigint] + +/** + * A literal SUL level with finite alphabet `[0, n)`. + */ +export type Level = { + /** Number of valid words whose first symbol is ≤ `i`. */ + readonly sum: (i: bigint) => bigint + /** Inverse of {@link Level.encode}: restores the complete word from a symbol. */ + readonly decode: (i: bigint) => List + /** Streaming encoder: processes one input symbol at a time, emitting an output symbol only + * when the terminating symbol `t >= last` arrives. */ + readonly encode: StateScan +} + +/** Combined encoder state for the three-level literal pipeline (L1 → L2 → L3). */ +export type PipelineState = readonly [EncodeState, EncodeState, EncodeState] + +export type LiteralToVec = Func diff --git a/fjs/sul/module.f.ts b/fjs/sul/module.f.ts index 4834d008d..ea9b46928 100644 --- a/fjs/sul/module.f.ts +++ b/fjs/sul/module.f.ts @@ -5,9 +5,12 @@ * @module */ -import { emptyPipelineState, pipelineStep, type PipelineState } from './level/literal/module.f.ts' -import { encode as hashEncode, type Add } from './level/hash/module.f.ts' -import { level3Id, type Id } from './id/module.f.ts' +import { emptyPipelineState, pipelineStep } from './level/literal/module.f.mjs' +import type { PipelineState } from './level/literal/types.ts' +import { encode as hashEncode } from './level/hash/module.f.mjs' +import type { Add } from './level/hash/types.ts' +import { level3Id } from './id/module.f.mjs' +import type { Id } from './id/types.ts' import type { InternalState } from '../types/patricia_trie/types.ts' type HashState = InternalState diff --git a/fjs/sul/proof.f.ts b/fjs/sul/proof.f.ts index 00dd76bf6..c42046367 100644 --- a/fjs/sul/proof.f.ts +++ b/fjs/sul/proof.f.ts @@ -1,7 +1,8 @@ import { assert, assertEq } from '../asserts/module.f.mjs' -import { compress, type Id } from './id/module.f.ts' +import { compress } from './id/module.f.mjs' +import type { Id } from './id/types.ts' import { encode, emptyEncodeState } from './module.f.ts' -import type { Add } from './level/hash/module.f.ts' +import type { Add } from './level/hash/types.ts' type Merge = readonly [Id, Id, Id, boolean] diff --git a/fjs/sul/todo/186.md b/fjs/sul/todo/186.md index 3bbea1ddc..4380fc003 100644 --- a/fjs/sul/todo/186.md +++ b/fjs/sul/todo/186.md @@ -7,7 +7,7 @@ 256-bit `bigint` by re-deriving the MSB concatenation by hand: ```ts -// fjs/sul/id/module.f.ts:125 +// fjs/sul/id/module.f.mjs:125 const hash2 = base32.compress(iv) const vecX20 = vec(0x20n) diff --git a/fjs/sul/todo/66m-sul-literal-level-reuse.md b/fjs/sul/todo/66m-sul-literal-level-reuse.md index 54006b556..e17d604ee 100644 --- a/fjs/sul/todo/66m-sul-literal-level-reuse.md +++ b/fjs/sul/todo/66m-sul-literal-level-reuse.md @@ -5,14 +5,14 @@ ### Problem -`fjs/sul/level/literal/module.f.ts` states one fact — *the first three literal SUL +`fjs/sul/level/literal/module.f.mjs` states one fact — *the first three literal SUL levels have exponents `0n`, `2n`, `7n`* — but states it **twice**, and builds the three `Level` objects **twice**. First, eagerly, for the streaming pipeline: ```ts -// fjs/sul/level/literal/module.f.ts:88-90 +// fjs/sul/level/literal/module.f.mjs:88-90 const l1 = level(0n) const l2 = level(2n) const l3 = level(7n) @@ -68,7 +68,7 @@ than two parallel constructions that happen to agree. Behaviour is unchanged — - [ ] Change `literalToVec`'s second parameter from `e: bigint` to a `Level` (or `{ decode }`), drop the internal `level(e)` call, and pass `l1`/`l2`/`l3` at the three call sites. -- [ ] Run `npx tsc` and `fjs t`; confirm `fjs/sul/level/literal/proof.f.ts` still +- [ ] Run `npx tsc` and `fjs t`; confirm `fjs/sul/level/literal/proof.f.mjs` still passes with full line/branch coverage. ### Related diff --git a/fjs/sul/todo/id-prefix-tag-factory.md b/fjs/sul/todo/id-prefix-tag-factory.md index b3f3d0eec..4ab8bcfcc 100644 --- a/fjs/sul/todo/id-prefix-tag-factory.md +++ b/fjs/sul/todo/id-prefix-tag-factory.md @@ -5,7 +5,7 @@ ### Problem -`fjs/sul/id/module.f.ts` implements the "a 256-bit id is tagged by its top +`fjs/sul/id/module.f.mjs` implements the "a 256-bit id is tagged by its top set bit at `offset`; membership is `asBase(v) >> offset === 1n`" scheme twice, byte-identical modulo the offset: diff --git a/fjs/text/ascii/proof.f.ts b/fjs/text/ascii/proof.f.ts index 306e5f6da..1117343d4 100644 --- a/fjs/text/ascii/proof.f.ts +++ b/fjs/text/ascii/proof.f.ts @@ -1,6 +1,6 @@ import { one, range } from './module.f.mjs' import { stringify as jsonStringify } from '../../media/json/module.f.ts' -import { sort } from '../../types/object/module.f.ts' +import { sort } from '../../types/object/module.f.mjs' import { assertEq } from '../../asserts/module.f.mjs' const stringify = jsonStringify(sort) diff --git a/fjs/text/utf16/proof.f.ts b/fjs/text/utf16/proof.f.ts index f3058d411..e6fe4f377 100644 --- a/fjs/text/utf16/proof.f.ts +++ b/fjs/text/utf16/proof.f.ts @@ -8,7 +8,7 @@ import { codePointToString } from './module.f.mjs' import { stringify as jsonStringify, type Unknown } from '../../media/json/module.f.ts' -import { sort } from '../../types/object/module.f.ts' +import { sort } from '../../types/object/module.f.mjs' import { toArray } from '../../types/list/module.f.mjs' import { assertEq } from '../../asserts/module.f.mjs' diff --git a/fjs/text/utf8/proof.f.ts b/fjs/text/utf8/proof.f.ts index 837d1118b..bc80dfd66 100644 --- a/fjs/text/utf8/proof.f.ts +++ b/fjs/text/utf8/proof.f.ts @@ -1,6 +1,6 @@ import { toCodePointList, fromCodePointList, fromVec, utf8ByteToCodePointOp } from './module.f.mjs' import { stringify as jsonStringify } from '../../media/json/module.f.ts' -import { sort } from '../../types/object/module.f.ts' +import { sort } from '../../types/object/module.f.mjs' import { toArray } from '../../types/list/module.f.mjs' import { msb, u8ListToVec, vec } from '../../types/bit_vec/module.f.mjs' import { assertEq } from '../../asserts/module.f.mjs' diff --git a/fjs/types/array/proof.f.ts b/fjs/types/array/proof.f.ts index c4bd82e59..a22743f53 100644 --- a/fjs/types/array/proof.f.ts +++ b/fjs/types/array/proof.f.ts @@ -1,6 +1,6 @@ import { at, first, last, head, tail, splitFirst, splitLast, empty } from './module.f.mjs' import { stringify as jsonStringify } from '../../media/json/module.f.ts' -import { sort } from '../object/module.f.ts' +import { sort } from '../object/module.f.mjs' import { assertEq, assertNotNullish } from '../../asserts/module.f.mjs' const stringify = jsonStringify(sort) diff --git a/fjs/types/btree/find/proof.f.ts b/fjs/types/btree/find/proof.f.ts index 52e6ebfcb..111d71bbb 100644 --- a/fjs/types/btree/find/proof.f.ts +++ b/fjs/types/btree/find/proof.f.ts @@ -2,7 +2,7 @@ import type { Result } from './types.ts' import { find as btreeFind } from './module.f.mjs' import { map, toArray } from '../../list/module.f.mjs' import { stringify, type Unknown } from '../../../media/json/module.f.ts' -import { sort } from '../../object/module.f.ts' +import { sort } from '../../object/module.f.mjs' import type { TNode } from '../types/types.ts' import { cmp } from '../../string/module.f.mjs' import { set as setSet } from '../set/module.f.mjs' diff --git a/fjs/types/btree/proof.f.ts b/fjs/types/btree/proof.f.ts index 66a5b1c25..fc927e915 100644 --- a/fjs/types/btree/proof.f.ts +++ b/fjs/types/btree/proof.f.ts @@ -1,7 +1,7 @@ import type { TNode } from './types/types.ts' import { values } from './module.f.mjs' import { stringify as jsonStringify, type Unknown } from '../../media/json/module.f.ts' -import { sort } from '../object/module.f.ts' +import { sort } from '../object/module.f.mjs' import { cmp } from '../string/module.f.mjs' import type { List, Result } from '../list/types.ts' import { next, toArray } from '../list/module.f.mjs' diff --git a/fjs/types/btree/remove/proof.f.ts b/fjs/types/btree/remove/proof.f.ts index 7f03924b2..967e89f2f 100644 --- a/fjs/types/btree/remove/proof.f.ts +++ b/fjs/types/btree/remove/proof.f.ts @@ -3,7 +3,7 @@ import type { TNode } from '../types/types.ts' import { set as setSet } from '../set/module.f.mjs' import { cmp } from '../../string/module.f.mjs' import { stringify } from '../../../media/json/module.f.ts' -import { sort } from '../../object/module.f.ts' +import { sort } from '../../object/module.f.mjs' import { assertEq, assertNotNullish } from '../../../asserts/module.f.mjs' const set = (node: TNode) => (value: string) => diff --git a/fjs/types/btree/set/proof.f.ts b/fjs/types/btree/set/proof.f.ts index 57148260f..5582926a4 100644 --- a/fjs/types/btree/set/proof.f.ts +++ b/fjs/types/btree/set/proof.f.ts @@ -2,7 +2,7 @@ import { set as setSet } from './module.f.mjs' import type { TNode } from '../types/types.ts' import { cmp } from '../../string/module.f.mjs' import { stringify } from '../../../media/json/module.f.ts' -import { sort } from '../../object/module.f.ts' +import { sort } from '../../object/module.f.mjs' import { assertEq } from '../../../asserts/module.f.mjs' const set = (node: TNode) => (value: string): TNode => diff --git a/fjs/types/byte_set/module.f.mjs b/fjs/types/byte_set/module.f.mjs new file mode 100644 index 000000000..2033f067a --- /dev/null +++ b/fjs/types/byte_set/module.f.mjs @@ -0,0 +1,66 @@ +/** + * Compact byte-set operations and predicates. See `./types.ts` for the + * `ByteSet` type. + * + * @module + */ +import { compose } from '../function/module.f.mjs' +/** @import { RangeMap } from '../range_map/types.ts' */ +/** @import { SortedSet } from '../sorted_set/types.ts' */ +import { reverse, countdown, flat, map } from '../list/module.f.mjs' +/** @import { ByteSet } from './types.ts' */ + +/** @typedef {number} _Byte */ + +/** @type {(n: _Byte) => (s: ByteSet) => boolean} */ +export const has = n => s => ((s >> BigInt(n)) & 1n) === 1n + +// create a set + +export const empty = 0n + +// 0 1 2 3 4 5 6 7 8 9 A B C D E F +export const universe = 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFFn + +/** @type {(n: _Byte) => ByteSet} */ +export const one = n => 1n << BigInt(n) + +/** @type {(r: readonly [_Byte, _Byte]) => ByteSet} */ +export const range = ([b, e]) => one(e - b + 1) - 1n << BigInt(b) + +// set operations + +/** @type {(a: ByteSet) => (b: ByteSet) => ByteSet} */ +export const union = a => b => a | b + +/** @type {(a: ByteSet) => (b: ByteSet) => ByteSet} */ +const intersect = a => b => a & b + +/** @type {(n: ByteSet) => ByteSet} */ +export const complement = n => universe ^ n + +/** @type {(a: ByteSet) => (b: ByteSet) => ByteSet} */ +const difference = compose(intersect)(compose(complement)) + +// additional operations + +/** @type {(_: number) => (b: ByteSet) => ByteSet} */ +export const set = compose(one)(union) + +/** @type {(_: readonly [number, number]) => (b: ByteSet) => ByteSet} */ +export const setRange = compose(range)(union) + +/** @type {(n: _Byte) => (s: ByteSet) => ByteSet} */ +export const unset = n => s => difference(s)(one(n)) + +const counter = reverse(countdown(256)) + +/** @type {(n: ByteSet) => (s: string) => (i: number) => RangeMap>} */ +const toRangeMapOp = n => s => i => { + const current = has(i + 1)(n) + const prev = has(i)(n) + return current === prev ? null : [[prev ? [s] : [], i]] +} + +/** @type {(n: ByteSet) => (s: string) => RangeMap>} */ +export const toRangeMap = n => s => flat(map(toRangeMapOp(n)(s))(counter)) diff --git a/fjs/types/byte_set/module.f.ts b/fjs/types/byte_set/module.f.ts deleted file mode 100644 index 4c0fbf5e0..000000000 --- a/fjs/types/byte_set/module.f.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Compact byte-set operations and predicates. - * - * @module - */ -import { compose } from '../function/module.f.mjs' -import type { RangeMap } from '../range_map/types.ts' -import type { SortedSet } from '../sorted_set/module.f.ts' -import { reverse, countdown, flat, map } from '../list/module.f.mjs' - -export type ByteSet = bigint -type Byte = number - -export const has: (n: Byte) => (s: ByteSet) => boolean - = n => s => ((s >> BigInt(n)) & 1n) === 1n - -// create a set - -export const empty = 0n - -// 0 1 2 3 4 5 6 7 8 9 A B C D E F -export const universe = 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFFn - -export const one: (n: Byte) => ByteSet - = n => 1n << BigInt(n) - -export const range: (r: readonly[Byte, Byte]) => ByteSet - = ([b, e]) => one(e - b + 1) - 1n << BigInt(b) - -// set operations - -export const union: (a: ByteSet) => (b: ByteSet) => ByteSet - = a => b => a | b - -const intersect: (a: ByteSet) => (b: ByteSet) => ByteSet - = a => b => a & b - -export const complement: (n: ByteSet) => ByteSet - = n => universe ^ n - -const difference: (a: ByteSet) => (b: ByteSet) => ByteSet - = compose(intersect)(compose(complement)) - -// additional operations - -export const set: (_: number) => (b: ByteSet) => ByteSet - = compose(one)(union) - -export const setRange: (_: readonly [number, number]) => (b: ByteSet) => ByteSet - = compose(range)(union) - -export const unset: (n: Byte) => (s: ByteSet) => ByteSet - = n => s => difference(s)(one(n)) - -const counter = reverse(countdown(256)) - -const toRangeMapOp: (n: ByteSet) => (s: string) => (i: number) => RangeMap> - = n => s => i => { - const current = has(i + 1)(n) - const prev = has(i)(n) - return current === prev ? null : [[prev ? [s] : [], i]] - } - -export const toRangeMap: (n: ByteSet) => (s: string) => RangeMap> - = n => s => flat(map(toRangeMapOp(n)(s))(counter)) diff --git a/fjs/types/byte_set/proof.f.ts b/fjs/types/byte_set/proof.f.ts index f8d9e2e16..563aff748 100644 --- a/fjs/types/byte_set/proof.f.ts +++ b/fjs/types/byte_set/proof.f.ts @@ -1,7 +1,7 @@ -import { has, empty, set, setRange, unset, universe, complement, toRangeMap } from './module.f.ts' +import { has, empty, set, setRange, unset, universe, complement, toRangeMap } from './module.f.mjs' import { every, countdown, map, toArray } from '../list/module.f.mjs' import { stringify as jsonStringify, type Unknown } from '../../media/json/module.f.ts' -import { sort } from '../object/module.f.ts' +import { sort } from '../object/module.f.mjs' import { assert, assertEq } from '../../asserts/module.f.mjs' const stringify: (a: readonly Unknown[]) => string diff --git a/fjs/types/byte_set/types.ts b/fjs/types/byte_set/types.ts new file mode 100644 index 000000000..de4ce510e --- /dev/null +++ b/fjs/types/byte_set/types.ts @@ -0,0 +1,7 @@ +/** + * Type-level API for the byte-set module. + * + * @module + */ + +export type ByteSet = bigint diff --git a/fjs/types/list/proof.f.ts b/fjs/types/list/proof.f.ts index ba60716c3..c93e47724 100644 --- a/fjs/types/list/proof.f.ts +++ b/fjs/types/list/proof.f.ts @@ -1,7 +1,7 @@ import type { List } from './types.ts' import { length, concat, countdown, cycle, drop, dropWhile, entries, every, filter, find, flat, flatMap, map, next, reduce, reverse, scan, some, take, takeWhile, toArray, zip, first, filterMap, isEmpty, equal, tryFold } from './module.f.mjs' import { stringify, type Unknown } from '../../media/json/module.f.ts' -import { sort } from '../object/module.f.ts' +import { sort } from '../object/module.f.mjs' import { addition, strictEqual, reduceToScan } from '../function/operator/module.f.mjs' import { assert, assertEq, assertNotNullish } from '../../asserts/module.f.mjs' diff --git a/fjs/types/nibble_set/module.f.mjs b/fjs/types/nibble_set/module.f.mjs index e0f1706d0..1f864a4cb 100644 --- a/fjs/types/nibble_set/module.f.mjs +++ b/fjs/types/nibble_set/module.f.mjs @@ -3,7 +3,7 @@ * * A set of nibbles (values `0..15`) stored as a 16-bit mask in a plain * `number`. It implements the same bitmask-as-set algebra as - * {@link ../byte_set/module.f.ts | `byte_set`}, which tracks all 256 byte + * {@link ../byte_set/module.f.mjs | `byte_set`}, which tracks all 256 byte * values in a `bigint`. * * **Prefer `byte_set`.** Its 256-value universe covers the common cases and diff --git a/fjs/types/object/module.f.mjs b/fjs/types/object/module.f.mjs new file mode 100644 index 000000000..a6060062f --- /dev/null +++ b/fjs/types/object/module.f.mjs @@ -0,0 +1,58 @@ +/** + * Plain-object helpers: safe property lookup via `at`, and conversions + * between entries and `OrderedMap`. See `./types.ts` for the + * `OptionalMap`/`RequiredMap`/`StringMap`/`Entry`/`OneKey`/`SingleProperty`/ + * `NotUnion` type-level API. + * + * @module + */ + +import { isArray } from '../array/module.f.mjs' +import { iterable } from '../list/module.f.mjs' +/** @import { List } from '../list/types.ts' */ +import { fromUndefined } from '../nullable/module.f.mjs' +/** @import { Nullable } from '../nullable/types.ts' */ +import { entries as mapEntries, fromEntries as mapFromEntries } from '../ordered_map/module.f.mjs' +/** @import { OrderedMap } from '../ordered_map/types.ts' */ +/** @import { StringMap, Entry } from './types.ts' */ + +const { getOwnPropertyDescriptor, fromEntries: objectFromEntries } = Object + +/** @type {(name: string) => (object: StringMap) => Nullable>} */ +export const at = name => object => { + const d = getOwnPropertyDescriptor(object, name) + return d === undefined ? null : fromUndefined(d.value) +} + +/** @type {(e: List>) => List>} */ +export const sort = e => mapEntries(mapFromEntries(e)) + +/** @type {(e: List>) => StringMap} */ +export const fromEntries = e => objectFromEntries(iterable(e)) + +/** @type {(m: OrderedMap) => StringMap} */ +export const fromMap = m => fromEntries(mapEntries(m)) + +/** + * @param {unknown} value + * @returns {value is { readonly [k in string]: unknown }} + */ +export const isObject = + value => + typeof value === 'object' && !isArray(value) && value !== null + +const { values, entries } = Object + +/** + * Returns only the defined (non-undefined) values of a partial record. + * + * @type {(map: StringMap>) => readonly Exclude[]} + */ +export const definedValues = + map => + values(map).filter(v => v !== undefined) + +/** @type {(cmd: StringMap>) => readonly (readonly [string, Exclude])[]} */ +export const definedEntries = + cmd => + entries(cmd).flatMap(([a, b]) => b === undefined ? [] : [[a, b]]) diff --git a/fjs/types/object/proof.f.mjs b/fjs/types/object/proof.f.mjs new file mode 100644 index 000000000..afc5722d1 --- /dev/null +++ b/fjs/types/object/proof.f.mjs @@ -0,0 +1,26 @@ +import { at } from './module.f.mjs' +/** @import { OptionalMap, RequiredMap, StringMap } from './types.ts' */ +import { assertEq } from '../../asserts/module.f.mjs' +/** @import { Assert } from '../../asserts/types.ts' */ +/** @import { Equal } from '../ts/types.ts' */ + +/** @typedef {Assert, { readonly [k in string]?: bigint }>>} _StringMapIsOptional */ + +/** @typedef {Assert, { readonly a?: bigint; readonly b?: bigint }>>} _OptionalIsPartial */ + +/** @typedef {Assert, { readonly a: bigint; readonly b: bigint }>>} _RequiredIsRequired */ + +/** @typedef {Assert, never>>} _RequiredOverAnyStringIsNever */ + +export const proof = { + ctor: () => { + const a = {} + const value = at('constructor')(a) + assertEq(value, null) + }, + property: () => { + const a = { constructor: 42 } + const value = at('constructor')(a) + assertEq(value, 42) + } +} diff --git a/fjs/types/object/proof.f.ts b/fjs/types/object/proof.f.ts deleted file mode 100644 index b37859392..000000000 --- a/fjs/types/object/proof.f.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { at } from './module.f.ts' -import type { OptionalMap, RequiredMap, StringMap } from './module.f.ts' -import { assertEq } from '../../asserts/module.f.mjs' -import type { Assert } from '../../asserts/types.ts' -import type { Equal } from '../ts/types.ts' - -type _StringMapIsOptional = Assert, - { readonly[k in string]?: bigint }>> - -type _OptionalIsPartial = Assert, - { readonly a?: bigint; readonly b?: bigint }>> - -type _RequiredIsRequired = Assert, - { readonly a: bigint; readonly b: bigint }>> - -type _RequiredOverAnyStringIsNever = Assert, never>> - -export const proof = { - ctor: () => { - const a = {} - const value = at('constructor')(a) - assertEq(value, null) - }, - property: () => { - const a = { constructor: 42 } - const value = at('constructor')(a) - assertEq(value, 42) - } -} diff --git a/fjs/types/object/todo/defined-values-from-entries.md b/fjs/types/object/todo/defined-values-from-entries.md index 5dfba411e..132a38503 100644 --- a/fjs/types/object/todo/defined-values-from-entries.md +++ b/fjs/types/object/todo/defined-values-from-entries.md @@ -5,7 +5,7 @@ ### Problem -`fjs/types/object/module.f.ts:66-77` expresses "drop the `undefined`-valued +`fjs/types/object/module.f.mjs:66-77` expresses "drop the `undefined`-valued entries" twice, with two different idioms: ```ts diff --git a/fjs/types/object/todo/structurally-same.md b/fjs/types/object/todo/structurally-same.md index cef9b836a..9f9738066 100644 --- a/fjs/types/object/todo/structurally-same.md +++ b/fjs/types/object/todo/structurally-same.md @@ -39,7 +39,7 @@ arbitrary host objects. Accepting `unknown` is useful at assertion and parsing boundaries, but the result only describes the structural rules below. The implementation placement must also avoid an import cycle. The existing -`fjs/types/object/module.f.ts` imports `fjs/types/nullable/module.f.mjs`, which +`fjs/types/object/module.f.mjs` imports `fjs/types/nullable/module.f.mjs`, which imports `fjs/asserts/module.f.mjs`. Therefore, `asserts/module.f.mjs` cannot import the object module to reuse `structurallySame` without creating the runtime cycle `asserts -> object -> nullable -> asserts`. @@ -54,8 +54,8 @@ export const structurallySame = (a: unknown, b: unknown): boolean => ... ``` The leaf must not import `fjs/asserts/module.f.mjs`, -`fjs/types/nullable/module.f.mjs`, or `fjs/types/object/module.f.ts`. -Re-export `structurallySame` from `fjs/types/object/module.f.ts` as the public +`fjs/types/nullable/module.f.mjs`, or `fjs/types/object/module.f.mjs`. +Re-export `structurallySame` from `fjs/types/object/module.f.mjs` as the public object-helper API. Add the corresponding assertion helper to `fjs/asserts/module.f.mjs`, importing @@ -158,7 +158,7 @@ These cases can be added later when a concrete consumer requires them. `fjs/types/object/structurally_same/module.f.ts`. - [ ] Add the co-located `fjs/types/object/structurally_same/proof.f.ts` module, export `proof`, and exercise every branch of `structurallySame` there. -- [ ] Re-export `structurallySame` from `fjs/types/object/module.f.ts`. +- [ ] Re-export `structurallySame` from `fjs/types/object/module.f.mjs`. - [ ] Add `assertStructurallySame` to `fjs/asserts/module.f.mjs`, importing `structurallySame` directly from the leaf module. - [ ] Verify that the change does not introduce the @@ -191,7 +191,7 @@ These cases can be added later when a concrete consumer requires them. ## Related -- [`fjs/types/object/module.f.ts`](../module.f.ts) — public object-helper module; +- [`fjs/types/object/module.f.mjs`](../module.f.mjs) — public object-helper module; it currently depends on `types/nullable`. - [`fjs/types/nullable/module.f.mjs`](../../nullable/module.f.mjs) — imports the assertion module, which makes importing the public object module from assertions diff --git a/fjs/types/object/module.f.ts b/fjs/types/object/types.ts similarity index 50% rename from fjs/types/object/module.f.ts rename to fjs/types/object/types.ts index dc6f21414..4d0c3d0d9 100644 --- a/fjs/types/object/module.f.ts +++ b/fjs/types/object/types.ts @@ -1,23 +1,11 @@ /** - * Plain-object helpers and types: the `OptionalMap`/`RequiredMap`/`StringMap` - * record shapes and `Entry`, safe property lookup via `at`, conversions - * between entries and `OrderedMap`, and the `OneKey`/`SingleProperty`/`NotUnion` + * Types for plain-object helpers: the `OptionalMap`/`RequiredMap`/`StringMap` + * record shapes and `Entry`, and the `OneKey`/`SingleProperty`/`NotUnion` * utility types. * * @module */ -import type { List } from '../list/types.ts' -import type { Nullable } from '../nullable/types.ts' -import type { OrderedMap } from '../ordered_map/types.ts' - -import { isArray } from '../array/module.f.mjs' -import { iterable } from '../list/module.f.mjs' -import { fromUndefined } from '../nullable/module.f.mjs' -import { entries as mapEntries, fromEntries as mapFromEntries } from '../ordered_map/module.f.mjs' - -const { getOwnPropertyDescriptor, fromEntries: objectFromEntries } = Object - /** A record over the keys of `K`, each value possibly missing at runtime. */ export type OptionalMap = { readonly[k in K]?: T } @@ -46,21 +34,6 @@ export type StringMap = OptionalMap export type Entry = readonly[string, T] -export const at: (name: string) => (object: StringMap) => Nullable> - = name => object => { - const d = getOwnPropertyDescriptor(object, name) - return d === undefined ? null : fromUndefined(d.value) - } - -export const sort: (e: List>) => List> - = e => mapEntries(mapFromEntries(e)) - -export const fromEntries: (e: List>) => StringMap - = e => objectFromEntries(iterable(e)) - -export const fromMap: (m: OrderedMap) => StringMap - = m => fromEntries(mapEntries(m)) - /** * A set of objects with a single key. * @@ -85,18 +58,3 @@ export type NotUnion = export type SingleProperty> = keyof T extends NotUnion ? T : never; - -export const isObject = - (value: unknown): value is { readonly[k in string]: unknown } => - typeof value === 'object' && !isArray(value) && value !== null - -const { values, entries } = Object - -/** Returns only the defined (non-undefined) values of a partial record. */ -export const definedValues = - (map: StringMap>): readonly Exclude[] => - values(map).filter(v => v !== undefined) - -export const definedEntries = - (cmd: StringMap>): readonly (readonly[string, Exclude])[] => - entries(cmd).flatMap(([a, b]) => b === undefined ? [] : [[a, b]]) diff --git a/fjs/types/range_map/proof.f.ts b/fjs/types/range_map/proof.f.ts index 13c3aa268..2f33d95ca 100644 --- a/fjs/types/range_map/proof.f.ts +++ b/fjs/types/range_map/proof.f.ts @@ -1,8 +1,9 @@ import type { RangeMapArray, Properties, RangeMap } from './types.ts' import { get, merge, fromRange, rangeMap } from './module.f.mjs' import { stringify, type Unknown } from '../../media/json/module.f.ts' -import { sort } from '../object/module.f.ts' -import { union, type SortedSet } from '../sorted_set/module.f.ts' +import { sort } from '../object/module.f.mjs' +import { union } from '../sorted_set/module.f.mjs' +import type { SortedSet } from '../sorted_set/types.ts' import { equal, toArray } from '../list/module.f.mjs' import { strictEqual } from '../function/operator/module.f.mjs' import { cmp } from '../string/module.f.mjs' diff --git a/fjs/types/rtti/common/module.f.ts b/fjs/types/rtti/common/module.f.ts index 6f337e2b0..1511d0b4e 100644 --- a/fjs/types/rtti/common/module.f.ts +++ b/fjs/types/rtti/common/module.f.ts @@ -41,7 +41,8 @@ import type { Error, Result as CommonResult } from '../../result/types.ts' import { error, ok } from '../../result/module.f.mjs' import type { Ts } from '../ts/module.f.ts' import { isArray as commonIsArray } from '../../array/module.f.mjs' -import { isObject as commonIsObject, type StringMap } from '../../object/module.f.ts' +import { isObject as commonIsObject } from '../../object/module.f.mjs' +import type { StringMap } from '../../object/types.ts' /** A path to a sub-value within the validated structure. Each step is an object key or stringified array index. */ export type Path = readonly string[] diff --git a/fjs/types/rtti/module.f.ts b/fjs/types/rtti/module.f.ts index 4c7998ee1..1f7ab5e12 100644 --- a/fjs/types/rtti/module.f.ts +++ b/fjs/types/rtti/module.f.ts @@ -40,7 +40,7 @@ import type { Assert } from '../../asserts/types.ts' import type { Includes } from '../array/types.ts' import { includes } from '../array/module.f.mjs' import type { Equal } from '../ts/types.ts' -import type { StringMap } from '../object/module.f.ts' +import type { StringMap } from '../object/types.ts' /** A constant schema: a primitive literal, a struct object, or a tuple. */ export type Const = diff --git a/fjs/types/rtti/parse/module.f.ts b/fjs/types/rtti/parse/module.f.ts index a3df0b961..9734b8c56 100644 --- a/fjs/types/rtti/parse/module.f.ts +++ b/fjs/types/rtti/parse/module.f.ts @@ -35,7 +35,7 @@ import { } from '../module.f.ts' import type { Result as CommonResult } from '../../result/types.ts' import { ok } from '../../result/module.f.mjs' -import type { StringMap } from '../../object/module.f.ts' +import type { StringMap } from '../../object/types.ts' import type { List } from '../../list/types.ts' import { reverse, toArray } from '../../list/module.f.mjs' import { diff --git a/fjs/types/rtti/proof.f.ts b/fjs/types/rtti/proof.f.ts index e254b141a..10632409b 100644 --- a/fjs/types/rtti/proof.f.ts +++ b/fjs/types/rtti/proof.f.ts @@ -1,4 +1,4 @@ -import type { StringMap } from '../object/module.f.ts' +import type { StringMap } from '../object/types.ts' type Tests = StringMap diff --git a/fjs/types/rtti/ts/module.f.ts b/fjs/types/rtti/ts/module.f.ts index 23a0b86ce..8dfc6c474 100644 --- a/fjs/types/rtti/ts/module.f.ts +++ b/fjs/types/rtti/ts/module.f.ts @@ -12,7 +12,7 @@ import { primitive, union, printer as tsPrinter } from '../../ts/module.f.mjs' import type { Tag0, Tag1, Const, Or, String as RttiString, Struct, Tuple, Type, ConstObject } from '../module.f.ts' import type { Assert } from '../../../asserts/types.ts' import type { phantomKey } from '../../phantom/types.ts' -import type { StringMap } from '../../object/module.f.ts' +import type { StringMap } from '../../object/types.ts' /** * The set of primitive literal types representable as rtti `Const` values. diff --git a/fjs/types/rtti/validate/module.f.ts b/fjs/types/rtti/validate/module.f.ts index b9ef7da50..652681b8e 100644 --- a/fjs/types/rtti/validate/module.f.ts +++ b/fjs/types/rtti/validate/module.f.ts @@ -36,7 +36,7 @@ import { type Type, } from '../module.f.ts' import { ok } from '../../result/module.f.mjs' -import type { StringMap } from '../../object/module.f.ts' +import type { StringMap } from '../../object/types.ts' import { constPrimitiveValidate, eachEntry, diff --git a/fjs/types/sorted_list/proof.f.ts b/fjs/types/sorted_list/proof.f.ts index 3ad92e2bd..92235a1ae 100644 --- a/fjs/types/sorted_list/proof.f.ts +++ b/fjs/types/sorted_list/proof.f.ts @@ -1,6 +1,6 @@ import { find, merge } from './module.f.mjs' import { stringify, type Unknown } from '../../media/json/module.f.ts' -import { sort } from '../object/module.f.ts' +import { sort } from '../object/module.f.mjs' import { toArray, countdown, length } from '../list/module.f.mjs' import { flip } from '../function/module.f.mjs' import { cmp } from '../number/module.f.mjs' diff --git a/fjs/types/sorted_set/module.f.mjs b/fjs/types/sorted_set/module.f.mjs new file mode 100644 index 000000000..fa08f3c55 --- /dev/null +++ b/fjs/types/sorted_set/module.f.mjs @@ -0,0 +1,56 @@ +/** + * A sorted set, implemented as a sorted array. + * + * @module + * + * @note + * + * All input arrays must be pre-sorted according to the provided comparison function (`Cmp`). + * The correctness of these functions depend on this requirement. + * + * @example + * + * ```js + * import { union, intersect, has } from './module.f.mjs' + * + * const cmp = (a: number) => (b: number) => a < b ? -1 : a > b ? 1 : 0 + * + * const setA = [1, 3, 5] + * const setB = [3, 4, 5] + * + * const unionSet = union(cmp)(setA)(setB) // [1, 3, 4, 5] + * + * const intersectionSet = intersect(cmp)(setA)(setB) // [3, 5] + * + * has(cmp)(3)(setA) // true + * has(cmp)(2)(setA) // false + * ``` + */ +/** @import { Cmp } from '../function/compare/types.ts' */ +import { toArray } from "../list/module.f.mjs" +import { merge, intersect as listIntersect, find } from '../sorted_list/module.f.mjs' +/** @import { SortedSet } from './types.ts' */ + +export const union = + /** + * @template T + * @param {Cmp} cmp + * @returns {(a: SortedSet) => (b: SortedSet) => SortedSet} + */ + cmp => a => b => toArray(merge(cmp)(a)(b)) + +export const intersect = + /** + * @template T + * @param {Cmp} cmp + * @returns {(a: SortedSet) => (b: SortedSet) => SortedSet} + */ + cmp => a => b => toArray(listIntersect(cmp)(a)(b)) + +export const has = + /** + * @template T + * @param {Cmp} cmp + * @returns {(value: T) => (set: SortedSet) => boolean} + */ + cmp => value => set => find(cmp)(value)(set) === value diff --git a/fjs/types/sorted_set/module.f.ts b/fjs/types/sorted_set/module.f.ts deleted file mode 100644 index d24dbd55f..000000000 --- a/fjs/types/sorted_set/module.f.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * A sorted set, implemented as a sorted array. - * - * @module - * - * @note - * - * All input arrays must be pre-sorted according to the provided comparison function (`Cmp`). - * The correctness of these functions depend on this requirement. - * - * @example - * - * ```js - * import { union, intersect, has } from './module.f.ts' - * - * const cmp = (a: number) => (b: number) => a < b ? -1 : a > b ? 1 : 0 - * - * const setA = [1, 3, 5] - * const setB = [3, 4, 5] - * - * const unionSet = union(cmp)(setA)(setB) // [1, 3, 4, 5] - * - * const intersectionSet = intersect(cmp)(setA)(setB) // [3, 5] - * - * has(cmp)(3)(setA) // true - * has(cmp)(2)(setA) // false - * ``` - */ -import type { Cmp } from '../function/compare/types.ts' -import { toArray } from "../list/module.f.mjs" -import { merge, intersect as listIntersect, find } from '../sorted_list/module.f.mjs' - -export type SortedSet = readonly T[] - -export const union: (cmp: Cmp) => (a: SortedSet) => (b: SortedSet) => SortedSet - = cmp => a => b => toArray(merge(cmp)(a)(b)) - -export const intersect: (cmp: Cmp) => (a: SortedSet) => (b: SortedSet) => SortedSet - = cmp => a => b => toArray(listIntersect(cmp)(a)(b)) - -export const has: (cmp: Cmp) => (value: T) => (set: SortedSet) => boolean - = cmp => value => set => find(cmp)(value)(set) === value diff --git a/fjs/types/sorted_set/proof.f.ts b/fjs/types/sorted_set/proof.f.ts index 799e496e1..4b429c30c 100644 --- a/fjs/types/sorted_set/proof.f.ts +++ b/fjs/types/sorted_set/proof.f.ts @@ -1,6 +1,6 @@ -import { has, intersect, union } from './module.f.ts' +import { has, intersect, union } from './module.f.mjs' import { stringify, type Unknown } from '../../media/json/module.f.ts' -import { sort } from '../object/module.f.ts' +import { sort } from '../object/module.f.mjs' import { toArray, countdown, length } from '../list/module.f.mjs' import { flip } from '../function/module.f.mjs' import { cmp } from '../number/module.f.mjs' diff --git a/fjs/types/sorted_set/types.ts b/fjs/types/sorted_set/types.ts new file mode 100644 index 000000000..f2583cc3d --- /dev/null +++ b/fjs/types/sorted_set/types.ts @@ -0,0 +1,7 @@ +/** + * Type-level API for the sorted-set module. + * + * @module + */ + +export type SortedSet = readonly T[] diff --git a/fjs/types/string_set/module.f.ts b/fjs/types/string_set/module.f.mjs similarity index 55% rename from fjs/types/string_set/module.f.ts rename to fjs/types/string_set/module.f.mjs index 1efec81cb..a0c072589 100644 --- a/fjs/types/string_set/module.f.ts +++ b/fjs/types/string_set/module.f.mjs @@ -1,12 +1,13 @@ /** - * A set of strings implemented as a B-Tree. + * A set of strings implemented as a B-Tree. See `./types.ts` for the + * `StringSet` type. * * @module * * @example * * ```js - * import { set, contains, remove, fromValues, values, empty } from './module.d.ts'; + * import { set, contains, remove, fromValues, values, empty } from './module.f.mjs'; * * let mySet = fromValues(['apple', 'banana', 'cherry']); * if (!contains('banana')(mySet)) { throw '1' } @@ -20,32 +21,33 @@ * ``` */ -import type { Tree } from '../btree/types/types.ts' import { empty as btEmpty, values as btValues } from '../btree/module.f.mjs' import { find, isFound } from '../btree/find/module.f.mjs' import { remove as btreeRemove } from '../btree/remove/module.f.mjs' import { set as btreeSet } from '../btree/set/module.f.mjs' import { cmp } from "../string/module.f.mjs" -import type { List } from '../list/types.ts' +/** @import { List } from '../list/types.ts' */ import { fold } from '../list/module.f.mjs' import { compose } from '../function/module.f.mjs' +/** @import { StringSet } from './types.ts' */ -export const values: (s: StringSet) => List = btValues -export const empty: null = btEmpty +/** @type {(s: StringSet) => List} */ +export const values = btValues +/** @type {null} */ +export const empty = btEmpty -export type StringSet = Tree - -export const contains: (value: string) => (set: StringSet) => boolean - = value => { +/** @type {(value: string) => (set: StringSet) => boolean} */ +export const contains = + value => { const f = find(cmp(value)) return s => s !== null && isFound(f(s).first) } -export const set: (value: string) => (s: StringSet) => StringSet - = value => btreeSet(cmp(value))(() => value) +/** @type {(value: string) => (s: StringSet) => StringSet} */ +export const set = value => btreeSet(cmp(value))(() => value) -export const fromValues: (input: List) => StringSet - = fold(set)(null) +/** @type {(input: List) => StringSet} */ +export const fromValues = fold(set)(null) -export const remove: (value: string) => (s: StringSet) => StringSet - = compose(cmp)(btreeRemove) +/** @type {(value: string) => (s: StringSet) => StringSet} */ +export const remove = compose(cmp)(btreeRemove) diff --git a/fjs/types/string_set/proof.f.ts b/fjs/types/string_set/proof.f.mjs similarity index 95% rename from fjs/types/string_set/proof.f.ts rename to fjs/types/string_set/proof.f.mjs index 155a1f5cd..8a17f751e 100644 --- a/fjs/types/string_set/proof.f.ts +++ b/fjs/types/string_set/proof.f.mjs @@ -1,4 +1,4 @@ -import { contains, fromValues, remove, set } from './module.f.ts' +import { contains, fromValues, remove, set } from './module.f.mjs' import { assert, assertEq } from '../../asserts/module.f.mjs' export const proof = { diff --git a/fjs/types/string_set/types.ts b/fjs/types/string_set/types.ts new file mode 100644 index 000000000..1cb27e79f --- /dev/null +++ b/fjs/types/string_set/types.ts @@ -0,0 +1,9 @@ +/** + * Type-level API for the string-set module. + * + * @module + */ + +import type { Tree } from '../btree/types/types.ts' + +export type StringSet = Tree diff --git a/fjs/types/todo/161.md b/fjs/types/todo/161.md index c3902f1d7..d7c859380 100644 --- a/fjs/types/todo/161.md +++ b/fjs/types/todo/161.md @@ -3,7 +3,7 @@ **Priority:** P3 **Status:** open -`fjs/types/string_set/module.f.ts` and `fjs/types/ordered_map/module.f.ts` are +`fjs/types/string_set/module.f.mjs` and `fjs/types/ordered_map/module.f.ts` are parallel thin wrappers over the same B-tree primitives (`btree/find`, `btree/set`, `btree/remove`, `btree/module`) keyed by the same string comparator (`string/module.f.ts` `cmp`). A set is logically a map whose diff --git a/fjs/types/todo/185.md b/fjs/types/todo/185.md index 7566d7986..45d47a635 100644 --- a/fjs/types/todo/185.md +++ b/fjs/types/todo/185.md @@ -6,11 +6,11 @@ `byte_set` hand-rolls contiguous bit masks that already exist as `bigint.mask`. ```ts -// fjs/types/byte_set/module.f.ts:24 +// fjs/types/byte_set/module.f.mjs:24 export const one: (n: Byte) => ByteSet = n => 1n << BigInt(n) -// fjs/types/byte_set/module.f.ts:27 +// fjs/types/byte_set/module.f.mjs:27 export const range: (r: readonly[Byte, Byte]) => ByteSet = ([b, e]) => one(e - b + 1) - 1n << BigInt(b) ``` @@ -63,7 +63,7 @@ the degenerate mask — but the clear win is `range`. parses as `(one(e-b+1) - 1n) << BigInt(b)`, which is exactly `mask(...) << BigInt(b)`, so the rewrite is behavior-preserving. The existing `byte_set` tests should confirm this. -- `has` (`byte_set/module.f.ts:15`, `((s >> BigInt(n)) & 1n) === 1n`) has no +- `has` (`byte_set/module.f.mjs:15`, `((s >> BigInt(n)) & 1n) === 1n`) has no existing `bigint` equivalent; leave it as is — this issue is only about the `mask` duplication. - Unrelated to the dead `nibble_set` of [i160](../../issues/README.md). diff --git a/fjs/types/todo/bit-set-factory.md b/fjs/types/todo/bit-set-factory.md index 1df2109e5..ff46c4b42 100644 --- a/fjs/types/todo/bit-set-factory.md +++ b/fjs/types/todo/bit-set-factory.md @@ -5,7 +5,7 @@ ### Problem -`fjs/types/nibble_set/module.f.mjs` and `fjs/types/byte_set/module.f.ts` +`fjs/types/nibble_set/module.f.mjs` and `fjs/types/byte_set/module.f.mjs` implement the same bitmask-as-set algebra, line for line, differing only in the numeric domain (`number` with `universe = 0xFFFF` vs `bigint` with the 256-bit universe) and the `BigInt(n)` shift-operand conversion. The diff --git a/todo/camel-case-proof-keys.md b/todo/camel-case-proof-keys.md index b03b080f1..3c52a9f7d 100644 --- a/todo/camel-case-proof-keys.md +++ b/todo/camel-case-proof-keys.md @@ -11,8 +11,8 @@ camelCase everywhere else. 48 keys across 7 files are snake_case instead: | file | count | | --- | --- | -| `fjs/sul/id/proof.f.ts` | 18 | -| `fjs/sul/level/hash/proof.f.ts` | 11 | +| `fjs/sul/id/proof.f.mjs` | 18 | +| `fjs/sul/level/hash/proof.f.mjs` | 11 | | `nanvm-lib/tests/proof.f.ts` | 6 | | `fjs/sul/proof.f.ts` | 5 | | `fjs/types/bit_vec/proof.f.ts` | 4 | diff --git a/todo/lang/1010-undefined-property.md b/todo/lang/1010-undefined-property.md index 2d9cef48b..980d54bcc 100644 --- a/todo/lang/1010-undefined-property.md +++ b/todo/lang/1010-undefined-property.md @@ -39,4 +39,4 @@ the entire class at the language level. - [undefined](./2310-undefined.md) — the `undefined` value in DJS - [built-in](./2360-built-in.md) — `Object.entries` / `Object.values` side-effect table -- `fjs/types/object/module.f.ts` — `definedEntries` and `definedValues` helpers +- `fjs/types/object/module.f.mjs` — `definedEntries` and `definedValues` helpers