From cb2c2adbf06361c1591580b055b4cd9209d3955e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 07:46:04 +0000 Subject: [PATCH 1/3] types/sorted_list: name the two tail policies keepTail and dropTail The name `tailReduce` was bound twice with opposite meanings -- a local in `merge` that keeps the remaining tail, shadowing a module const that discards it -- so the same identifier at two call sites meant contradictory things. Rename them for what they do and pass each explicitly, leaving `merge` and `intersect` as the same one-line shape differing only in `reduceOp` and tail policy. Dropping the local also meant dropping its `TailReduce` annotation, which turned out to be load-bearing: an explicit generic annotation on a tail policy gives `genericMerge` a second inference candidate for `T` whose best common supertype with `reduceOp`'s is `unknown`, widening `T` and breaking both call sites. Leaving both policies to inference removes the candidate, so `reduceOp` alone fixes `T` -- and matches AGENTS.md 6.2 on preferring inference for private constants. Recorded in a comment where the annotation used to be. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs --- .../todo/uncurry-accumulator-types.md | 2 +- fjs/types/sorted_list/module.f.mjs | 28 +++++++----- .../sorted_list/todo/tail-reduce-shadowing.md | 44 ------------------- todo/migrate-typescript-to-mjs.md | 2 +- 4 files changed, 20 insertions(+), 56 deletions(-) delete mode 100644 fjs/types/sorted_list/todo/tail-reduce-shadowing.md diff --git a/fjs/types/function/todo/uncurry-accumulator-types.md b/fjs/types/function/todo/uncurry-accumulator-types.md index 468615f6be..912aa04081 100644 --- a/fjs/types/function/todo/uncurry-accumulator-types.md +++ b/fjs/types/function/todo/uncurry-accumulator-types.md @@ -35,7 +35,7 @@ This removes the partial-application footgun and drops one closure allocation pe ### Considerations -- **Broad mechanical refactor.** ~20+ operator definitions across `bigint`, `prime_field`, `bit_vec`, `string`, `monoid`, `number`, `range_map`, plus `operator` itself and the `foldToScan`/`reduceToScan`/`fold`/`reduce` plumbing in `list`. `genericMerge`/`cmpReduce`/`mergeTail` in `sorted_list` and the `range_map` merge consumers change accordingly. +- **Broad mechanical refactor.** ~20+ operator definitions across `bigint`, `prime_field`, `bit_vec`, `string`, `monoid`, `number`, `range_map`, plus `operator` itself and the `foldToScan`/`reduceToScan`/`fold`/`reduce` plumbing in `list`. `genericMerge`/`cmpReduce`/`keepTail` in `sorted_list` and the `range_map` merge consumers change accordingly. - **`Fold` can no longer be `Binary`.** This draws a clean line between combinators where currying is genuinely useful (`Binary`/`Equal`/`Unary`) and accumulators where currying is dangerous (`Fold`/`Reduce`/`StateScan`/`ReduceOp`/`TailReduce`). - Could be split: `Fold`/`Reduce` first, `sorted_list`'s `ReduceOp`/`TailReduce` as a follow-up. diff --git a/fjs/types/sorted_list/module.f.mjs b/fjs/types/sorted_list/module.f.mjs index 092eb8e416..cb3d814252 100644 --- a/fjs/types/sorted_list/module.f.mjs +++ b/fjs/types/sorted_list/module.f.mjs @@ -9,7 +9,7 @@ import { bsearch } from '../function/compare/module.f.mjs' import { next } from '../list/module.f.mjs' /** @import { List } from '../list/types.ts' */ import { identity } from '../function/module.f.mjs' -/** @import { ReduceOp, SortedList, TailReduce, _MergeReduce } from './types.ts' */ +/** @import { ReduceOp, SortedList, _MergeReduce } from './types.ts' */ /** @template T @typedef {readonly T[]} _SortedArray */ @@ -53,11 +53,7 @@ export const merge = * @param {Cmp} cmp * @returns {(a: SortedList) => (b: SortedList) => SortedList} */ - cmp => { - /** @type {TailReduce} */ - const tailReduce = mergeTail - return genericMerge({ reduceOp: cmpReduce(cmp), tailReduce })(null) - } + cmp => genericMerge({ reduceOp: cmpReduce(cmp), tailReduce: keepTail })(null) const cmpReduce = /** @@ -70,10 +66,22 @@ const cmpReduce = return [sign === 1 ? b : a, sign, null] } -/** @type {() => (tail: List) => List} */ -const mergeTail = () => identity +/** + * The two tail policies `genericMerge` takes, named for what they do with the + * list that has not been exhausted yet: `merge` keeps it, because everything + * still there belongs in the union, and `intersect` drops it, because with one + * side exhausted nothing remaining can be matched. + * + * Both are left to inference deliberately. An explicit + * `() => (tail: List) => List` compiles here but introduces its own + * type parameter, which `genericMerge` then collects as an inference candidate + * alongside `reduceOp`'s; the best common supertype of the two is `unknown`, so + * `T` widens and both call sites fail to match their declared return type. With + * no annotation there is no second candidate and `reduceOp` alone fixes `T`. + */ +const keepTail = () => identity -const tailReduce = () => () => null +const dropTail = () => () => null const intersectReduce = /** @@ -92,7 +100,7 @@ export const intersect = * @param {Cmp} cmp * @returns {(a: SortedList) => (b: SortedList) => SortedList} */ - cmp => genericMerge({ reduceOp: intersectReduce(cmp), tailReduce })(null) + cmp => genericMerge({ reduceOp: intersectReduce(cmp), tailReduce: dropTail })(null) export const find = /** diff --git a/fjs/types/sorted_list/todo/tail-reduce-shadowing.md b/fjs/types/sorted_list/todo/tail-reduce-shadowing.md deleted file mode 100644 index 71396c4c54..0000000000 --- a/fjs/types/sorted_list/todo/tail-reduce-shadowing.md +++ /dev/null @@ -1,44 +0,0 @@ -## Two opposite `tailReduce`s share one name - -**Priority:** P3 -**Status:** open - -### Problem - -`module.f.mjs` binds the name `tailReduce` twice with contradictory meanings, -one shadowing the other: - -```js -export const merge = cmp => { - /** @type {TailReduce} */ - const tailReduce = mergeTail // :58 — keeps the remaining tail - return genericMerge({ reduceOp: cmpReduce(cmp), tailReduce })(null) -} -const mergeTail = () => identity // :74 -const tailReduce = () => () => null // :76 — discards the tail -export const intersect = cmp => - genericMerge({ reduceOp: intersectReduce(cmp), tailReduce })(null) // :95 -``` - -A reader at `:59` and a reader at `:95` see the same identifier meaning -opposite tail policies. The local binding exists only to carry a JSDoc -annotation; it captures nothing, so §6.3 says hoist it. - -### Proposal - -Rename the two to say what they do (`keepTail` / `dropTail`), annotate -`keepTail` at module scope, and delete the shadowing local. `merge` and -`intersect` then read as the same shape differing only in `reduceOp` and -tail policy. - -### Tasks - -- [ ] Rename `mergeTail` → `keepTail` (with the module-scope annotation) and - the module-level `tailReduce` → `dropTail` -- [ ] Remove the shadowing local in `merge` - -### Related - -- [66b-sorted-list-cmp-reduce-factory](../../todo/66b-sorted-list-cmp-reduce-factory.md) - — covers `cmpReduce` vs `intersectReduce`; this issue covers the tail - policies diff --git a/todo/migrate-typescript-to-mjs.md b/todo/migrate-typescript-to-mjs.md index f0689418e5..449fa1a6b1 100644 --- a/todo/migrate-typescript-to-mjs.md +++ b/todo/migrate-typescript-to-mjs.md @@ -558,7 +558,7 @@ data. A related mechanical finding from the same review round: composing multiple independently-generic helper functions inside another generic function's body -(e.g. `genericMerge` calling `cmpReduce` calling into `mergeTail`, all +(e.g. `genericMerge` calling `cmpReduce` calling into `keepTail`, all separately ``-generic) loses type inference when each is annotated with a single `@type {(...) => ...}` on the whole arrow chain — TypeScript cannot always unify the type parameters across the nested generic-value calls, From 7376d128f5703e4d3c7dfadd4033bfe691bf572e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 07:47:00 +0000 Subject: [PATCH 2/3] CHANGELOG: entry for the sorted_list tail-policy rename (#1546) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d18faec5bb..fbb60dff40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ history. ## Unreleased +- `types/sorted_list`: the two merge tail policies are now named `keepTail` and + `dropTail` instead of sharing one shadowed name; internal only + [#1546](https://github.com/functionalscript/functionalscript/pull/1546) - `types/uint8array`: `toVec` attempts the conversion instead of precomputing a byte-count bound; behavior and error message unchanged [#1543](https://github.com/functionalscript/functionalscript/pull/1543) From 2dac820641e73a44011ce3924f1df6e9d93122ce Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 13:16:27 +0000 Subject: [PATCH 3/3] types/sorted_list: drop the now-unused TailReduce import The merge-queue resolution of the #1545 @import sweep kept TailReduce in the module header, but this branch removed its last use when the shadowing local in `merge` went away. The type itself stays in types.ts, where range_map still imports it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs --- fjs/types/sorted_list/module.f.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fjs/types/sorted_list/module.f.mjs b/fjs/types/sorted_list/module.f.mjs index 8f9874a0d6..3838149ea2 100644 --- a/fjs/types/sorted_list/module.f.mjs +++ b/fjs/types/sorted_list/module.f.mjs @@ -5,7 +5,7 @@ * * @import { Cmp } from '../function/compare/types.ts' * @import { List } from '../list/types.ts' - * @import { ReduceOp, SortedList, TailReduce, _MergeReduce } from './types.ts' + * @import { ReduceOp, SortedList, _MergeReduce } from './types.ts' */ import { bsearch } from '../function/compare/module.f.mjs'