Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ history.

## Unreleased

- **BREAKING CHANGES:** Implementation-only JSDoc typedefs in the migrated
`fjs/types/array` and `fjs/types/bit_vec` modules are renamed to their private
`_`-prefixed forms. Public declarations keep the same expanded types
[#1462](https://github.com/functionalscript/functionalscript/pull/1462)
- **BREAKING CHANGES:** `fjs/types/bit_vec` migrates 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
Expand Down
4 changes: 2 additions & 2 deletions fjs/common/monoid/todo/balanced-fold.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Hence the result is `null` **iff** the total length exceeds `maxLength`,
independent of grouping — so the balanced fold's reassociation is safe.
`bit_vec.tryListToVec` then becomes the generic balanced `fold` over this
monoid, with `null` meaning "overflowed"; the `len` bookkeeping and the
`Accumulator` / `ListToVecState` machinery go away.
`Accumulator` / `_ListToVecState` machinery go away.

Accepted trade-off: unlike today's `unpackListToVec`, this does **not** break
out the moment the cap is crossed — the fold runs to completion and `null`
Expand All @@ -115,7 +115,7 @@ finishing a walk over an already-doomed list.
`number.sum`.
- [ ] Re-express `bit_vec`'s `tryListToVec` as the balanced `fold` over a
`Nullable<Unpacked>` monoid whose operation is null-absorbing and caps at
`maxLength`; delete the `Accumulator` / `ListToVecState` bookkeeping.
`maxLength`; delete the `Accumulator` / `_ListToVecState` bookkeeping.
- [ ] Update any proof asserting an exact `number.sum` value to the new result;
confirm `bigint` / `string` / `bit_vec` results are unchanged.
- [ ] Run `npx tsc` and `fjs t`.
Expand Down
13 changes: 10 additions & 3 deletions fjs/fsc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,16 @@ export type Public = readonly [_Internal]
The emitted private alias is still private, but the expanded public contract of
`Public` changed from `readonly [number]` to `readonly [string]`.

Public typedefs keep ordinary names without the `_` prefix. When upstream
support is ready, replace this workaround with `@internal`; that cleanup is
tracked by
Public typedefs keep ordinary names without the `_` prefix. Which typedefs are
public is an API design decision, not a mechanical restatement of what the
pre-migration `.f.ts` file happened to export: a helper that belongs to the
module's public vocabulary may be published under an ordinary name even though
its TypeScript alias was module-private, and a former export may become `_` when
it only ever described an implementation detail. Decide per typedef what the
Comment on lines +115 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark reclassifying former exports as breaking

When a typedef was explicitly exported from the pre-migration .f.ts, consumers were entitled to import it; changing it to an _ name makes that API private under this README's own convention. This guidance presents such reclassification as an ordinary per-typedef choice without requiring breaking-change treatment, so a future migration could silently remove supported imports. Qualify the former-export case as a breaking API change that needs the corresponding changelog entry and importer updates.

AGENTS.md reference: AGENTS.md:L965-L973

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving to .mjs is a breaking change anyway, so we have an opportunity to fix what was broken before. We are only discussing future changes in .mjs types.

module should offer its consumers, then apply the prefix accordingly.

When upstream support is ready, replace this workaround with `@internal`; that
cleanup is tracked by
[`todo/blocked/jsdoc-typedef-strip-internal.md`](../../todo/blocked/jsdoc-typedef-strip-internal.md).

When the last authored `.ts` / `.f.ts` file is gone, remove the
Expand Down
8 changes: 4 additions & 4 deletions fjs/types/array/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,24 @@ export const isArray = value => value instanceof Array
* @template {number} N
* @template T
* @template {readonly T[]} R
* @typedef {N extends R['length'] ? R : TupleX<N, T, readonly[...R, T]>} TupleX
* @typedef {N extends R['length'] ? R : _Tuple<N, T, readonly[...R, T]>} _Tuple
*/

/**
* @template {number} N
* @template {readonly unknown[]} R
* @typedef {R['length'] extends N ? never : R['length'] | IndexX<N, readonly[...R, unknown]>} IndexX
* @typedef {R['length'] extends N ? never : R['length'] | _Index<N, readonly[...R, unknown]>} _Index
*/

/**
* @template {number} N
* @typedef {number extends N ? number : N extends number ? IndexX<N, readonly[]> : never} Index
* @typedef {number extends N ? number : N extends number ? _Index<N, readonly[]> : never} Index
*/

/**
* @template {number} N
* @template T
* @typedef {TupleX<N, T, readonly[]>} Tuple
* @typedef {_Tuple<N, T, readonly[]>} Tuple
*/

/**
Expand Down
32 changes: 16 additions & 16 deletions fjs/types/bit_vec/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ import { cmp, max, min } from '../function/compare/module.f.mjs'
import { mapUnwrap } from '../nullable/module.f.mjs'
/** @import { Nullable } from '../nullable/module.f.mjs' */

/** @typedef {'1a23a4336197e6158b6936cad34e90d146cd84b9b40ff7ab75a17c6d79e31d89'} Revision */
/** @typedef {'1a23a4336197e6158b6936cad34e90d146cd84b9b40ff7ab75a17c6d79e31d89'} _Revision */

/**
* A vector of bits represented as a signed `bigint`.
*
* @typedef {Nominal<'bit_vec', Revision, bigint>} Vec
* @typedef {Nominal<'bit_vec', _Revision, bigint>} Vec
*/

/**
Expand Down Expand Up @@ -182,17 +182,17 @@ export const unpackedUint = ({ uint }) => uint
* @typedef {(len: bigint) => {
* readonly a: bigint
* readonly b: bigint
* }} Norm
* }} _Norm
*/

/** @typedef {Binary<Unpacked, Unpacked, Norm>} NormOp */
/** @typedef {Binary<Unpacked, Unpacked, _Norm>} _NormOp */

/** @typedef {OpReduce<Vec>} Reduce */

/**
* Normalizes two vectors to the same length before applying a bigint reducer.
*
* @type {(norm: NormOp) => (op: BigintReduce) => Reduce}
* @type {(norm: _NormOp) => (op: BigintReduce) => Reduce}
*/
const op = norm => op => ap => bp => {
const au = unpack(ap)
Expand Down Expand Up @@ -221,10 +221,10 @@ const op = norm => op => ap => bp => {
* readonly listToVec: (list: List<Vec>) => Vec
* readonly xor: Reduce
* readonly unpackPopFront: PopFront<Unpacked>
* readonly norm: NormOp
* readonly norm: _NormOp
* readonly cmp: (a: Vec) => (b: Vec) => Sign
* readonly unpackSplit: (len: bigint) => (u: Unpacked) => readonly[bigint, bigint]
* readonly unpackConcat: UnpackConcat
* readonly unpackConcat: _UnpackConcat
* readonly startsWith: (prefix: Vec) => (v: Vec) => boolean
* }} BitOrder
*/
Expand All @@ -233,27 +233,27 @@ const op = norm => op => ap => bp => {
* @typedef {{
* readonly front: (len: bigint) => (v: Vec) => bigint
* readonly removeFront: (len: bigint) => (v: Vec) => Vec
* readonly norm: NormOp
* readonly norm: _NormOp
* readonly uintCmp: (a: bigint) => (b: bigint) => Sign
* readonly unpackSplit: (len: bigint) => (u: Unpacked) => readonly[bigint, bigint]
* readonly unpackConcatUint: (a: Unpacked) => (b: Unpacked) => bigint
* }} Base
* }} _Base
*/

const unpackEmpty = /** @type {const} */{ length: 0n, uint: 0n }

/** @typedef {(a: Unpacked) => (b: Unpacked) => Unpacked} UnpackConcat */
/** @typedef {(a: Unpacked) => (b: Unpacked) => Unpacked} _UnpackConcat */

/**
* @typedef {{
* readonly len: bigint
* readonly stack: readonly Unpacked[]
* }} ListToVecState
* }} _ListToVecState
*/

/** @typedef {Accumulator<Unpacked, ListToVecState, Vec>} ListToVecOp */
/** @typedef {Accumulator<Unpacked, _ListToVecState, Vec>} _ListToVecOp */

/** @type {(unpackConcat: UnpackConcat) => ListToVecOp} */
/** @type {(unpackConcat: _UnpackConcat) => _ListToVecOp} */
const listToVecOp = unpackConcat => ({
init: { len: 0n, stack: [] },
update: (v, {len, stack}) => {
Expand Down Expand Up @@ -299,11 +299,11 @@ const listToVecOp = unpackConcat => ({
* and materializes the combined result on demand, such as `StringBuilder`
* (Java, C#) or `strings.Builder` (Go).
*
* @param {UnpackConcat} unpackConcat
* @param {_UnpackConcat} unpackConcat
*/
const unpackListToVec = unpackConcat => tryFold(listToVecOp(unpackConcat))

/** @type {(base: Base) => BitOrder} */
/** @type {(base: _Base) => BitOrder} */
const bo = ({ front, removeFront, norm, uintCmp, unpackSplit, unpackConcatUint }) => {
/** @param {bigint} len */
const unpackPopFront = len => {
Expand All @@ -315,7 +315,7 @@ const bo = ({ front, removeFront, norm, uintCmp, unpackSplit, unpackConcatUint }
return /** @type {const} */([uint & m, { length: v.length - len, uint: rest }])
}
}
/** @type {UnpackConcat} */
/** @type {_UnpackConcat} */
const unpackConcat = a => b => ({
length: a.length + b.length,
uint: unpackConcatUint(a)(b)
Expand Down
2 changes: 1 addition & 1 deletion fjs/types/list/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
/**
* @template T
* @typedef {Result<T> |
* Concat<T> |
* Concat<T> |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep list implementation aliases private

Keeping NotLazy and Concat unprefixed makes them supported public names under the convention added by this commit, but fjs/types/list/todo/simplify-list-type.md:22-26 identifies Concat as internal traversal machinery and NotLazy as an alias that exists only because of it, and lines 47-50 plan to remove both. Publishing them now turns that planned cleanup into an avoidable breaking API change; rename them to _NotLazy and _Concat during this accidental-exposure cleanup.

Useful? React with 👍 / 👎.

@sergey-shandar sergey-shandar Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, Concat should be public.

* readonly T[]
* } NotLazy
*/
Expand Down
49 changes: 44 additions & 5 deletions todo/migrate-typescript-to-mjs.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,30 @@ change. The underscore exempts only the private alias itself, never a change to
the expanded public API. Public typedefs keep ordinary names without a leading
`_`.

Which typedefs are public is an API design decision made at the migration
boundary, not a mechanical copy of what the `.f.ts` happened to export. The
`.f.ts` -> `.f.mjs` rename is already a breaking change — importers must update
the specifier — so it is the one moment where a module's visibility contract can
be corrected at no extra cost to consumers: a former export whose only role was
an implementation detail may become `_`, and a module-private helper that belongs
to the module's public vocabulary may be published under an ordinary name. Such
a correction rides along with the migration's own `**BREAKING CHANGES:**` entry
and does not need a second one.

After a module is `.mjs` its visibility contract is settled, and the convention
then runs in one direction only. Moving a published public typedef to a `_` name
is an ordinary breaking API change from that point on: it needs its own
`**BREAKING CHANGES:**` entry and importer updates, exactly like removing any
other public declaration. Only the migration itself gets the free correction.

A pending refactor is not a reason to pre-privatize. Visibility follows what the
module should offer consumers today, not what a future task plans to delete:
`Concat` and `NotLazy` in `fjs/types/list` stay public even though
[`../fjs/types/list/todo/simplify-list-type.md`](../fjs/types/list/todo/simplify-list-type.md)
plans to remove both. Hiding a type behind `_` to make its eventual removal
cheaper gives up a real present-day API in exchange for a discount on a breaking
change that should simply be documented when it happens.

This convention is temporary. Once TypeScript can strip `@internal` JSDoc
typedefs correctly, replace the underscore workaround as tracked by
[`blocked/jsdoc-typedef-strip-internal.md`](./blocked/jsdoc-typedef-strip-internal.md).
Expand Down Expand Up @@ -251,11 +275,17 @@ compiler-compatibility rename.
compiler support.
- [ ] Translate TypeScript generic constraints and `in` / `out` variance to
JSDoc `@template` syntax without changing assignability.
- [ ] Prefix implementation-only JSDoc typedefs with `_` when converting
non-exported TypeScript types; keep public typedef names unprefixed.
- [ ] Decide each typedef's visibility at the migration boundary: prefix
implementation-only typedefs with `_` and leave publicly useful ones
unprefixed, judged by what the module should offer its consumers rather
than by what the `.f.ts` happened to export or by what a pending refactor
plans to delete.
- [ ] Treat `_`-prefixed typedef names as private even when declarations emit
them as exports, but still require `**BREAKING CHANGES:**` whenever a
change to one alters the assignability of a public declaration.
- [ ] Once a module is `.mjs`, treat any later move of a public typedef to a `_`
name as an ordinary breaking API change with its own changelog entry and
importer updates, not as a visibility cleanup.
- [ ] Continue upward through the dependency graph in reviewable groups until no
authored TypeScript remains.
- [ ] Translate `.ts` to `.mjs` and `.f.ts` to `.f.mjs`, moving static type
Expand All @@ -266,9 +296,12 @@ compiler-compatibility rename.
documentation for every migrated group.
- [ ] Sweep prose references to already-migrated modules: `AGENTS.md`, README
files, and `todo/*.md` still name `.f.ts` paths that no longer exist, so
snippets copied from them produce broken imports and links. Per-group
updates miss these because they are not importers; run a repository-wide
`.f.ts`-reference check at least at the end of stage 1.
snippets copied from them produce broken imports and links. The sweep
covers typedefs a migration renamed as well — `balanced-fold.md` still
calls `bit_vec`'s accumulator state `ListToVecState`, now
`_ListToVecState`. Per-group updates miss both kinds because prose files
are not importers; run a repository-wide `.f.ts`-reference and
renamed-typedef check at least at the end of stage 1.
- [ ] Preserve Node, Deno, Bun, proof, coverage, type-checking, declaration, and
CI package behavior throughout the migration.
- [ ] Add required `**BREAKING CHANGES:**` changelog entries for every public
Expand Down Expand Up @@ -304,6 +337,12 @@ compiler-compatibility rename.
- Renaming or removing an emitted `_`-prefixed alias is not breaking solely due
to that alias being emitted; any resulting change to a public declaration's
assignability is still a breaking change.
- Each migrated module's typedef visibility is justified by the public
vocabulary that module should offer; pre-migration export status is evidence
for that decision, not the decision itself.
- Reclassifying a public typedef as `_` after its module has migrated is treated
as a breaking API change, so the free correction is available only at the
`.f.ts` -> `.f.mjs` boundary.
- `.f.mjs` means FunctionalScript-intent JavaScript, not current-compiler
compatibility.
- Migrated JavaScript never depends on remaining authored TypeScript during the
Expand Down
Loading
Loading