Modules¶
.import brings symbols from another module into the current translation
unit. The build driver discovers dependencies, topologically sorts them in a
stable order, and recompiles only the modules whose inputs changed (see
Incremental builds).
Resolving a name¶
.import "vwf" resolves in this order:
vwf.oin--obj-dir(defaultbuild/obj).vwf.son a search path (-I/--module-pathor the same directory).
If only .s is available it is compiled to .o first, then linked.
Symbol visibility¶
- Names starting with
_are LOCAL to their module. - All other names are GLOBAL and exported in the object file.
- Names declared inside
named_scope { ... }export asnamed_scope.name. - Anonymous
{ ... }blocks are scoped — labels declared inside never leak.
What .import actually brings in¶
.import "module" pairs two views of the imported module:
- Compile-time content comes from
module.s(struct defs, macros, constants, typed binds, nested.import,.pooldecls,.scopebodies). These get inlined into the importer's resolver so codegen sees their effects ((addr as Type).fieldresolves,MyMacro()expands, pool names register). - Runtime symbols come from
module.o(label addresses, alloc placements,.incbinbyte content). These surface asExternNodestubs in the importer's.o; the linker resolves each to the owner's single GLOBAL definition during merge.
Neither half is complete on its own — .o can't carry a struct def
(structs never get emitted as bytes); inlining the source would
duplicate the runtime symbols the .o already owns. The paired flow
lets a sub-module reach a parent's typed binds without needing
explicit .extern declarations for every label.
You can still write .extern name for symbols you want to reference
without .importing the owning module — useful for build-script
injected constants or third-party .o drops.
Cross-module references¶
Declare symbols defined in another module with .extern:
.extern external_func
.extern messages_vwf
.extern messages_vwf.init_commands_list ; sub-symbols need their own decl
main:
jsr.w external_func
rts
The linker verifies all externs are resolved and reports missing ones.
Constants over externs¶
name = expression is allowed even when expression references an extern.
The constant is recorded as a deferred alias and resolved at link time:
Workflow¶
Mixed source + object inputs work too:
Incremental builds¶
Compiled objects are cached in --obj-dir (default build/obj) next to a
<module>.deps sidecar listing every file the object was built from. A module
recompiles when:
- its object or
.depssidecar is missing, or - any recorded dependency (the module source, an
.included file, or an.incbin/.tableasset) is newer than the object, or - a module it
.imports recompiled (the importer bakes in the importee's exported constants, so a stale object would carry old values).
Editing a constant in an .included file therefore invalidates every module
that pulls it in; no rm -rf build/obj needed.
Reproducible output¶
Builds are deterministic: identical source produces an identical ROM,
independent of PYTHONHASHSEED. Module discovery, compilation, and pool
placement order are stable (dependencies are sorted, not iterated from a set),
so an address-sensitive bug surfaces the same way on every rebuild instead of
flickering with the interpreter's hash seed.
Auto-generated symbols¶
.incbin "data.bin" defines both the data label and a <label>__size
symbol with the byte count, so callers can do bounds checks without
tracking the length manually.
Pools across modules¶
.pool NAME { range ... } declared in one module is visible to any
module that .imports it. The decl serialises into both files'
.o; the linker merges decls by name (identical shape required —
mismatched ranges / fill / strategy is a hard error) so a shared
preamble can hand out pool names like client and engine and
sub-modules .alloc … in client against them without redeclaring.
See Freespace pools for .alloc, .relocate,
and .reclaim semantics.
Preamble¶
Shared compile-time material (feature flags, register-size hints,
.table configuration, pool decls, typed binds) lives in a .s
file imported explicitly from each entry point: .import "preamble"
at the top of main.s. The inline classifier picks up the
preamble's structs / pools / constants; runtime symbols become
externs the linker resolves.