Spike: open module-registry seam (illustrates #601) - #615
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Merging this PR will not alter performance
Comparing Footnotes
|
|
I'm afk right now, so haven't looked at the code yet, but wouldn't it be more elegant long term to add a |
|
Agreed that's a nicer end state, and I think in line with #568. I left it out here because |
Spike for the extensibility direction discussed on pydantic#601: replace the closed `StandardLib`/`ModuleFunctions` enum path with a name-keyed, append-only registry so adding a native stdlib module is a registration, not a core edit. The seam: - `modules/registry.rs`: a `static REGISTRY` of `ModuleDescriptor`s and a free fn `registry::call` (dispatch by stable `ModuleFuncId`, a plain match — the simplest shape; now that pydantic#613 has removed the VM's `ResourceTracker` generic, a fn-pointer table or `dyn` registration could replace the match without touching callers). - Dynamic interning: module/function names go through the dynamic string pool, not `StaticStrings`, so the per-token parse cost stays flat in the number of registered modules; `prepare` interns only the modules a program actually imports (full-tree walk, so nested `import`s are covered), so that cost tracks usage rather than registry size. - One immediate `Value::RegistryFunction(ModuleFuncId)` variant, no heap reference, trivial clone/drop — the scalar hot path is untouched and no `HeapData` variant is added (the pydantic#248 regression concern). - `LoadModule` now carries a u16 const-pool module-name id (mirroring `RaiseImportError`); the compiler import gate and `load_module` resolve registry-first, then fall back to `StandardLib`. `struct` (`calcsize`/`pack`/`unpack` over standard-size numeric codes) is an illustrative stub to exercise the seam end to end without a new heap type; it is not a full `struct` implementation. Resource use is charged before every format-derived allocation (`pack` buffer, `unpack` output tuple, and the parsed-field vector), and huge repeat counts are rejected rather than overflowing. Everything is contained to the `monty` crate. See `limitations/struct.md`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
67d6e89 to
d1b2fa9
Compare
|
Rebased onto main now that #613 is in. The @samuelcolvin on Happy to take a pass at the dyn PyTrait version of this once #568 hits. |
|
Makes sense. In general I think the direction is good. I'll let @davidhewitt review the implementation. A few notes:
My claude conversation in case it's helpfulPR #615 — open module-registry seam: review conversationNotes from reviewing Q: Compare this PR to main, and look at issue #601. Explain simply how it works — I'm not clear how it makes it easier to add new modules?What the PR isIssue #601 is a direction check, not a feature request: jakeb-grant asked whether How it worksOn main, an import is resolved entirely at compile time against a closed enum:
On this branch, that lookup becomes name-based and late:
So what actually gets easier?Concretely, adding a module today touches four places:
With the registry it's two:
The real payoff isn't the edit count, it's step 1. Where I'd push backThe edit-count win is modest and partly self-inflicted. The stated reason for ids-not-fn-pointers ("plain data, so it lives in a Also worth checking: Q: We want to support modules loaded from the monty filesystem, e.g. from the monty-fs machinery for reading files. Will this support that, or should that come later?Later — but this PR moves one step in the right direction. The half that generalizes is the opcode change. Going from The half that doesn't generalize is the interning strategy, and that's where the
Beyond interning, a monty-fs import needs three more things this PR doesn't touch:
Also, the compiler's static gate ( So: it's a genuinely bigger piece of work than the registry, and none of it is Q: Is the search for a module linear? Should we use a hashmap or similar to make lookup faster?Not worth a Where the scans are and how hot:
The fix worth making isn't a map at all: make The hashmap that will be genuinely needed is a different one: the per-session Q: What is phf?
static MODULES: phf::Map<&'static str, &ModuleDescriptor> = phf_map! {
"struct" => &struct_::DESCRIPTOR,
"array" => &array::DESCRIPTOR,
};The difference from
It's the standard tool for "fixed set of string keys known at compile time", which That said, for this case I'd not bother. It came up mainly to make the point that if the scan ever becomes a real cost, |
A few things I had Fable 5 vet for me
|
Draft, not for merge. This is a spike to make the idea in #601 concrete,
since you said you were open to seeing what it looks like. The interesting
part is the seam; the module I used to exercise it is throwaway.
One self-contained commit; it builds clean and
struct__ops.pypasses on bothMonty and CPython.
The idea
Today, adding a native stdlib module means editing the closed
StandardLib/ModuleFunctionsenums and threading a new variant through every match thatreferences them. This replaces that with a name-keyed registry. Building the
seam is a one-time change to the core (the files in the reading order below).
After that, adding a pure-function module like this one is a registration in
two files (
modules/mod.rsandregistry.rs) plus the module's own file, withno further core changes. A module that needed a new heap type would touch more,
which is the #568 question below.
The pieces:
modules/registry.rs: astatic REGISTRYofModuleDescriptors (plaindata, no tracker generic) and a
registry::callthat dispatches by astable
ModuleFuncId. It's a plain match, nodyn, so it compiles undertoday's
VM<'h, T: ResourceTracker>.StaticStrings, so theper-token parse cost stays flat regardless of how many modules exist. And
prepare interns only the modules a program actually imports, not the whole
registry, so that cost tracks usage rather than registry size.
Value::RegistryFunction(ModuleFuncId): no heap ref, trivialclone/drop, no new
HeapDatavariant. The scalar hot path is untouched,which I think is the Add built-in NumPy support #248 regression you flagged.
LoadModulenow carries a u16 const-pool name id (same shape asRaiseImportError). The import gate andload_moduleresolve registryfirst, then fall back to
StandardLib.What I left open on purpose
HeapData::ForeignvsInstanceconvergence from split all binary operators to be proper methods onPyTrait#568.structispure functions over
bytes/tupleand needs no new heap type, so the spikeproves the registration/interning seam without prejudging that. It felt like
a separate decision, happy to talk it through.
impl ResourceTrackerannotations Remove theResourceTrackergeneric, always useResourceTracker#613 removes. If Remove theResourceTrackergeneric, always useResourceTracker#613 lands first they dropto
VM<'h>and the seam gets simpler; dropping the tracker generic is alsowhat should make
dyn PyTraitfeasible. The registry shape is unchangedeither way, only what an id resolves to (a runtime table instead of the
match).
About the
structmoduleThrowaway.
calcsize/pack/unpackover a few numeric codes; format andvalue errors are
ValueError(buffer type errors stayTypeError), nostruct.error/Struct/iter_unpack/native sizes. It's thereonly to run the seam end to end and show a module producing and consuming
existing heap types. Please don't review it for parity. Divergences are in
limitations/struct.md.Reading order
modules/registry.rs— the registryprepare.rs— lazy per-import name interningbytecode/compiler.rscompile_import— the "is X in scope" gatebytecode/vm/mod.rsload_module+vm/call.rs— resolution and dispatchvalue.rs— the one new variantEverything is inside the
montycrate (plus thelimitations/docs). No hostcrate, wire protocol, or binding changes.
Built with AI assistance (noted in the commit trailer). What I'm after is your
read on whether this is the shape you had in mind, not a merge review.
Summary by cubic
Introduces an open, name-keyed stdlib module registry so new native modules can be added via registration instead of editing closed enums; adds an illustrative
structmodule to exercise the seam end to end. Implements the extensibility direction discussed in #601 while keeping hot paths unchanged.New Features
modules/registry.rswithModuleDescriptor,ModuleFuncId,REGISTRY,lookup/is_registered,create_module,call(append-only dispatch), plusdescriptor_namesandfunction_name.Value::RegistryFunctionwith VM call dispatch andrepr; identity/serde and hashing support added.LoadModulewith a u16 const index for the module name; the import gate checks the registry first, thenStandardLib.preparelazily interns names only for imported registry modules (walks nested imports).structmodule (calcsize,pack,unpack) plus tests and docs under limitations.Refactors
load_modulenow resolves by interned name (registry-first, fallback toStandardLib); VM reads the name from the const pool at runtime.LoadModulenow uses a u16 const-pool operand; stack effects updated accordingly.RegistryFunctionviaregistry::call; module attribute calls usevm.call_function.ExcType::runtime_errorfor invariant failures like invalid registry ids from snapshots; REPL/callability checks includeRegistryFunction.Written for commit d1b2fa9. Summary will update on new commits.