Splitting a project into modules¶
Once a patch grows past one screen, splitting it into modules pays off: faster incremental builds, scoped symbol namespaces, and reusable helpers across hacks. This walkthrough builds a two-module project end to end.
Starting layout¶
a816.toml:
A reusable module¶
src/modules/vwf.s exports a small variable-width-font init routine:
"""VWF helpers shared across the hack."""
.scope vwf {
init:
"""Initialise VRAM tile slots used by the VWF renderer."""
lda.b #0x00
sta.w 0x2115
rts
; private — only callable from inside this module.
_zero_pad:
rep #0x20
lda.w #0x0000
rts
}
Symbols inside .scope vwf { ... } export as vwf.init. The leading
_ on _zero_pad keeps it LOCAL to the module — other modules cannot
reference it through the linker.
The entrypoint pulls it in¶
src/main.s:
"""Top-level patch."""
.import "vwf"
*= 0x008000
main:
"""Entry — run the VWF init then loop."""
jsl vwf.init
bra main
.import "vwf" resolves in this order:
vwf.oin--obj-dir(defaultbuild/obj).vwf.son a search path (-I/--module-pathor the same directory).
When only .s is available, the build driver compiles to .o first
on the fly.
Build¶
A single command does compile + link with auto-imports:
If you want to inspect the intermediates:
Cross-module references¶
When module A .imports module B, every runtime symbol B exports
(GLOBAL labels, alloc names, .incbin auto-symbols) is automatically
available as an extern in A. No explicit .extern needed — the
per-node import classifier emits the extern stubs from B's .o and
inlines B's compile-time content (structs, macros, typed binds,
pool decls) for codegen.
.extern name is still useful for symbols you don't want to import
the owning module for — build-script-injected constants, third-party
.o drops, or sub-symbols of a .label-declared name that the
auto-classifier doesn't reach.
The linker verifies every extern resolves to a definition and fails the build (with a useful message) if any are missing.
Constants over externs¶
You can compute compile-time constants from external symbols. The
expression is recorded in the .o's alias table and resolved at link
time:
Caveats: the alias's expression has to evaluate to a constant at link
time. Macros that reference externals via source >> 16 generally
work; constant assignments that the assembler tries to fold into a
single literal during compile (e.g., font_ptr := assets_menu_font_dat)
don't, so use the external symbol directly in the instruction.
Preamble¶
Put shared compile-time material (feature flags, .table defaults,
pool decls, typed binds, structs) in a .s file and import it
explicitly from each entry point:
The inline classifier picks up the preamble's compile-time symbols; runtime symbols become externs the linker resolves.
Lint as you go¶
The relevant rules for module work:
- DOC001 — every module needs a leading docstring.
- DOC002 — public macros / scopes / labels need attached docs.
- DOC003 — docstrings sitting outside their target's body get
flagged. Move them inside
{ ... }or above the label.
See Fluff (lint + format) for the full rule set and
the ; noqa suppression syntax.