Phase 5: wrapper synthesiser
Phase 5 produces the Python-side bridge code for one consumed PyPI package: a <pkg>_externs.py shape-coercing wrapper plus a matching <pkg>_externs.pyi stub. The synthesiser walks the typed ModuleSurface from Phase 3 and the closed type-mapping pass from Phase 4. Items the type table refuses appear in Wrapper.Skipped and a generated SKIPPED.txt. A shared _mochi_wrap.py runtime helper module accompanies every wrapper.
Gate
go test ./package3/python/wrapper/... is green. The gate covers:
- The
Synthesiseentry point: validates package name as a Python identifier, requires a non-nil surface, emits a deterministic wrapper. - Function lowering: positional
arg0, arg1, ...shim that forwards to_src.<name>(...). Async functions produce a synchronous<name>_syncentry that runs through_run_async(per-call mode, default) or_MOCHI_LOOP.run_until_complete(...)(persistent mode, opt-in), plus anasync def <name>_asyncdirect entry. - Record lowering: TypedDict + frozen
@dataclassre-export the source class and emit a_<Name>_to_mochi_dictcompanion that walks every field viagetattr+_to_mochi_dict. Mutable@dataclassand plain classes are refused with the Phase 4 override hint propagated. - Interface lowering: Protocol classes re-export the source class verbatim; the
.pyimirrors the method set. - Constant lowering: re-exports the source attribute and emits a typed stub entry. Unannotated constants are refused.
- Privacy: leading-underscore names (other than dunders) are skipped with
SkipPrivateName. - Refusal qualification: every
SkipReport.ItemPathis rewritten to<package>.<item>so theSKIPPED.txtoutput groups by package. - Deterministic ordering: Items are sorted by source name.
- Renderer correctness:
renderPyAnnowalks everyMochiTypeKind, including nesteddict[str, list[Optional[int]]]shapes, and falls back toAnyonKindUnknown. - Helper imports: every wrapper imports
_to_mochi_dict; async wrappers add_run_async; persistent-loop wrappers add_persistent_loop. - Sentinel
TestPhase5WrapperSynthesiserwalks a representative.pyiend-to-end including the private + complex refusal subcases.
Files
package3/python/wrapper/doc.go— package overview.package3/python/wrapper/wrapper.go—Wrapper,Item,Options,EventLoopModetype set.package3/python/wrapper/synth.go—Synthesise(pkg, surface, opts)entry point + privacy / refusal qualification.package3/python/wrapper/render.go—<pkg>_externs.py+<pkg>_externs.pyirenderers;renderPyAnno(MochiType → Python annotation).package3/python/wrapper/runtime.go—Runtime()+RuntimeStub()returning the shared_mochi_wrap.py+.pyitext.package3/python/wrapper/synth_test.go,render_test.go,phase05_test.go— ~50 tests covering the gate above.
Fixtures
The unit tests construct ModuleSurface values directly and also round-trip representative .pyi source via stubs.ParsePYI. The 25-package corpus run is staged for Phase 6 (extern emit), where the typed wrapper Items can be exercised end-to-end against actual PyPI wheels.
Skip count
The Phase 5 surface is a pure projection of Phase 4 decisions plus three Phase-5-specific refusals (private name, unannotated constant, qualification rewrite). The expected SkipReport count per fixture package is therefore phase4_skips + phase5_private_count and is captured in the sentinel TestPhase5WrapperSynthesiser.
Notes
- The synthesiser is deliberately Python-source-only at this phase. The CPython extension (
.so) and the libpython link step land in Phase 8 (build orchestration); Phase 5 stops at the<pkg>_externs.py+<pkg>_externs.pyiPython source pair. Iterator[T] → list<T>is lowered by Phase 4; the wrapper currently leaves the lazy iterator companion off (the helper exists in_mochi_wrap.pyas_materialise_iterfor Phase 6 to wire in if a fixture demands lazy access).
Timestamps
- 2026-05-29 23:35 (GMT+7): Phase 5 started.
- 2026-05-29 23:48 (GMT+7): Phase 5 LANDED.