Skip to content

Commit 59833e1

Browse files
feat: add python bisect harvest 1 (#612)
* feat: add python bisect harvest 1 * docs: clear stale python bisect wishlist * data: sync website upstream bisect inventory --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent c320fe9 commit 59833e1

23 files changed

+3224
-1558
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ Version rationale: `patch` for additive Python runtime expansion without import-
3939

4040
## main
4141

42+
### Expansion
43+
44+
- Added a first `python/bisect` harvest covering `bisect`, `bisect_left`, and `bisect_right`.
45+
4246
Released: TBA. [Diff](https://github.com/locutusjs/locutus/compare/v3.0.32...main).
4347

4448
### Fixes

docs/non-php-api-signatures.snapshot

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,9 @@ src/powershell/string/toupper.ts :: export function toupper(str: string): string
257257
src/powershell/string/trim.ts :: export function trim(str: string): string
258258
src/powershell/string/trimend.ts :: export function trimend(str: string, chars?: string): string
259259
src/powershell/string/trimstart.ts :: export function trimstart(str: string, chars?: string): string
260+
src/python/_helpers/_bisect.ts :: export function pythonBisect(sequence: unknown, item: unknown, lo?: unknown, hi?: unknown): number
261+
src/python/_helpers/_bisect.ts :: export function pythonBisectLeft(sequence: unknown, item: unknown, lo?: unknown, hi?: unknown): number
262+
src/python/_helpers/_bisect.ts :: export function pythonBisectRight(sequence: unknown, item: unknown, lo?: unknown, hi?: unknown): number
260263
src/python/_helpers/_calendar.ts :: export function buildMonthCalendar(year: number, month: number): number[][]
261264
src/python/_helpers/_calendar.ts :: export function buildWeekHeader(width: number): string
262265
src/python/_helpers/_calendar.ts :: export function countLeapYearsBetween(startYear: number, endYear: number): number
@@ -337,6 +340,9 @@ src/python/_helpers/_statistics.ts :: export function toNumericSequence(data: un
337340
src/python/_helpers/_statistics.ts :: export function toStatisticNumber(value: unknown, functionName: string): number
338341
src/python/_helpers/_statistics.ts :: export type StatisticsNumericInput = number | boolean | bigint
339342
src/python/_helpers/_statistics.ts :: export type StatisticsSortableInput = StatisticsNumericInput | string
343+
src/python/bisect/bisect.ts :: export function bisect(a: unknown, x: unknown, lo?: unknown, hi?: unknown): number
344+
src/python/bisect/bisect_left.ts :: export function bisect_left(a: unknown, x: unknown, lo?: unknown, hi?: unknown): number
345+
src/python/bisect/bisect_right.ts :: export function bisect_right(a: unknown, x: unknown, lo?: unknown, hi?: unknown): number
340346
src/python/calendar/calendar.ts :: export function calendar(theyear: unknown, w = 2, l = 1, c = 6, m = 3): string
341347
src/python/calendar/formatstring.ts :: export function formatstring(items: unknown, cols = 20, spacing = 6): string
342348
src/python/calendar/isleap.ts :: export function isleap(year: unknown): boolean

docs/prompts/LOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3219,3 +3219,28 @@ LLMs log key learnings, progress, and next steps in one `### Iteration ${increme
32193219
- Key learnings:
32203220
- QEMU was necessary but not sufficient: prewarming and runtime execution must agree on platform, otherwise Docker silently falls back to an implicit second pull at execution time.
32213221
- `discoverUsesDocker` and `discoverDockerPlatform` together are now part of the discovery contract for platform-specific runtimes like Swift.
3222+
3223+
### Iteration 159
3224+
3225+
2026-03-30
3226+
3227+
- **Area: Product (Python bisect harvest 1)**
3228+
- Plan:
3229+
- Harvest the parity-friendly `python/bisect` surface now that nightly maintenance is green again.
3230+
- Keep the scope to the non-mutating search helpers and avoid `insort*` side effects.
3231+
- Progress:
3232+
- Added `src/python/bisect` with `bisect`, `bisect_left`, and `bisect_right`, backed by a shared `_bisect` helper that mirrors Python 3.12 insertion-point semantics for arrays and strings.
3233+
- Preserved key parity edges around duplicate ranges, `lo`/`hi` bounds, `hi=None`, NaN ordering behavior, and Python-style incomparable-type errors.
3234+
- Updated `src/python/index.ts`, both Rosetta maps, generated bisect tests, website pages, non-PHP API signatures, and type-contract snapshots.
3235+
- Validation:
3236+
- `corepack yarn exec vitest run test/util/python-bisect-harvest-1.vitest.ts`
3237+
- `corepack yarn test:parity python/bisect/bisect python/bisect/bisect_left python/bisect/bisect_right --no-cache`
3238+
- `node scripts/rmrf.ts test/generated && corepack yarn build:tests`
3239+
- `corepack yarn exec vitest run test/generated/python/bisect/*.vitest.ts`
3240+
- `corepack yarn injectweb && corepack yarn website:verify`
3241+
- `corepack yarn fix:api:snapshot:nonphp`
3242+
- `corepack yarn fix:type:contracts`
3243+
- `corepack yarn check`
3244+
- Key learnings:
3245+
- The generated standalone test path will expose symbol collisions that the source/module variants can miss, so alias-like function names need helper internals with distinct names.
3246+
- `bisect` is a good example of a small Python module where strict target-definition work pays off: once the inventory and wishlist are explicit, the harvest becomes a narrow parity exercise rather than guesswork.

docs/upstream-surface-inventory.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,9 +1454,6 @@ python:
14541454
note: Most base64 helpers traffic in bytes rather than plain JS string values.
14551455
bisect:
14561456
title: bisect module
1457-
default:
1458-
decision: wanted
1459-
note: Binary-search helpers are strong plain-value portability targets.
14601457
decisions:
14611458
insort:
14621459
decision: skip_side_effects

src/python/_helpers/_bisect.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { isNumericLike, toPythonInteger, toPythonNumber } from './_operator.ts'
2+
3+
type BisectSequence = unknown[] | string
4+
5+
export function pythonBisect(sequence: unknown, item: unknown, lo?: unknown, hi?: unknown): number {
6+
return bisectCore(sequence, item, lo, hi, 'bisect', 'right')
7+
}
8+
9+
export function pythonBisectLeft(sequence: unknown, item: unknown, lo?: unknown, hi?: unknown): number {
10+
return bisectCore(sequence, item, lo, hi, 'bisect_left', 'left')
11+
}
12+
13+
export function pythonBisectRight(sequence: unknown, item: unknown, lo?: unknown, hi?: unknown): number {
14+
return bisectCore(sequence, item, lo, hi, 'bisect_right', 'right')
15+
}
16+
17+
function bisectCore(
18+
sequence: unknown,
19+
item: unknown,
20+
lo: unknown,
21+
hi: unknown,
22+
functionName: string,
23+
side: 'left' | 'right',
24+
): number {
25+
const values = toBisectSequence(sequence, functionName)
26+
let left = normalizeLo(lo, functionName)
27+
let right = normalizeHi(hi, values.length, functionName)
28+
29+
while (left < right) {
30+
const middle = Math.floor((left + right) / 2)
31+
const candidate = getSequenceItem(values, middle)
32+
33+
if (
34+
side === 'left' ? pythonLessThan(candidate, item, functionName) : !pythonLessThan(item, candidate, functionName)
35+
) {
36+
left = middle + 1
37+
} else {
38+
right = middle
39+
}
40+
}
41+
42+
return left
43+
}
44+
45+
function toBisectSequence(sequence: unknown, functionName: string): BisectSequence {
46+
if (Array.isArray(sequence) || typeof sequence === 'string') {
47+
return sequence
48+
}
49+
50+
throw new TypeError(`${functionName}() expected an indexable sequence`)
51+
}
52+
53+
function normalizeLo(value: unknown, functionName: string): number {
54+
if (value === undefined) {
55+
return 0
56+
}
57+
58+
const index = toSequenceIndex(value, functionName)
59+
if (index < 0) {
60+
throw new RangeError('lo must be non-negative')
61+
}
62+
63+
return index
64+
}
65+
66+
function normalizeHi(value: unknown, sequenceLength: number, functionName: string): number {
67+
if (value === undefined || value === null) {
68+
return sequenceLength
69+
}
70+
71+
return toSequenceIndex(value, functionName)
72+
}
73+
74+
function toSequenceIndex(value: unknown, functionName: string): number {
75+
const index = Number(toPythonInteger(value, functionName))
76+
if (!Number.isSafeInteger(index)) {
77+
throw new RangeError(`${functionName}() index must fit within JS safe integer precision`)
78+
}
79+
80+
return index
81+
}
82+
83+
function getSequenceItem(sequence: BisectSequence, index: number): unknown {
84+
if (index < 0 || index >= sequence.length) {
85+
throw new Error(Array.isArray(sequence) ? 'list index out of range' : 'string index out of range')
86+
}
87+
88+
return sequence[index]
89+
}
90+
91+
function pythonLessThan(left: unknown, right: unknown, functionName: string): boolean {
92+
if (isNumericLike(left) && isNumericLike(right)) {
93+
const leftNumber = toPythonNumber(left, functionName)
94+
const rightNumber = toPythonNumber(right, functionName)
95+
if (Number.isNaN(leftNumber) || Number.isNaN(rightNumber)) {
96+
return false
97+
}
98+
99+
return leftNumber < rightNumber
100+
}
101+
102+
if (typeof left === 'string' && typeof right === 'string') {
103+
return left < right
104+
}
105+
106+
throw new TypeError(`'<' not supported between instances of '${pythonTypeName(left)}' and '${pythonTypeName(right)}'`)
107+
}
108+
109+
function pythonTypeName(value: unknown): string {
110+
if (typeof value === 'string') {
111+
return 'str'
112+
}
113+
114+
if (typeof value === 'boolean') {
115+
return 'bool'
116+
}
117+
118+
if (typeof value === 'bigint' || typeof value === 'number') {
119+
return 'int'
120+
}
121+
122+
if (Array.isArray(value)) {
123+
return 'list'
124+
}
125+
126+
if (value === null) {
127+
return 'NoneType'
128+
}
129+
130+
if (value === undefined) {
131+
return 'undefined'
132+
}
133+
134+
return typeof value
135+
}

src/python/bisect/bisect.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { pythonBisect } from '../_helpers/_bisect.ts'
2+
3+
export function bisect(a: unknown, x: unknown, lo?: unknown, hi?: unknown): number {
4+
// discuss at: https://locutus.io/python/bisect/bisect/
5+
// parity verified: Python 3.12
6+
// original by: Kevin van Zonneveld (https://kvz.io)
7+
// example 1: bisect([1, 2, 2, 4], 2)
8+
// returns 1: 3
9+
10+
return pythonBisect(a, x, lo, hi)
11+
}

src/python/bisect/bisect_left.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { pythonBisectLeft } from '../_helpers/_bisect.ts'
2+
3+
export function bisect_left(a: unknown, x: unknown, lo?: unknown, hi?: unknown): number {
4+
// discuss at: https://locutus.io/python/bisect/bisect_left/
5+
// parity verified: Python 3.12
6+
// original by: Kevin van Zonneveld (https://kvz.io)
7+
// example 1: bisect_left([1, 2, 2, 4], 2)
8+
// returns 1: 1
9+
10+
return pythonBisectLeft(a, x, lo, hi)
11+
}

src/python/bisect/bisect_right.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { pythonBisectRight } from '../_helpers/_bisect.ts'
2+
3+
export function bisect_right(a: unknown, x: unknown, lo?: unknown, hi?: unknown): number {
4+
// discuss at: https://locutus.io/python/bisect/bisect_right/
5+
// parity verified: Python 3.12
6+
// original by: Kevin van Zonneveld (https://kvz.io)
7+
// example 1: bisect_right([1, 2, 2, 4], 2)
8+
// returns 1: 3
9+
10+
return pythonBisectRight(a, x, lo, hi)
11+
}

src/python/bisect/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { bisect } from './bisect.ts'
2+
export { bisect_left } from './bisect_left.ts'
3+
export { bisect_right } from './bisect_right.ts'

src/python/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * as bisect from './bisect/index.ts'
12
export * as calendar from './calendar/index.ts'
23
export * as difflib from './difflib/index.ts'
34
export * as itertools from './itertools/index.ts'

0 commit comments

Comments
 (0)