Skip to content

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:

wire
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 nameFile path
quic.varintquic/varint.wspec
quic.framesquic/frames.wspec
mpquic.pathmpquic/path.wspec
net.udpnet/udp.wspec

Imports

Use import to bring a type from another module into scope:

wire
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:

wire
module myapp.protocol

import quic.varint.VarInt
import ble.att.AttHandle
import net.udp.UdpDatagram

File Path Resolution

When the compiler encounters import quic.varint.VarInt, it resolves the module name quic.varint to a file path by:

  1. Converting dots to directory separators: quic.varintquic/varint
  2. Appending .wspec: quic/varint.wspec
  3. 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/:

bash
# 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)

bash
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:

bash
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:

bash
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

bash
wirespec check examples/quic/frames.wspec

Parses 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.c

When quic.frames imports quic.varint, the generated quic_frames.h automatically includes the dependency:

c
/* 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:

wire
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 → a

wirespec does not support circular dependencies. Restructure shared types into a common base module that both modules can import:

wire
# 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.Handle

Example: 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:

bash
# 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_frames

The dependency ordering is handled automatically: quic_varint.c is compiled first because quic_frames.c depends on it.