Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

About SPy package distribution

What a package contains

A package would be a wheel with three kinds of file:

An archive holds WebAssembly object code, which does not depend on the operating system or on the CPU. Sources, archive and manifest are therefore platform independent, and the wheel can be a py3-none-any wheel: no platform tag is needed.

A linked .wasm file is not part of the package. It would be a complete program with its own copy of libspy, its own memory and its own allocator, which cannot be combined with another one (next section).

Using a package from the interpreter

One instance, not two

Loading the .wasm of a package as a second WASM instance would give two linear memories. A pointer is an index into one memory, so a str created by one instance means nothing to the other. Making both instances import the same memory fails for three reasons:

So everything is linked into one instance: libspy and the archives of all the packages are merged into the vm.ll of the interpreter. There is one memory and one allocator, and a pointer means the same thing for interpreted and compiled code.

A user module and its dependency

The machinery of the prebuilt stdlib already accepts modules which are not in the stdlib. Here depa plays the role of a package which depends on depb:

# depb.spy
def bump(x: i32) -> i32:
    return x + 1
# depa.spy
import depb

def twice(x: i32) -> i32:
    return depb.bump(depb.bump(x))

The C backend lowers depa.twice to (line directives removed):

int32_t spy_depa$twice(int32_t x$0) {
    int32_t $v0;
    $v0 = spy_depb$bump(x$0);
    return spy_depb$bump($v0);
}

The call is compiled as a C call to the symbol spy_depb$bump, so the archive of depa must contain the code of depb too. It does: it holds depa.o, depb.o and native_shim.o, while the manifest lists only depa::twice, the function of the requested module. The interpreter then uses it like this:

nsl = get_native_stdlib(("depa",), ("/tmp/deptest",))  # builds, then caches
vm = SPyVM(native_stdlib=nsl)
vm.path.append("/tmp/deptest")
vm.import_("depa")
w_twice = vm.lookup_global(FQN("depa::twice"))   # a W_NativeFunc
vm.unwrap(vm.fast_call(w_twice, [W_I32(40)]))    # 42

The second argument of get_native_stdlib is the list of directories where to look for modules, in addition to the stdlib. Called directly from the interpreter, depb::bump stays interpreted: only the functions of the requested modules are replaced.

Each symbol needs one owner

An archive contains the compiled code of every module of its import tree. Linking the archives of depa and depb into one instance therefore fails:

wasm-ld: error: duplicate symbol: spy_depb$bump
>>> defined in .../libspy_stdlib.a(depb.o)
>>> defined in .../libspy_stdlib.a(depb.o)

For a function this is only a link error. For a module variable, two copies would mean two independent states. Two ways to give every symbol exactly one definition:

The first option is needed to ship prebuilt archives. The second needs nothing new in the package format.

The modules must be known before the VM starts

vm.ll is created by the constructor of SPyVM, so the bundle must be linked before. In SPy, import is a static declaration: ImportAnalyzer collects every import statement, including those inside functions, starting from the root module. The full set of modules is therefore known before anything runs:

  1. walk the imports from the root module;

  2. find, for each module, the archive of the package which provides it;

  3. link and cache the bundle (wasm_bundle.py);

  4. create the VM on this bundle.

Today ImportAnalyzer receives a VM, mostly for vm.path and robust_import_caching. Step 1 needs a variant which works without a VM.

This does not fit an incremental session such as IPython: the imports of a cell are known only when the cell runs, and relinking would move the live heap. The rule of the prebuilt stdlib covers this case: a module which is not in the bundle stays interpreted, slower but identical. A session can also declare its modules at startup.

Using a package from spy build

A native archive per platform, as C libraries do, would be a third option. It would require platform-specific wheels.

Versioning libspy

On Linux, a shared library such as libfoo.so.1.2.3 embeds a SONAME, libfoo.so.1. Every program records the SONAME it was linked against, and the loader looks for exactly that name. The major number changes only when the ABI breaks (a struct layout, a removed function or a changed signature), so several majors can be installed side by side.

libspy is linked statically: there is no loader, and the check happens when the bundle is linked. What can break an archive:

The plan, in two stages:

Open questions

Cross-module redshifting. If mypackage.add(x, y) can be specialised for i32 arguments, does that specialisation happen at package-compile time (several variants in the archive) or at application-compile time (which requires the source)? This is a trade-off between convenience of distribution and performance.

Python bindings. There is no mechanism to generate a Python extension from SPy code. It would need something like a CFFI or pybind11 equivalent for SPy, and it would be the only part which needs platform-specific wheels.

PyPI and WebAssembly. There is no agreed platform tag for wasm32 wheels on PyPI. It matters only for Python bindings, since the wheels above are py3-none-any.

Plan

Each step is a separate, testable change.

☐ 1. Import pre-pass. Compute the set of modules from the root module, without a VM.

☐ 2. Several archives in one bundle. Link libspy and the archives of all the required packages, with the ownership rule chosen above.

☐ 3. Package layout. One py3-none-any wheel with sources, archive and manifest; the interpreter finds them through the installed packages.

☐ 4. Exact version check. The manifest records the libspy version and the build configuration; a mismatch is an error.

☐ 5. ABI number. Once libspy is stable.