v0.10.1: backlog patches across exceptions, slices, ranges#17
Conversation
Mirrors CPython's Py_Ellipsis: a single ellipsisObject of type ellipsis with Repr/Str returning "Ellipsis". Provides Ellipsis() and IsEllipsis() to match the Py_Ellipsis / Py_IsEllipsis surface.
Adds the panels left out of the v0.3 first cut: - ArithmeticError gains FloatingPointError so the trio matches CPython (ZeroDivision, Overflow, FloatingPoint). - Warning plus the eleven Warning subclasses (User, Deprecation, PendingDeprecation, Syntax, Runtime, Future, Import, Unicode, Bytes, Resource, Encoding) so the warnings module can raise them. - SyntaxError plus IndentationError, TabError, and the private _IncompleteInputError that the parser uses for partial input. - OSError plus its fifteen subclasses, and ErrnoSubclass(errno) for the errnomap promotion CPython does inside OSError.__new__. Aliasing errnos (EAGAIN/EWOULDBLOCK on Linux) are deduped so the first ADD_ERRNO call wins, matching the C order. - UnicodeError plus the Encode/Decode/Translate variants. - BaseExceptionGroup and ExceptionGroup for PEP 654 support. The PyExc_ prefix carries underscores into Go names; the lint exclusion that already covered errors/builtins.go is widened to cover errors/exc_*.go for the same reason.
Mirrors namespaceobject.c: a Header-backed object whose attributes live in a plain Dict, with attribute access wired to GetItem / SetItem / DelItem on that dict. Repr formats name=value pairs in sorted-key order. Equality compares the underlying dicts pairwise through RichCmpBool so SimpleNamespace(a=1) == SimpleNamespace(a=1) holds.
Wraps an arbitrary Go value behind a name. NewCapsule rejects nil pointers (CPython raises ValueError); GetPointer requires the caller to present the same name the capsule was created with. Context() and SetContext() expose the user-data slot so existing callers that round-trip extension state through the capsule keep working.
Routes slice subscripting through Unpack, AdjustIndices, and GetIndicesEx so list[a:b:c] and friends can resolve to int triples plus a length. Also adds slice.indices(length) on the Slice type. CPython: Objects/sliceobject.c
Moves rangeIterator out of range.go and adds LengthHint so callers like sorted() and reversed() can pre-size buffers from a partly consumed iterator. Ceil((stop - cur) / step) via big.Int, clamped to zero in either direction. CPython: Objects/rangeobject.c:1006 longrangeiter_len CPython: Objects/rangeobject.c:1128 PyLongRangeIter_Type
Builds the four iterator adapters that builtins layer on top of tp_iter. SeqIter is the iter(seq) fallback for objects with only __getitem__; CallIter handles the iter(callable, sentinel) form; enumerate yields (index, item) pairs with a big.Int index so it stays correct past int64; reversed walks a sequence backward via SequenceMethods.GetItem. CPython: Objects/iterobject.c:21 PySeqIter_Type CPython: Objects/iterobject.c:171 PyCallIter_Type CPython: Python/bltinmodule.c:1455 PyEnum_Type CPython: Python/bltinmodule.c:2724 PyReversed_Type
Both walk the items and compare with PyObject_RichCompareBool / Py_EQ, the same dispatch CPython uses. Index follows CPython's bound normalisation: negative start/stop offset by len, clamp to zero, clamp stop to len, raise ValueError on miss. CPython: Objects/tupleobject.c:765 tuple_index CPython: Objects/tupleobject.c:822 tuple_count
The previous formula was a simple XOR-fold with rotation. Replace it with the actual CPython algorithm: per-element shuffle_bits, XOR combine, count-mixed multiplier, and avalanche dispersion. The -1 sentinel is mapped to 590923713 the way frozenset_hash does. CPython: Objects/setobject.c:768 _shuffle_bits CPython: Objects/setobject.c:793 frozenset_hash
Adds the set-vs-set algebra plus the subset/superset/disjoint predicates and an in-place Update. Result type follows CPython: self.Union(...) returns a frozenset when self is a frozenset. CPython: Objects/setobject.c:1448 set_union CPython: Objects/setobject.c:1611 set_intersection CPython: Objects/setobject.c:1750 set_difference_multi CPython: Objects/setobject.c:1881 set_symmetric_difference CPython: Objects/setobject.c:1986 set_issubset CPython: Objects/setobject.c:2185 set_isdisjoint
Adds the explicit allocation function lists use under the hood: new_allocated = (newsize + (newsize >> 3) + 6) & ~3, with the shrink-below-half rule and the don't-overallocate-tiny-jump collapse. listGrowthCurve is the same formula exposed for tests so the canonical 0/4/8/16/24/32/40/52/64/76 pattern stays pinned. CPython: Objects/listobject.c:55 list_resize
Wires Type.Repr / Type.Str so Repr(IntType) yields "<class 'int'>" instead of the generic "<type object at addr>" fallback. Adds a Module field for types defined in non-builtin modules so the qualified "<class 'pkg.Name'>" form surfaces when set; an empty or "builtins" module is hidden, matching CPython. CPython: Objects/typeobject.c:1268 type_repr
Adds IsTrue/IsFalse/IsBool singleton checks plus the PyObject_* container free functions (Length, GetItem, SetItem, DelItem, Contains, Iter, IterNext, TypeOf) so callers stop reaching into type slots directly.
Adds HashBytes/HashString thin wrappers around hash.Buffer and rewires strStub.Hash to use them. Drops the inline FNV-1a placeholder; every buffer-shaped value now goes through the same SipHash-1-3 path that reads the runtime hash secret.
Adds NewStructSeqType / NewStructSeq for the named-tuple types CPython uses for sys.flags, sys.version_info, os.stat_result, etc. Instances behave like tuples for indexing, iteration, hashing, and richcompare; Getattro maps field names onto positional slots.
Pulls the -5..256 PyLong_SMALL_INTS cache out of int.go and into its own file with smallIntFromInt64 / smallIntFromBig helpers. NewInt and NewIntFromBig now route through the helpers so the hit path is one function call and the cache window only lives in one place.
…cores IntFromString accepts the same shapes int(s, base) does in CPython: optional whitespace, optional sign, optional 0x/0o/0b prefix when base is 0 or matches, PEP 515 underscores between digits, and arbitrary precision through math/big. Misplaced underscores, dangling prefixes, and invalid digits all surface as ValueError.
Pulls intAnd / intOr / intXor / intInvert / intLshift / intRshift out of int.go. Adds a shared shiftOperands helper so lshift and rshift share the negative-count and overflow checks instead of repeating them.
Pulls add/sub/mul/true-div/floor-div/mod/divmod/pow out of int.go. The split keeps int.go limited to type setup, repr/hash/richcmp, and the unary slots; the binary-arithmetic panel is now line-by-line parallel to the long_add/long_sub/... ordering in CPython's longobject.c.
Continues the longobject.c-shaped split. int.go keeps just the struct, type singleton, slot wiring, and the constructors; everything slot-shaped lives next to its peers in long_arith / long_bitwise / long_misc.
|
Finished the longobject.c-shaped split. int.go is back to just the struct, type singleton, slot wiring, and constructors. The slot bodies live next to their peers in long_arith / long_bitwise / long_misc, which lines up with the file layout CPython uses. |
…s.go These three slot-bundle structs lived at the bottom of type.go but they aren't really part of the Type definition; they're the protocol panels Type points at. Pulling them into slots.go matches CPython's layout in Include/cpython/object.h and keeps type.go focused on the typeobject itself.
|
Pulled NumberMethods, SequenceMethods, and MappingMethods into their own slots.go. type.go was getting noisy with the bundle structs at the bottom and these are really the protocol panels that hang off the type, not part of the typeobject definition itself. Same arrangement as Include/cpython/object.h. |
…ar/copy) into list_misc.go Adds the user-facing list methods that listobject.c exposes through list_methods. They were missing entirely from gopy; the runtime had Append and SetSlice and that was it. Also moves listRichCmp out of list.go to keep the slot dispatch table separate from the method panel.
|
list had Append/SetSlice and nothing else. Ported the rest of the listobject.c method panel: count, index, insert, remove, pop, reverse, clear, copy. Sort still needs Timsort and lives in its own task. Also moved listRichCmp out of list.go into list_misc since the slot dispatch table doesn't need to share a file with the slot bodies. |
The previous repr leaned on strconv.FormatFloat 'g' -1, which gives the right digits but flips to exponential at a different threshold than CPython. repr(1e15) was coming out as "1e+15" instead of "1000000000000000.0", and integral floats lost their trailing ".0". Ports format_float_short's layout decision: take the shortest digit string from Go's 'e' -1 form, then route to fixed or exponential output based on CPython's decpt > 16 / decpt <= -4 rule, padding zeros and adding ".0" the way Py_DTSF_ADD_DOT_0 asks for. Test panel pins repr against CPython 3.14 across the boundary values (0, -0.0, 1e15, 1e16, 0.0001, 0.00001, nan, inf).
|
Ported float repr to match CPython's shortest-roundtrip output. The old code used Go's strconv 'g' -1, which produces the right digits but flips to exponential form at a smaller magnitude than CPython does. Now repr(1e15) gives "1000000000000000.0" instead of "1e+15", and integral floats keep their trailing ".0" via the Py_DTSF_ADD_DOT_0 behaviour. Test panel pins the boundary cases (0, -0.0, 1e15/1e16 hop, 0.0001/0.00001 hop, nan, inf). |
…rscores float() didn't have a string parser at all. Ported PyFloat_FromString to objects/float_parse.go: trim whitespace, peel + or - sign, recognise inf / infinity / nan case-insensitively, accept PEP 515 underscores between digits, and hand the remainder to strconv.ParseFloat for the actual digit-to-double step. Test panel covers the accept side (signs, exponents, underscores, inf, nan) and the reject side (empty, lone sign, dangling underscores, underscores adjacent to dot or exponent marker).
|
float() never had a string parser, so reaching for |
bytes was missing entirely. Ported the immutable bytes type: constructors (NewBytes, NewBytesFromString, EmptyBytes singleton), the b'...' repr with quote selection and \x escapes, hash through the runtime SipHash dispatcher, lexicographic richcmp, and the sequence panel for length/getitem/contains. Methods (replace, split, decode, ...) come in the next task.
Adds TpTraverse to cell, bound method, classmethod, staticmethod, frame (including the activation-record's globals/builtins/locals/fast/ cell/free locals plus the back-chain), coroutine wrapper, and the two async generator awaitables. Generators, coroutines, and async generators themselves carry no Object fields in gopy (the suspended frame lives on the goroutine), so they remain leaves. Splits gc_gil.c into gc/gil.go as a documented no-op: gopy has no per-type freelists for _PyGC_ClearAllFreeLists to clear.
|
Filled in two of the three remaining 1613 gaps.
For the gc_gil piece — that file in CPython is one function, Tests at |
The collector reclaimed every unreachable cycle unconditionally, even when a finalizer took a fresh ref. Add handleResurrected after finalize_garbage to redo the deduce pass on the post-finalize list and pull resurrected objects back to a generation. DEBUG_SAVEALL now appends the still-dead set to the gc.garbage list, which is the same *objects.List the gc module exposes, so user code that reads or clears gc.garbage sees the runtime view directly. Mirrors gc.c:1261 handle_resurrected_objects and the SAVEALL branch of gc.c:1142 delete_garbage.
|
1613-P done. The collector now does a proper resurrection pass after finalize_garbage instead of trusting Go's GC to keep refloated objects alive: DEBUG_SAVEALL is wired through end-to-end. The module's Tests in gc/garbage_test.go cover: empty by default, populated under SaveAll, the module attribute and the runtime view share storage, finalizer-driven resurrection keeps both cycle members tracked, the resurrected finalizer fires exactly once across two collections, and SaveAll-skipped-when-disabled. Existing cycle/finalize tests still pass; resurrection's new pre-pass leaves them unchanged because no finalizer Increfs. Spec 1613-N/-O/-P now all show |
Walks the GCTests panel in cpython/Lib/test/test_gc.py and translates every scenario gopy can run today into a Go counterpart. Each test carries the matching CPython source citation and an explicit assertion against the corresponding gc surface (cycle reclaim per container type, get_referents shape, is_tracked / is_finalized membership, get_count / collect_generations promotion, get_stats panel and counter increments, freeze / unfreeze round-trip, get_objects per-generation visibility, the three resurrection invariants from PEP 442, and the bug21435 regression). Scenarios that hinge on user classes, capsules, the trashcan, or collector reentrancy are pinned as TestParityGapsTracked subtests with a citation and the gating spec/task ID, so the gap is discoverable from `go test -v` rather than dropped silently.
|
test_gc.py parity sweep landed in commit 8e32f09. Walked the GCTests panel end to end and shipped a 1:1 Go counterpart for every scenario gopy can execute today, each carrying a
Scenarios that hinge on user classes, capsules, the trashcan, or collector reentrancy are pinned as |
Lib/weakref.py and Lib/_weakrefset.py wrap a regular set/dict with a small _remove closure installed as the weakref callback so dead referents drop out automatically. The Go port mirrors that shape: each container is a *Header-embedding type whose internal map is keyed by *objects.Weakref, and a per-container BuiltinFunction removes the matching entry when handle_weakrefs fires the callback. TpTraverse on each container visits the keys/values that should participate in cycle detection but skips the weakly-referenced side, preserving weak semantics. Tests exercise the API surface plus the end-to-end gc.Track -> gc.Collect -> _remove path that actually proves the collector wiring works.
|
WeakSet, WeakValueDictionary, and WeakKeyDictionary now exist as Go types in a new
Tests cover the API (add/remove/discard/contains/iter/update/copy/pop on the set side, get/set/del/contains/keys/values/items/update on the dict side) plus the end-to-end gc.Track -> gc.Collect -> _remove path that proves the collector wiring actually fires. One known leak: each |
Wraps Bytes / ByteArray / MemoryView. Indexing returns an int byte; slicing returns a new MemoryView (alias for step==1, copy otherwise). Exposes the standard buffer-protocol attributes (format="B", itemsize, nbytes, readonly, ndim=1, shape, strides, suboffsets, c_contiguous, f_contiguous, contiguous, obj). Hashing only on read-only views; equality compares against any bytes-like. Multi-format and writable views are deferred to 1689-B once Type grows a buffer slot. CPython: Objects/memoryobject.c:3402 PyMemoryView_Type
|
Added Writable views and the PEP 3118 multi-format path are still deferred to 1689-B; they need a real buffer slot on |
LOAD_BUILD_CLASS used to error out; now it pushes the builtin, the
inner class body runs with the class namespace as f_locals, and the
result flows through type(name, bases, ns) to NewUserType. Method
descriptors bind through Function's tp_descr_get.
Filling in the path uncovered three smaller bugs that had to be
fixed before the end-to-end tests could pass: actionAstClassDef was
a placeholder that swallowed every class statement at parse time;
argumentsOf did not unwrap the []any{*ast.Arguments} shape so every
def had Args=[] (so methods saw self as a global); LOAD_ATTR's oparg
was missing the <<1 shift, so c.foo looked up Names[0] instead of
Names[1]. The second const-table entry sometimes pointing at a
nested code object also needed a wrapConst case to lift it.
|
Got The path was longer than I expected. LOAD_BUILD_CLASS now resolves To make Three latent bugs in the parser/compiler showed up only once classes
Also a tiny wrapConst case for Eight end-to-end tests in vm/build_class_test.go cover empty body,
|
The vm reads LOAD_GLOBAL's oparg as (idx<<1)|push_null. addOpName was only shifting LOAD_ATTR, so the second LOAD_GLOBAL inside a function (idx 1) ended up looking like 'push NULL plus idx 0' to the interpreter and the wrong global got loaded with a stray NULL on the stack. Surfaced once super(B, self) gave us a function that loads two distinct globals. CPython: Python/codegen.c:L354 codegen_addop_name
CPython 3.14 dropped MAKE_FUNCTION's flags oparg and made attribute attachment a separate op: MAKE_FUNCTION just wraps the code object and one SET_FUNCTION_ATTRIBUTE per attribute (closure, annotations, kwdefaults, defaults) stamps the value pushed in the outer scope onto the new function. The vm already handled SET_FUNCTION_ATTRIBUTE but compile was still folding the flags into MAKE_FUNCTION's oparg, so closure tuples ended up stranded on the stack and inner functions hit COPY_FREE_VARS with no closure. CPython: Python/codegen.c:L923 codegen_make_closure
The objects layer gets a Super type that mirrors CPython's superobject: __getattr__ walks the MRO past the named type, and the descriptor protocol rebinds super(C) to super(C, owner) for the implicit-instance form. Type call dispatch routes super(...) through SuperType.Call; supercheck enforces the type/instance relationship. Class bodies now stamp __class__ as an implicit cell when any method references super (or __class__ directly): the codegen prologue emits MAKE_CELL for it, the epilogue copies it into __classcell__, and __build_class__ patches the cell with the freshly built class so super(C, self) inside C's methods resolves correctly. CPython: Objects/typeobject.c:11769 superobject CPython: Python/codegen.c:L1515 codegen_class_body class-cell prologue/epilogue CPython: Objects/typeobject.c:4474 type_new_set_classcell
|
super lands on this branch — The Two compile-side bugs surfaced while building the e2e tests and got their own commits up front:
Class bodies now stamp Coverage:
Next up: #367 |
Plumbs __slots__ through NewUserType so a class that declares it gets a fixed-index slot array instead of the implicit per-instance __dict__. Each slot becomes a MemberDescr (data descriptor) on the type; reads and writes route through DescrGet/DescrSet against Instance.slots, and attribute writes for any name that isn't a slot or descriptor raise AttributeError when no dict is present. Mirrors CPython's type_new_slots / type_new_descriptors split: the slot list is validated up front (identifiers, no class-variable conflicts, __dict__/__weakref__ specials), then each entry is registered as a PyMemberDef-style descriptor at a fixed offset. Drops the trailing period on a super.go error string while here.
|
slots landed in dc9ab25.
Validation mirrors Coverage:
|
Wires the parser + compile pipeline behind the compile(source, filename, mode, ...) builtin. The mode string maps to parser.ModeFile / ModeEval / ModeSingle; the resulting compile.Code is lifted to objects.Code so the returned value can flow into vm.EvalCode (or the eval/exec builtins landing alongside this). Argument validation matches CPython: ValueError on bad mode / out-of- range optimize, TypeError on missing required args, wrong types, or duplicate keyword arg. flags is parsed for signature parity (the AST bits are rejected because gopy has no Python-level AST), dont_inherit is accepted but ignored (gopy carries no compiler-flags context to inherit), and func_type is rejected with the same wording CPython uses when PyCF_ONLY_AST is missing.
|
compile() landed in d043a68. The builtin now wraps the existing parser + compile pipeline: mode picks the parser entry ( Validation mirrors CPython:
Coverage:
|
Both builtins delegate to the same compile + EvalCode pipeline. The v0.10.1 cut wires the source coercion (str -> compile, code passed through), the (globals, locals) defaulting from the running frame via SetCurrentScope, and the (globals must be a real dict, locals can be any object) validation CPython enforces. vm.builtins_hook installs a SetEvaluator hook that grabs the current state.Thread (allocating a fresh one when called outside any frame, so library callers work too) and dispatches through EvalCode. eval() returns the expression value; exec() returns None on success and propagates the real Python error otherwise. Closure tuples for exec(code, ..., closure=...) are not yet supported; the hook rejects unknown kwargs so the call surfaces a TypeError today rather than silently ignoring closure cells.
|
eval() and exec() landed in dabbe1a. They share so much shape (the same source coercion, the same globals/locals defaulting, the same evaluator hook) that splitting them across two commits would have been pure noise. The dispatch goes:
Defaulting matches CPython: Closure tuples for Coverage:
Marking #345 and #344 done. |
The full _io tree splits raw / buffered / text I/O across PyFileIO, PyBufferedReader / PyBufferedWriter, and PyTextIOWrapper, with io.open gluing them together. v0.10.1 collapses that stack into one File type (objects/file.go) backed by *os.File: read modes wrap a bufio.Reader, write/append modes wrap a bufio.Writer, the binary flag decides whether read / write trade in bytes or str. Methods (read, readline, write, close, flush, __enter__, __exit__, readable, writable) and attributes (name, mode, closed) come through Getattro since the type panel registration plumbing isn't wired for built-ins yet. builtins/open.go ports the validator from Lib/_pyio.py:75 def open: it checks the mode chars against "axrwb+t", rejects duplicates, t+b, and multiple direction bits, then maps the validated booleans onto an os.OpenFile flag combo (O_RDONLY / O_WRONLY|O_CREATE|O_TRUNC / O_WRONLY|O_CREATE|O_APPEND / O_WRONLY|O_CREATE|O_EXCL, with '+' flipping to O_RDWR). encoding / errors / newline are accepted but not yet honored. The split into RawIOBase / Buffered / Text and the encoding-aware TextIOWrapper land with the rest of 1689.
|
Picked up #365 next: open() and a minimum-viable File type to back it. Rather than land the full _io tree (PyFileIO, PyBufferedReader/Writer, PyTextIOWrapper, IOBase) in one shot, I collapsed those layers into a single
Tests cover the round-trip (write then reopen-and-read), all six error paths in mode validation, append vs. truncate vs. exclusive, and the iterator/enter/exit contract. Splitting this back into separate raw/buffered/text classes — and adding a real codec layer — stays on the 1689 punchlist. |
Parser/myreadline.c is the dispatch layer between PyRun_Interactive*
and whichever line reader is installed: PyOS_StdioReadline by default,
the GNU readline / libedit shim once the readline extension module
loads and overwrites PyOS_ReadlineFunctionPointer. The new
myreadline package keeps that shape - a Reader function-pointer hook
plus a stdio fallback - so plugging in peterh/liner or a cgo readline
bridge later is a SetReader call away rather than a refactor.
The stdio default mirrors PyOS_StdioReadline: write the prompt,
ReadString('\n'), return the line keeping its trailing newline (or
shorter on EOF). io.EOF means clean EOF; ErrInterrupt is the Ctrl-C
counterpart of the C version's "return 1 (Interrupt)" path.
re-entrancy is rejected the same way myreadline.c rejects nested
PyOS_Readline calls.
pythonrun.InteractiveLoop now drives input through myreadline.Readline
and prints "KeyboardInterrupt" + keeps prompting on ErrInterrupt
instead of exiting. Multi-line PS2 continuation still needs a parser
"input incomplete" signal and stays out of this cut.
|
On to #364: myreadline. Parser/myreadline.c is the dispatch layer that sits between the REPL and whichever actual line-editor is loaded. By default it points at New package
PS2 continuation is still on the punchlist — it needs the parser to surface an "input incomplete" signal so the loop knows when to keep asking for more lines, which the gopy parser doesn't yet do. |
makeReadFile / makeWriteFile leaked an open file handle past the test:
on Linux and macOS t.TempDir's RemoveAll happily reclaims the path,
but Windows refuses to unlink while a handle is still open and the
cleanup hook errors the test out. t.Cleanup(fi.Close) closes the
handle before the parent t.TempDir cleanup runs.
Spotted on the v0.10.1 backlog branch's CI run when the Windows job
flagged TestFile{ReadAllText,ReadAllBinary,ReadSizedShort,
IteratorYieldsLinesUntilEOF,MethodLookup}.
First slice of the v0.10.1 backlog. These are small isolated ports that piled up across v0.2 through v0.10 and aren't tied to any one panel.
What's in here
Notes
.golangci.ymlexclusion extended toerrors/exc_*.goso the PyExc_ singletons stop tripping revive's var-naming checkTest plan
go test ./... -count=1golangci-lint run ./...