Reference

Directories, Mounting, and Process Control

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

These constructs were added for writing early-userspace/init-style programs in Vox - see examples/initramfs.vox for a complete, working early-userspace init sequence exercising all of them together.

Directories

Create a directory called "/proc".
Remove the directory called "/proc".
Delete the directory "/proc".
Change directory to "/newroot".

Rules:

  • Create a directory called '<path>'. - mkdir(2), mode 0755. The article (a) is optional; called is required.
  • Remove the directory called '<path>'. / Delete the directory "<path>". - rmdir(2). Both Remove and Delete work; the and called are optional.
  • Change directory to "<path>". - chdir(2).
  • All three set the error flag on failure - use On error to catch it.

Mounting Filesystems

Mount "proc" at "/proc" with type "proc".
Mount "tmpfs" at "/dev/shm" with type "tmpfs" with options "size=64m".
On error print "mount failed", exit 1.

Unmount "/dev/shm".
Unmount "/dev/shm" lazily.
On error print "unmount failed".

Rules:

  • Mount "<source>" at "<target>" with type "<fstype>" [with options "<options>"]. lowers directly to mount(2). source/target/fstype/options accept string literals, text variables, or buffers (including format-string-built buffers).

  • Moving/binding an already-mounted filesystem uses fstype "none" with options "move" or options "bind" - Vox recognizes this pattern and translates it into the correct MS_MOVE/MS_BIND mount flags:

    Mount "/proc" at "/newroot/proc" with type "none" with options "move".
  • Unmount "<target>". - umount2(2). umount is accepted as an alias for Unmount. Append lazily for MNT_DETACH (detaches immediately and releases the mount once nothing is using it any longer, instead of failing with "device busy") - needed when unmounting a filesystem your own running program was loaded from.

  • Both set the error flag on failure.

Device Nodes

Create a device node called "/dev/null" with type "c" major 1 minor 3.
Create a device node called "/dev/loop0" with type "b" major 7 minor 0.

mknod(2). type is "c" (character device) or "b" (block device); major/minor are the standard Linux device-driver identification numbers (see man 4 null/the kernel's Documentation/admin-guide/devices.txt for the registry of standard values). Sets the error flag on failure.

Create symbolic link from "/proc/self/fd" to "/dev/fd".

symlink(2): Create symbolic link from '<target>' to "<linkpath>". Sets the error flag on failure.

Switching the Root Filesystem

Pivot root to "/newroot" with old root "/newroot/oldroot".

pivot_root(2). put_old (the second path) must be a directory that already exists inside new_root - create it after mounting the new root, not before. After a successful pivot, the previous root filesystem is accessible at put_old's path relative to the new root (here, /oldroot), and should typically be released with Unmount "..." lazily once your program has chdir'd away from it. Sets the error flag on failure.

Executing Programs

Execute "/bin/sh".
Execute "/bin/echo" with arguments ["hello", "world"].

a list called cmdargs is ["hello", "world"].
Execute "/bin/echo" with arguments cmdargs.

On error print "execve failed", exit 1.

execve(2) - replaces the current process image entirely. Three forms:

  • No arguments: Execute "<path>". synthesizes argv = [path, NULL] (argc 1).
  • Literal argument list: Execute '<path>' with arguments [...]. - argv is built at compile time.
  • List variable: Execute '<path>' with arguments <list>. - argv is built at runtime from the list's current length and contents, sized and bounds-checked from that single length read so the argv array cannot be overrun regardless of the list's contents.

The environment is inherited from the calling process in all three forms. execve only ever returns on failure (there is no "success" path to return to - the process image is gone), so On error after Execute is the normal and only way to detect that it didn't work.

Process Control: fork and reap

Set pid to fork the process.
If pid is 0 then,
    (this branch runs in the child)
    Execute "/bin/some-program".
If pid is greater than 0 then,
    (this branch runs in the parent - pid holds the child's real PID)
    Set reaped to reap any child process.

These are expressions, not statements - use them anywhere an expression is valid (typically the right-hand side of Set/a number called ... is).

  • fork the process (the trailing the process is optional; bare fork also works) - fork(2). Returns 0 in the child, the child's PID in the parent, or a negative value on error. Sets the error flag on failure.
  • reap any child process - wait4(2) with pid = -1, waiting for any child. Returns the reaped child's PID, or a negative value on error.
  • reap process <pid-expr> / reap child <pid-expr> - wait4(2) for a specific PID.

Both set the error flag on failure (e.g. On error after reap process 999999 catches ECHILD when the PID is not actually your child).

Non-blocking reap: without waiting

Any reap form takes a without waiting suffix, which calls wait4(2) with WNOHANG instead of blocking:

Set r to reap any child process without waiting.
Set r to reap child pid without waiting.
Set r to reap process pid without waiting.

The return value is the whole point of the form, and the three cases must be told apart:

  • a child finished → its PID, error flag cleared;
  • children exist but none has finished → 0, error flag cleared (this is not an error — it is how you tell "still running" from "gone");
  • genuine error, e.g. no such child (ECHILD) → negative, error flag set, catchable with On error.

A non-blocking reap that returns 0 reaps nothing, so it does not disturb the reaped status (below) — only a reap that actually returns a child's PID changes it. without is already a reserved keyword (it is the print ... without newline token), so the suffix cannot be confused with a call argument after the pid expression, and waiting remains an ordinary identifier everywhere it is not this suffix.

The reaped status

Set r to reap child pid.
Set status to the reaped status.

the reaped status is an expression yielding the raw wait4 status word as a plain number — exactly the int status the kernel writes, undecoded. It reflects the most recent successful reap in the current process. Before any successful reap it is -1, a sentinel no real status can take, so "never reaped" is distinguishable from "exited 0". The sentinel lives in loader-initialized .data, not .bss, because _start (which would zero a .bss global) is only emitted for executables — a --shared library would otherwise read 0 and silently report "exited cleanly" with no child ever reaped.

reaped stays an ordinary identifier: the reaped status is consumed only as that exact phrase, and the reaped followed by anything else is an ordinary variable reference. (tests/102_fork_reap.vox does Set reaped to reap any child process. and keeps passing.)

Decoding the status

The compiler knows nothing about the wait-status encoding — the reaped status hands back the raw word, and a program decodes it with divide, modulo, and bit-and. Vox has no standard library on purpose, and this feature is complete with nothing installed:

To 'exit code of' with a number called status.
  Return a number, status divide 256 modulo 256.

To 'signal of' with a number called status.
  Return a number, status bit-and 127.

For ready-made decoding — these two plus crashed and 'exited normally', matching the <sys/wait.h> macros — the process library lives at Vox-lang/vox-libs, installable as an ordinary shared library:

see process version "0.1" from "./libprocess.lib".

It provides four functions over the raw status word, matching the <sys/wait.h> macros: 'exit code of' (bits 8–15), 'signal of' (the low 7 bits), crashed (true if a signal killed it), and 'exited normally' (true if no signal was involved). Use them at the call site, where they read as English:

If crashed of status then,
    Print "died by signal {'signal of' of status}".
If 'exited normally' of status then,
    Print "exit {'exit code of' of status}".

A supervisor loop, with no shelling out

These pieces compose into a complete supervisor — poll a child with non-blocking reap, time it out, kill it, and report how it died — using only Vox, no /bin/sh and no coreutils. examples/supervisor.vox is this loop as a runnable program, supervising both a job that finishes and a job that hangs:

see process version "0.1" from "./libprocess.lib".

Set pid to fork the process.
If pid is 0 then,
    Exit 0.

a timer called clock.
Start the clock.
a boolean called 'child is still running' is true.
a boolean called 'child was killed' is false.
While 'child is still running',
    Set 'reap result' to reap child pid without waiting,
    If 'reap result' is pid then,
        Set 'child is still running' to false.
    If 'child is still running' then,
        a number called 'milliseconds waited' is the clock's elapsed in milliseconds,
        If 'milliseconds waited' is greater than 5000 then,
            Send signal 9 to process pid,
            Set 'reap result' to reap child pid,
            Set 'child is still running' to false,
            Set 'child was killed' to true.
    If 'child is still running' then,
        Wait 10 milliseconds.

If 'child was killed' then,
    Print "hang".
If 'child was killed' is false then,
    Set status to the reaped status,
    If crashed of status then,
        Print "died by signal {'signal of' of status}".
    If 'exited normally' of status then,
        Print "exit {'exit code of' of status}".

A note on timing: the clock's elapsed in milliseconds reports true milliseconds, so the 5000-millisecond deadline above fires accurately at the five-second mark.

Send a signal: Send signal

Unlike fork/reap, this is a statement, not an expression:

Send signal <N-expr> to process <pid-expr>.

It performs kill(2) (syscall 62): <pid-expr> is the target PID (loaded into rdi), <N-expr> is the signal number (loaded into rsi). child is accepted as an alias for process, mirroring reap process/child:

Send signal 9 to child pid.

On success it clears the error flag; on failure (ESRCH no such process, EINVAL invalid signal, EPERM not permitted) it sets it, exactly like the other syscall statements, so On error catches the failure:

Send signal 0 to process 999999.
On error print "no such process".

Signal 0 is the standard existence check: it delivers nothing but returns an error if no process has that PID, which makes it a safe way to probe the error path. A common pattern is to send a real signal to a forked child and reap it:

Set pid to fork the process.
If pid is 0 then,
    Wait 30 seconds.
If pid is greater than 0 then,
    Send signal 9 to process pid.
    Set reaped to reap any child process.
    If reaped is pid then,
        Print "sigkilled child reaped with matching pid".

System Control: Shutdown, Reboot, Halt

Shutdown.
On error print "shutdown failed - are you root?".

Reboot.
Halt.

reboot(2), requiring CAP_SYS_BOOT (root). Each statement calls sync(2) first to flush filesystem buffers, then issues the matching command:

StatementAliasesCommand
ShutdownPoweroffLINUX_REBOOT_CMD_POWER_OFF
RebootRestartLINUX_REBOOT_CMD_RESTART
Halt-LINUX_REBOOT_CMD_HALT

On success, none of these return - the machine powers off/restarts/halts. On failure (not root, or no CAP_SYS_BOOT), the error flag is set instead of crashing or exiting, so On error safely catches the failure and execution continues - an unprivileged or accidental invocation can never bring down the machine.