Skip to content

fix: REDUCE-created instances re-encode with REDUCE (#32) - #56

Merged
jensens merged 4 commits into
mainfrom
fix/reduce-build-newobj
Sep 30, 2026
Merged

jensens merged 4 commits into
mainfrom
fix/reduce-build-newobj

Conversation

@jensens

@jensens jensens commented Sep 30, 2026

Copy link
Copy Markdown
Member

Fixes #32

An instance that a pickler wrote as GLOBAL () REDUCE state BUILD (a __reduce__ returning (cls, (), state), for example OrderedDict subclasses with a __dict__, or a module-level factory function with () and state) was re-encoded as NEWOBJ + BUILD, so cls.__new__(cls) ran instead of cls() and __init__ was skipped; a factory function even failed to load (NEWOBJ class argument must be a type).

What changes

  • InstanceData gains newobj (true for NEWOBJ, the default everywhere; the decoder's BUILD arm takes it from the popped REDUCE/NEWOBJ, a second BUILD keeps it).
  • JSON: "@newobj": false next to @cls/@s, written only for the REDUCE kind; absent means NEWOBJ, so stored data is unaffected. Documented in the format reference, with the note that @reduce's newobj key has the opposite default (each absent value is the common case of its form).
  • Both pickle encoders emit REDUCE for the false kind, items before BUILD as before; the direct Python-dict encoder routes such dicts through the PickleValue path.
  • Review findings folded in: the Python-dict readers now recognise instance dicts of five keys (@cls, @s, @newobj, @items, @appends; the old len > 4 guard would have pickled such an object as a plain dict of five strings), and every reader rejects @newobj, @items or @appends on a bare class reference without @s (the JSON reader used to drop them silently).

Evidence

  • 240 Rust and 394 Python tests; the new tests count __init__ calls, check opcode order with pickletools and cover the five-key shape, non-bool flag values and the rejected shapes.
  • The 1,692-record sample produces identical PG JSON, dict-path output and re-encoded pickles (no REDUCE-kind instance in it).
  • A fresh reviewer ran 22 shapes (both reduce kinds, factory functions, __getnewargs__, dict/list/OrderedDict subclasses with and without __dict__, both-items reduce, persistent refs, shared objects, uuid, Persistent, OOBTree) through every path at protocols 2 and 3 against pickle.loads of the original, plus 22 stored-JSON inputs: every shape that must not change is byte-identical to main; the four REDUCE-kind shapes are fixed. Its one Important finding (the five-key guard) is the last commit.

Assisted-by: Claude Fable 5.1

…32)

`InstanceData` gains `newobj` (true for `NEWOBJ`, the default everywhere).
The `BUILD` arm sets it from the popped `Reduce` when the arguments are
empty, so `GLOBAL () REDUCE state BUILD` and `GLOBAL () NEWOBJ state BUILD`
no longer collapse into the same node; a second `BUILD` keeps the flag.
Nothing reads it yet.

Assisted-by: Claude Fable 5.1
…JSON (#32)

Both JSON writers add `"@newobj": false` next to `@cls`/`@s` when the
instance was created by calling the class; the JSON reader and the Python
dict reader take an absent key (stored data) or any value but `false` as
`NEWOBJ`. Both pickle encoders emit `REDUCE` for the false kind, items
before `BUILD` as before; the direct dict encoder routes such dicts through
the `PickleValue` path. The 1,692-record sample produces identical PG JSON,
dict-path output and re-encoded pickles (no such instance in it).

Assisted-by: Claude Fable 5.1
Assisted-by: Claude Fable 5.1
…nts (#32)

Fresh review findings: the Python-dict readers took any dict of more than
four keys for a plain dict, and `@newobj` makes an instance with `@items`
and `@appends` five keys, so such an object came back as a dict of five
string keys on the dict path and the record path (silent). Both guards now
allow five keys, the test pushes the shape through `dict_to_pickle` and
`encode_zodb_record`. Every reader rejects `@newobj`, `@items` or `@appends`
on a bare class reference (the JSON reader used to drop them). Minor: stale
encoder comments, the docs name the callable and the opposite default of the
`@reduce` key, the dict path's non-bool flag handling and a PG writer case
are tested, "unpickling" is in the spell-check vocabulary.

Assisted-by: Claude Fable 5.1
@jensens
jensens merged commit d36d5bf into main Sep 30, 2026
5 checks passed
@jensens
jensens deleted the fix/reduce-build-newobj branch September 30, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

REDUCE(cls, ()) + BUILD instances are re-encoded as NEWOBJ + BUILD (skips __init__)

1 participant