Modules & Imports
wirespec supports multi-file projects through a module system. Every .wspec file belongs to a named module, and types can be imported across module boundaries.
Module Declarations
Every .wspec file begins with a module declaration that names the module:
module quic.varint
@strict
type VarInt = {
prefix: bits[2],
value: match prefix {
0b00 => bits[6],
0b01 => bits[14],
0b10 => bits[30],
0b11 => bits[62],
},
}The dotted module name maps directly to the file path. quic.varint corresponds to quic/varint.wspec (or quic/varint.wspec) on disk. The dots are directory separators.
| Module name | File path |
|---|---|
quic.varint | quic/varint.wspec |
quic.frames | quic/frames.wspec |
mpquic.path | mpquic/path.wspec |
net.udp | net/udp.wspec |
Imports
Use import to bring a type from another module into scope:
module quic.frames
@endian big
import quic.varint.VarInt
const MAX_CID_LENGTH: u8 = 20
packet AckRange {
gap: VarInt,
ack_range: VarInt,
}
frame QuicFrame = match frame_type: VarInt {
0x00 => Padding {},
0x01 => Ping {},
0x06 => Crypto {
offset: VarInt,
length: VarInt,
data: bytes[length],
},
# ...
_ => Unknown { data: bytes[remaining] },
}The import path quic.varint.VarInt has the form module.name.TypeName. The last component is the type being imported; everything before it is the module name.
Multiple imports are listed one per line:
module myapp.protocol
import quic.varint.VarInt
import ble.att.AttHandle
import net.udp.UdpDatagramFile Path Resolution
When the compiler encounters import quic.varint.VarInt, it resolves the module name quic.varint to a file path by:
- Converting dots to directory separators:
quic.varint→quic/varint - Appending
.wspec:quic/varint.wspec - Searching each include path in order until the file is found
The default include path is the current working directory. Add more search roots with -I path/:
# Resolves quic/varint.wspec relative to examples/
wirespec compile examples/quic/frames.wspec -I examples/ -o build/If the file cannot be found in any include path, the compiler reports an error:
error: module 'quic.varint' not found
searched: ./, examples/Compiling Multi-Module Projects
Single module (no imports)
wirespec compile examples/net/udp.wspec -o build/Module with imports
Pass the root module file and add the include path that contains the imported modules:
wirespec compile examples/quic/frames.wspec -I examples/ -o build/The compiler resolves all imports, compiles each dependency, and emits output for every module involved.
Recursive mode
--recursive compiles the given file and all transitive dependencies in one pass:
wirespec compile examples/quic/frames.wspec -I examples/ --recursive -o build/This is convenient for larger projects where the dependency graph is deep.
Check without generating code
wirespec check examples/quic/frames.wspecParses and type-checks the file without writing any output files. Useful in CI to validate .wspec files independently of the C build. Note that check only takes an input file — it does not accept -I flags.
Generated Output
Each module produces its own .h and .c pair. The module name becomes the file prefix, with dots replaced by underscores:
build/
quic_varint.h # quic.varint → quic_varint prefix
quic_varint.c
quic_frames.h # quic.frames → quic_frames prefix
quic_frames.cWhen quic.frames imports quic.varint, the generated quic_frames.h automatically includes the dependency:
/* Auto-generated by wirespec compiler -- DO NOT EDIT */
#ifndef WIRESPEC_QUIC_FRAMES_H
#define WIRESPEC_QUIC_FRAMES_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include "wirespec_runtime.h"
#include "quic_varint.h" /* imported: quic.varint.VarInt */
/* ... struct and function declarations ... */
#endif /* WIRESPEC_QUIC_FRAMES_H */No manual #include management is needed — the compiler tracks dependencies and generates the correct include directives.
Export and Visibility
By default, every type in a module is importable. For library modules where you want to hide internal types, prefix public definitions with export:
module mylib.codec
# Only PublicHeader is importable from outside this module
export packet PublicHeader {
version: u8,
length: u16,
}
# InternalHelper is not visible to importers
packet InternalHelper {
checksum: u32,
}The rule is:
- If any item in a module has
export, only exported items are visible to importers. - If no items have
export, everything is public (backward compatibility mode).
This means you can gradually add visibility control to an existing module without breaking existing importers — add export to the items you want to expose, and the rest become private.
Cycle Detection
Circular imports are detected at compile time:
error: circular import detected: a → b → awirespec does not support circular dependencies. Restructure shared types into a common base module that both modules can import:
# shared/types.wspec
module shared.types
export type Handle = u16le
# module_a.wspec
module module_a
import shared.types.Handle
# module_b.wspec
module module_b
import shared.types.HandleExample: QUIC VarInt + Frames
The QUIC example demonstrates a two-module project. quic/varint.wspec defines the variable-length integer encoding, and quic/frames.wspec imports it:
# Compile frames.wspec; the compiler finds varint.wspec via -I examples/
wirespec compile examples/quic/frames.wspec -I examples/ -o build/
# Build the generated C alongside your tests
cd build
gcc -Wall -Wextra -Werror -O2 -std=c11 -I../runtime \
-o test_frames quic_varint.c quic_frames.c test_quic_frames.c
./test_framesThe dependency ordering is handled automatically: quic_varint.c is compiled first because quic_frames.c depends on it.