Reference
Modules
Every import resolves at compile time. The compiler asks the host for each module and flattens it into the bytecode. The VM never fetches anything at run time.
The host decides what each name means. It is the JS host, the CLI or a Rust program that drives compiler.wasm.
A module is one of two kinds, and its artifact decides which. Both use the same import syntax.
Nobody classifies a package. A manifest entry only names a spec.
| Kind | Artifact | What it is |
|---|---|---|
| Code module | .py | Its top level runs once at startup. Its exports live on a module object shared by every importer. |
| Native module | .wasm | A plugin over the ABI. |
- A file that starts with the wasm magic bytes is a native module.
- Any other file is Python source, whatever its extension.
- A
.jsor.mjsimport fails where it is written withmodule 'charts' is JavaScript, ship a .py or a .wasm. No JavaScript loads besides the system calls of the host. edge add,edge lock,edge buildandedge publishstop on a JavaScript module before they write anything.
Syntax
from json import dumps, loads # bare name, declared in edge.json
from .lib.helpers import slugify # relative to the importing file (./lib/helpers.py)
from ..shared.util import chunks # one dir up per extra dot (../shared/util.py)
from lib.helpers import slugify as sl # absolute from the nearest edge.json dir
import math # binds the module itself, use math.sqrt(2.0)
from utils import * # every export becomes a flat name in scopeName lists can span lines inside parentheses, with an optional trailing comma. Dots map to directories and the .py suffix is implicit.
| Spec | Anchored at |
|---|---|
A leading dot, .lib.helpers | The importing file |
A dotted name, lib.helpers | The nearest edge.json directory |
A plain name, json | An entry in edge.json, which edge add <name> writes |
Two forms do not work.
from . import xnames nothing. Edge has no packages for a bare dot to reach.- Dynamic imports are absent. There is no
__import__and noimportlib, and the module set is fixed per compilation.
Module semantics
Modules are singletons. The same canonical spec is fetched, parsed and initialized once, and every importer sees the same object.
- Mutating a module attribute is visible to every importer.
- At the top level of a module,
__name__is its canonical spec. Anif __name__ == "__main__":block is skipped on import. - The helpers of a module stay private to it. Attribute access goes through the module object, not the globals of the importer.
True True __main__
import_module(name) looks up a module bound by a plain import in the current scope. It can dispatch among modules already imported. A module pulled in only with from x import ... is not visible to it.
An import cycle, where a.py imports b.py and b.py imports a.py, raises RuntimeError: circular import at startup.
edge.json
Bare names resolve through edge.json, the only manifest name. Every field is optional.
{
"name": "charts",
"version": "0.1.0",
"description": "Draw a chart from a list of points.",
"repository": "https://github.com/you/charts",
"docs": "./docs",
"edge": "0.9.5",
"imports": {
"json": "0.1.0",
"utils": "./lib/utils.py",
"fastmath": "./vendor/fastmath.wasm"
},
"extends": ".."
}| Field | Meaning |
|---|---|
imports | Bare name to spec, a path, a URL or a major.minor.patch version of a registry package. One map holds every module, and the artifact behind each spec tells its kind. A version names a release only, and edge.lock says where its bytes are. |
permissions | Which system module the program and each package may use, and what each may reach, as module:scope entries per holder. See Permissions. |
extends | A directory whose edge.json answers a name not declared locally. Use it for monorepo sub-packages that share the dependencies of a parent. Omit it for hermetic libraries. A cycle in the chain fails at compile time. |
edge | The lowest engine the project runs on, major.minor.patch. edge init writes the version that scaffolded the project, and omitting it runs anywhere. |
name, version, description, repository, docs | What a package carries into a registry. The compiler ignores all five, and their rules are in Publishing. |
- No import name may be the name of a system module, such as
timeornet. Permissions grant those instead. - An engine older than
edgestops withthis project needs edge 1.0.0, this is 0.9.5. A dependency written for a later engine is refused where it loads, by the CLI and the browser alike. - Unknown keys are ignored.
- Values must be strings, lists of strings, or objects of them. Numbers and booleans are rejected.
- The string escapes are
\",\\,\/,\n,\tand\r.\uXXXXis not supported, so paste UTF-8 literally.
Resolution rules
- Walk-up. A bare name resolves against the nearest
edge.json, walking up from the directory of the importing file. Each manifest is a package boundary, like thenode_modulesdiscovery of Node. The chain is capped at 32 hops. - Hermetic. The nearest manifest wins. If it does not declare the name and has no
extends, the name is a system module the host serves to that package. Compilation fails when the host serves none or refuses it. A deep dependency cannot borrow the aliases of a parent. - Relative to the importer. A leading-dot spec resolves against the file that contains the import. A transitively imported
lib/a.pydoingfrom .b import gfindslib/b.py. - Spec shapes. See the table below.
| Spec | Read as |
|---|---|
Contains :// or starts with / | Used as is |
Starts with ./ or ../ | Joined against the directory of the importer |
| Three numeric parts | A version, which the host replaces with its locked URL before the compiler reads the manifest |
Any other spec with a / | Joined against the nearest edge.json directory |
| Anything else | A bare name for the walk-up |
edge.lock
A version says which release a project wants. The edge.lock beside its edge.json says where that release is. edge lock writes it and every other command reads it, so a run resolves a name without asking a registry.
{
"json": {
"version": "0.1.0",
"url": "https://cdn.edgepython.com/pkg/json/0.1.0/app.edge",
"digest": "sha256-0a68e05fc4b6e9110fccafd57802cab5c2a3ca64ed1d3b486ec2770f1a8c758a"
}
}- The lock holds one entry per declared name and nothing transitive. A published package carries its own dependencies inside its
.edge. - An entry for a bare URL records only the URL and the digest.
- A relative path has no entry. Its bytes are already in the project.
- A URL that carries its own
#sha256-has no entry. It already says what to expect. - A lock answers for the manifest it sits beside. A nested package and a packed
.edgeeach carry their own.
A declared version with no entry, or with an entry for another release, fails before anything compiles.
error: edge.json at 'edge.json': 'json' is not locked, run edge lockIntegrity
Append #sha256-<64 hex chars> to a spec in edge.json to pin its content. A locked version is pinned already.
{ "imports": { "utils": "https://example.com/utils.py#sha256-deadbeef0123456789abcdef0123456789abcdef0123456789abcdef01234567" } }A host hashes the raw bytes and refuses to run on a mismatch. The diagnostic shows both digests.
error: integrity check failed for 'https://example.com/utils.py'
expected sha256-deadbeef0123456789abcdef0123456789abcdef0123456789abcdef01234567
got sha256-36e4838513e46116f258c86b494eaa826d64fa0a9abdf36e8720a31b3d2862e2- Only
sha256is supported. Other prefixes fail withunrecognized integrity fragment. - The JS host checks the pin in its fetch layer. How a page caches modules is in JavaScript.
- The CLI checks the pin on every module it downloads or reads from disk,
.wasmplugins included. Its disk cache is in Cache.
Resolution errors
A bad import is a compile-time diagnostic with the source position of the statement. It is never a catchable runtime exception.
error: module 'utils' is not provided by this host and no edge.json declares it
--> main.py:1:6
|
1 | from utils import f
| ^^^^^
error: module 'json' has no export 'badname'
--> main.py:2:6- The first diagnostic is the same for a typo and for a package you forgot to declare.
- The CLI adds
help: declare it in edge.json, or use a relative import. - It also covers modules Edge Python does not ship, like
osorsys. They parse for syntactic compatibility and are rejected here, before any code runs.
Writing your own modules
A module of your own ships as a .py, or as a .wasm plugin. A plugin sees transit values and reaches the outside only through the system calls its package is granted.
Transit values are None, bool, int (128-bit), float, str, bytes, and nested list and dict. A plugin follows the sealed, language-agnostic ABI, written in Rust with wasm-pdk or in Zig, C or AssemblyScript. A script imports it through a manifest alias like any other module.