Reference

Libraries and Imports

Generated from LANGUAGE.md at bf2cba0, 2026-08-22.

The see Keyword

see pulls in code from another file. It has two distinct jobs:

  • see "<path>.vox". — include another Vox source file. This works today: the file is parsed as part of your program, so its functions become callable with no linking step. It is how you split a program across files.
  • see '<lib>' version "<ver>" from "<path>.lib". — consume a shared library through its .lib interface. This is the library path; see Shared libraries below.
see "./utils.vox".
see mathkit version "1.0" from "./libmathkit.lib".

There is exactly one library form. Three other shapes — see "./path.so"., see "lib" version "1.0" from "./path.so"., and see "./path.so" for "lib" version "1.0". — point see at a .so directly. A .so is binary ELF: it carries mangled symbol names but no Vox type information, so the compiler cannot check a call against it. All three are refused: see of a .so errors and directs you to the .lib, and the see ... for ... form has its own diagnostic — both name the canonical form see '<lib>' version "<x.y>" from "<path>.lib"..

Search paths. see resolves the path by its shape:

  • ./… or ../… — resolved against the directory of the file that contains the see statement, and only there.
  • /… — used as-is (absolute).
  • a bare name — /usr/share/vox/lib/<name> is checked first, and only if that does not exist does it fall back to the containing file's directory. Watch this: a bare see "utils.vox". can silently pick up a system file in preference to the one sitting next to your source. Write ./utils.vox when you mean the local one.

Those three shapes describe a .vox source include. A .lib path resolves differently: relative or bare, it is tried against the containing file's directory first and then each --lib-path directory, and its Location .so against the .lib's own directory first and then --lib-path; absolute paths are used as-is. --lib-path is not consulted for a .vox include at all; for --link it only passes search paths to the linker (-L). See Consuming a library.

Circular includes. The compiler tracks files already seen and skips a see that would re-enter one.

Shared libraries

A shared library is a .so you build from Vox and call from Vox — or from C, Rust, or any other host. The chain is:

.voxsee a .libLocation.so

The .lib is the typed interface (the .h equivalent); the .so it points at is linked, never read for types. This section covers writing one, the .lib file, consuming one, putting several libraries in one .so, and the symbol names a non-Vox caller needs.

What runs today. The whole path runs: building a library with --shared produces a self-contained .so plus its .lib interface, see of a .lib consumes it from Vox, export names are mangled, and multi-input --shared links several libraries (and several versions of one library) into one .so. A foreign host can also call the .so directly — see Calling a library from a non-Vox host.

Writing a library

Add a Library declaration at the top of a .vox file, then build with --shared:

Library mathkit version "1.0".

To 'add two numbers' with a number called x and a number called y. Return a number, x add y.

To greet.
  Print "hello from mathkit".
vox mathkit_lib.vox --shared -o libmathkit.so

That writes two files: libmathkit.so, and libmathkit.lib beside it — the typed interface a Vox consumer sees, described in The .lib file below. The .lib name comes from -o, so the pair always travels together.

This compiles to a self-contained shared object. It carries its own copy of the Vox runtime, so it is loadable from C, Rust, or any other host — not only from Vox. The runtime is position-independent, so a library may use the full core language — arithmetic, printing, buffers, files, floats, lists, maps — not a runtime-free subset. Only the library's own function definitions are exported; every runtime symbol is kept out of the dynamic symbol table.

Verify what you built:

$ nm -D --defined-only libmathkit.so
000000000000072c T mathkit_1_0_add_two_numbers
000000000000076c T mathkit_1_0_greet
$ readelf -r libmathkit.so
There are no relocations in this file.

Two exports and nothing else leaked; zero absolute relocations, so the whole object is position-independent. The labels are the mangled <library>_<version>_<func> form — mathkit_1_0_add_two_numbers and mathkit_1_0_greet — so two versions of one library can live in one .so without colliding; see Mangling below.

A library needs an identity. The Library declaration gives the library the name and version that drive mangling and the .lib. A --shared build with no Library line has no identity and is rejected — add the declaration.

Top-level statements are rejected. A shared library has no entry point, so a top-level executable statement (Print, assignment, If, a bare function call, …) would be silently dropped. The compiler rejects it instead:

error: Top-level print statement is not allowed in a shared library: only
function definitions, 'Library', and 'see' may appear at the top level.

Only function definitions, Library, and see may appear at the top level of a --shared compile. Put any work you need inside a function.

An empty library is rejected. A --shared compile with no function definitions exports nothing, which would yield a malformed version script and an opaque linker error. The compiler rejects it instead: a shared library must export at least one function.

The .lib file

The .lib is the public interface to a library: its name and version, where its .so is, and a table of contents of every exported function's signature. It is what a consumer sees, and the only place Vox types live — ELF carries mangled names but no types, so the .lib is the type source. A --shared build writes <output-stem>.lib beside the .so, one Library block per input. The .lib is a declared output like the .so, derived from the same -o: a rebuild overwrites it in place, so an edit-build loop needs no manual cleanup and anything hand-edited into a .lib is lost. The pair is written together, so a fresh .so never lands beside a stale .lib. The format:

Library mathkit version "1.0".
Location "./libmathkit.so".

Table of Contents:
    To 'add two numbers' with a number called x and a number called y, returning a number.
    To greet.
  • Library '<name>' version "<ver>". — the block's identity. Several Library blocks may appear in one .lib, each with its own Location; parsing runs to EOF, and a Library line starts a new block.
  • Location "<path>". — where the .so is. It resolves relative to the .lib first, then --lib-path, then error. Absolute paths are honoured but never generated, so a .lib is portable.
  • Table of Contents: — one line per exported function, in the same 11-type vocabulary as Vox source, in EITHER position: number, float, text, boolean, list, map, buffer, file, time, timer, value; anything else is an error naming the unsupported type. void isn't a spelling — a function returning nothing omits the , returning clause entirely — and neither is unknown, the compiler's own internal placeholder for an untyped parameter. A user-defined thing has no noun here either, so a --shared build refuses an export that takes or returns one — see .lib export of a thing is not yet supported.
  • A list may optionally carry its element type: a list of text called out, returning a list of text. This is compiler-inferred, not author- declared — Vox source has no generic/typed-collection syntax, so a library author still just writes a list called out; a --shared build scans the exported function's own body and, when every appended/returned element provably agrees on one type, writes list of <type> for you. Disagreement or no evidence emits plain untyped list, same as always. map's value type isn't carried this way; map stays element-untyped in both positions.
  • , returning a <type> exists only in .lib files. Vox source declares return types in the body (Return a number, x.), which a bodiless .lib declaration has no room for. No returning clause means the function returns nothing — which is also why a list's element type only shows up in the .lib when the exporting function has a declared return type (Return a list, out.) in the first place; a bare Return out. records no return type at all, list-element-typing included.

A .lib is lexed with the Vox lexer but parsed by a dedicated parser, so it cannot carry executable statements — only the interface above.

Consuming a library

see mathkit version "1.0" from "./libmathkit.lib".

a number called sum is 'add two numbers' of 3 and 4.
Print the sum.
$ vox mathkit_consumer.vox -o consumer
$ ./consumer
7

That is the whole consumer build — no --link, no -l, no -L. The see does the linking, because the .lib says where the .so is.

see of a .lib is the consumption path. The compiler:

  1. Resolves the .lib (relative to the source, then --lib-path).
  2. Parses it and selects the block matching name and version.
  3. Resolves Location relative to the .lib, then --lib-path.
  4. Verifies against the .so's dynamic symbol table — every mangled name the .lib promises must exist in the .so. This is the staleness check: a .lib that lies about a function is a compile error, not a runtime crash.
  5. Registers the signatures, so calls type-check like any other function.
  6. Emits extern <mangled> for each used function and adds the .so and an -rpath to the link line.

The -rpath in step 6 is the directory the .so was found in, recorded as an absolute RUNPATH. So the program finds its library where it stood at build time; move the .so afterwards and the loader will not find it, unless LD_LIBRARY_PATH points at the new directory.

Each failure is its own diagnostic naming the file and what was expected: missing .lib; no such library in it; version mismatch, listing the versions the .lib does offer; missing .so at Location; symbol absent from the .so (the stale-.lib case — it names the symbol); arity or type mismatch at the call site; and reading the result of an entry that has no , returning clause, which returns nothing, so there is no value to read — call it as a statement instead.

The stale-.lib case is the one you meet by hand-editing a .lib or by rebuilding a library with different exports:

Error: the .lib entry 'To 'ghostgreet' ...' promises the symbol
'mathkit_1_0_ghostgreet', but 'libmathkit.so' does not export it (not in
.dynsym).
The .lib is stale: it does not match the library binary. Rebuild the library
with `vox --shared` to regenerate the pair.

The worked example set in examples/ shows the workflow: mathkit_lib.vox is the library and mathkit_consumer.vox is the Vox consumer above. A foreign caller — C, Rust, or assembly linking the .so directly — is shown in Calling a library from a non-Vox host below.

Several libraries in one .so

vox a.vox b.vox --shared -o lib.so links several libraries into one .so in a single step — you cannot append to a linked .so, so one link step is the only way to combine them. The sources are parsed independently and then compiled into one unit, so the runtime is included once and shared by every library in the .so.

The reason this exists is backwards compatibility: two versions of the same library can live in one .so, kept apart by mangling. A consumer who upgrades the library keeps calling mathkit_1_0_add_two_numbers after mathkit_2_0_add_two_numbers ships beside it, with no recompile — both symbols are present and independently callable. That version isolation is why the whole design looks the way it does, and it is the case to keep in mind when the rest of it seems elaborate.

Duplicate <library, version> pairs across inputs are rejected with both filenames. Multi-input is --shared only; it is rejected for executable builds, where the semantics would be ambiguous.

Mangling

Every exported function is mangled to a single flat label:

<library>_<version>_<func>

mathkit + 1.0 + add two numbersmathkit_1_0_add_two_numbers. Each component is sanitized by mapping every character outside [A-Za-z0-9_] to _. The leading-digit prefix (a digit is not a legal C identifier start) applies only to the library component, which begins the symbol; the version and function components are interior and take the sanitizer alone, so the version 1.0 appears as 1_0 (the . becomes a single _, no prefix — applying the prefix per component would yield mathkit__1_0_add_two_numbers, a double underscore). A non-Vox caller — C, Rust, anything that links the .so — needs this mangled name to call the function at all, which is why the scheme is documented here and not only in docs/SYMBOL_MANGLING.md (the full rules, including what is and is not mangled). There is no unmangled alias: an alias would defeat the version isolation above.

Runtime state is not mangled — a deliberate non-goal. The runtime is emitted once per .so and shared by every library in it (one resource table, one .fini_array, one idempotent cleanup). Cross-.so isolation holds because each .so carries its own runtime and the version script hides it. Only function labels are mangled. See docs/SYMBOL_MANGLING.md.

Calling a library from a non-Vox host

A shared library is a plain .so, so any caller that can link one can use it — C, Rust, or hand-written assembly. This is also the case the mangling scheme above exists for: the foreign caller must name the export by its mangled label. Build the example library, then call it from a small assembly driver (nasm + ld only — the tools Vox already requires). Run these from the examples/ directory:

$ vox mathkit_lib.vox --shared -o libmathkit.so
$ nm -D --defined-only libmathkit.so
000000000000072c T mathkit_1_0_add_two_numbers
000000000000076c T mathkit_1_0_greet
; mathkit_driver.asm — link against libmathkit.so and call its exports.
global _start
extern mathkit_1_0_add_two_numbers
extern mathkit_1_0_greet

section .text
_start:
    and rsp, -16            ; 16-byte stack alignment for the Vox prologue
    mov rdi, 3
    mov rsi, 4
    call mathkit_1_0_add_two_numbers  ; mathkit_1_0_add_two_numbers(3, 4) -> 7, in rax
    cmp rax, 7
    jne .fail
    call mathkit_1_0_greet            ; prints "hello from mathkit"
    mov rax, 60             ; SYS_exit
    xor rdi, rdi
    syscall
.fail:
    mov rax, 60
    mov rdi, 2
    syscall
$ nasm -f elf64 -o mathkit_driver.o mathkit_driver.asm
$ ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 -rpath '$ORIGIN' \
      -o mathkit_driver mathkit_driver.o -L. -lmathkit
$ ./mathkit_driver
hello from mathkit

The driver declares the exports extern and calls them with the Vox calling convention: integer arguments in rdi, rsi, … and the result in rax. -rpath '$ORIGIN' makes it find libmathkit.so in its own directory, so the pair is relocatable. (The .asm extension is gitignored under examples/ because the compiler emits .asm there as output, so this driver is shown here rather than tracked as a file — copy it out to run it.) The extern names are the mangled labels mathkit_1_0_add_two_numbers and mathkit_1_0_greet, matching what nm -D showed above.

Linking an executable against a .so directly

If the library has a .lib, you do not need this: see it, which registers its signatures and links its .so. --link is for a .so with no .lib — foreign, or hand-built — where the compiler knows no Vox signatures and only the linker is involved.

--link puts a built .so on the link line of an executable. It takes the library's soname stem — the part between lib and .so — so a file named libmath.so is linked as --link math:

$ vox hello.vox --link math --lib-path ./libs -o hello
$ readelf -d hello | grep -E 'NEEDED|RUNPATH'
 0x0000000000000001 (NEEDED)             Shared library: [libmath.so]
 0x000000000000001d (RUNPATH)            Library runpath: [./libs]

Because such an executable needs the dynamic loader at runtime, --link automatically adds the loader (/lib64/ld-linux-x86-64.so.2) and an -rpath for each --lib-path — but only when libraries are actually linked, so a plain vox hello.vox build stays a flat static binary with no loader dependency.

--link alone does not teach the compiler a library's function signatures, so it does not let Vox source call the library's functions — that is what see of a .lib does (it registers the signatures and adds the .so to the link line). --link is for the case where the program already references the symbols another way, or for a non-Vox driver assembled by hand — the Calling a library from a non-Vox host driver above is exactly that, linked with ld rather than --link.