# File I/O

This page is the "File I/O" section of the Voxlang language reference. It is generated from LANGUAGE.md in the Voxlang compiler's own repository, so it says what the specification says and nothing more.

A URL is the heading, lowercased, with punctuation dropped and spaces turned into hyphens. Each "##" section of the spec is a page under /docs/, and every heading inside it is a fragment on that page, so "File I/O" is /docs/file-io/ and "Reading a whole file" inside it is /docs/file-io/#reading-a-whole-file.

As data: https://vox-lang.dev/docs/index.json lists every section and every heading with its URL, https://vox-lang.dev/docs/search.json carries one entry per heading with its first sentence and keywords, and https://vox-lang.dev/docs/anchors.json maps every slug to the page it lives on. Search is also a JSON endpoint: https://vox-lang.dev/docs/search?q=<words> returns ranked results as JSON, no page load.

Source: LANGUAGE.md at bf2cba0, 2026-08-22. https://github.com/Vox-lang/vox/blob/bf2cba037976586c30d22a48e7b7246d347f54a1/LANGUAGE.md

[Reference](https://vox-lang.dev/docs/) > File I/O

## Buffers

Buffers are memory blocks for I/O operations. They come in two types:

### Dynamic Buffers (default)

```
a buffer called inputbuf.
a buffer called data.
```

**Features:**

- Start with zero capacity and grow automatically as needed
- No buffer overflows possible - memory expands dynamically
- Automatically freed on program exit

### Fixed-Size Buffers

```
a buffer called small is 256 bytes in size.
a buffer called large is 8192 bytes in size.
```

**Features:**

- Allocates exactly the specified capacity
- Does NOT grow - a read or write past capacity is truncated at capacity and sets the error flag
- Useful when you need predictable memory usage
- User programs can check buffer length to detect truncation
- Automatically freed on program exit

**The size bound.** A fixed buffer's size must be between 1 and 1073741824 bytes (1 GiB), and the bound holds however the size is written - a literal, a name, or a number the program only works out as it runs:

- A size the compiler can see is refused where it is written, whether that is the literal `a buffer called small is 0 bytes in size.` or a name whose value is fixed for the whole program.
- A size only run time can decide - one read from an argument, a file or a calculation - is refused when it is asked for. The buffer is made with no capacity and the error flag is raised, so `On error` catches it and the program carries on, exactly as it does for a fixed buffer that has become full.

A size of 0 is refused because a buffer with no fixed capacity is a *dynamic* buffer, which is declared with no size at all: `a buffer called small.`

**Truncation Behavior:** When reading into a fixed buffer that becomes full:

- Reading stops and sets an error flag
- Data beyond capacity is discarded
- Program continues normally
- Use `On error` to catch and handle the overflow

## Object Properties

Access properties of objects using the `'s` syntax:

```
a number called len is mybuffer's size.
print myfile's size.

If mybuffer's size is equal to mybuffer's capacity then,
    print "Buffer is full!".
```

### Universal Properties

Every variable has a `type` property that reports its declared type as text:

```
a number called n is 3.
a value called v is "hello".

print n's type.     (prints: Number (static))
print v's type.     (prints: Text (dynamic))
```

| Property | Description | Example |
| --- | --- | --- |
| `type` | Declared type name plus `(static)` or `(dynamic)` | `Number (static)`, `Text (dynamic)` |

Statically-typed variables (`number`, `float`, `text`, `boolean`, `list`, `map`, `buffer`, `file`, `time`, `timer`) report their type with `(static)` because the compiler knows the type from the declaration. A `value` variable reports whatever its runtime tag currently holds, so it always uses `(dynamic)`.

This property is intended for printing and logging. For type *tests*, use the `is a <type>` predicate — comparing the display string is stringly-typed and can drift from the predicate.

### Buffer Properties

| Property | Description | Type |
| --- | --- | --- |
| `size` | Current number of bytes stored | Number |
| `length` | Same as size | Number |
| `capacity` | Maximum bytes the buffer can hold | Number |
| `empty` | Whether the buffer has no data (size = 0) | Boolean |
| `full` | Whether size equals capacity (for fixed buffers) | Boolean |

**Example:**

```
a buffer called data is 256 bytes in size.
Read from file into data.

If data's full then,
    print "Buffer is at capacity".

If data's empty then,
    print "No data was read".
```

### Buffer Resizing

Resize a buffer to a new capacity:

```
a buffer called buf is 64 bytes in size.
resize buf to 256 bytes.
resize buf to 128.
```

**Keywords:** `resize`, `reallocate`, `grow`, `shrink`

**Behavior:**

- Data is preserved up to min(old_length, new_capacity)
- If shrinking below current data length, data is truncated
- New buffer is allocated and old buffer is freed
- Texts already made from the buffer with `as text` are independent copies, so resizing never disturbs them

### Buffer Byte Access

Read and write individual bytes in buffers and strings by position. Positions are **1-indexed** (like natural language: "the first byte", "the second byte").

**Reading bytes:**

```
a number called 'first' is byte 1 of data.
a number called 'byte value' is byte i of buf.
```

**Writing bytes:**

```
Set byte 1 of data to 0x48.
Set byte 2 of data to 'A'.
Set byte 3 of buf to value.
```

**Creating buffer from string:**

```
a buffer called buf is "Hello".
Set byte 1 of buf to 'J'.
Print buf.  (prints "Jello")
```

**Modifying string bytes:**

```
a buffer called msg is "Hello World".
Set byte 1 of msg to 'J'.
Print msg.  (prints "Jello")
```

**Bounds Checking:**

- Out-of-bounds access sets an error flag and returns 0
- Errors can be caught with `On error`
- Buffer overflow is impossible - the compiler enforces bounds

What "in bounds" means differs for a write and a read, and the worked example below depends on it. A **write** (`Set byte N of buf to ...`) accepts any position from 1 up to the buffer's *capacity*: writing past the current size extends `size` to that position, zero-filling any gap (a dynamic buffer grows its capacity as needed). A **read** (`byte N of buf`) accepts positions from 1 up to the current *size* only - a byte that has never been written or appended is out of bounds even when the capacity has room for it. Position 0 is out of bounds for both.

### Buffer Append and Copy

Efficiently combine buffers without byte-by-byte loops:

```vox fragment
append source to destination.
copy source to destination.
clear destination.

set destination to "line {n:06}\t{content}".
a buffer called destination is "line {n:06}\t{content}".
append "line {n:06}\t{content}" to destination.
copy "line {n:06}\t{content}" to destination.
```

**Behavior:**

- `append source to destination` adds source bytes to the end of destination.
- `copy source to destination` replaces destination contents with source bytes.
- `clear destination` sets destination length to `0` and preserves destination capacity.
- When destination is a buffer, format-string sources are supported for `set`, `is`, `append`, and `copy`.
- Format-string buffer writes are built in-place: literals/parts are appended directly to the destination buffer.
- Dynamic destination buffers grow automatically as needed.
- Fixed destination buffers truncate when full and set the error flag.
- Source buffer is never modified.

**Example:**

```
Create a buffer called data with size 16.
Set byte 1 of data to 0xDE.
Set byte 2 of data to 0xAD.
Set byte 3 of data to 0xBE.
Set byte 4 of data to 0xEF.

a number called b1 is byte 1 of data.
Print "First byte: {b1:02X}".

(Out of bounds - caught by error handler)
a number called bad is byte 100 of data.
On error print "Index out of bounds!".
```

### File Properties

| Property | Description | Type |
| --- | --- | --- |
| `size` | File size in bytes | Number |
| `descriptor` | Raw file descriptor number | Number |
| `readable` | Whether file is open for reading | Boolean |
| `writable` | Whether file is open for writing | Boolean |
| `modified` | Last modification time (Unix timestamp) | Number |
| `accessed` | Last access time (Unix timestamp) | Number |
| `permissions` | File permission bits (e.g., 0644) | Number |

**Example:**

```
open a file for reading called src at "./data.txt".

print src's size.
print src's modified.

If src's size is greater than 1048576 then,
    print "File is larger than 1MB".
```

**Checking whether a file exists.** There is no `exists` property: every property above describes a handle that is already open, and a file that did not exist could not have been opened, so `exists` on a handle would be trivially `true` and answer nothing. The question worth asking — "can this path be opened?" — is answered by opening it and catching the failure with `On error`, the same pattern used for every other file operation that can fail:

```
open a file for reading called present at "./data.txt".
On error print "data.txt: cannot be opened".
If present's descriptor is greater than -1 then,
    print "data.txt: exists".

open a file for reading called missing at "./no-such-file.txt".
On error print "no-such-file.txt: cannot be opened".
```

→ `data.txt: exists` then `no-such-file.txt: cannot be opened`

A path-level `exists` predicate — asked before opening, with no handle involved — is a planned future addition; today the `On error` idiom above is how a program finds out.

### List Properties (Object Properties)

| Property | Description | Type |
| --- | --- | --- |
| `length` | Number of items in the list | Number |
| `size` | Same as length | Number |
| `empty` | Whether the list has no items | Boolean |
| `first` | The first item in the list | Item |
| `last` | The last item in the list | Item |

**Example:**

```
a list called names is ["Alice", "Bob", "Charlie"].

print names's length.

If names's empty then,
    print "No names in list".
```

### List Element Access (Object Properties)

Access list elements by index. Indexes are **1-indexed** (like natural language: "the first element", "the second element").

**By index:**

```
a list called nums is [10, 20, 30].

Print element 1 of nums.   (prints 10)
Print element 2 of nums.   (prints 20)

a number called i is 2.
Print element i of nums.   (prints 20)
```

**By property:**

```
Print nums's first.        (prints 10)
Print nums's last.         (prints 30)
```

**Bounds Checking:**

- Out-of-bounds access sets an error flag. Where the compiler can prove the index is past the end, it returns the **number** 0 whatever the list's elements are, so read it into a `number`; where it cannot, it returns the destination's default value from the table under [Two Canonical Forms](https://vox-lang.dev/docs/variables/#two-canonical-forms) — `0` for a `number`, the empty text for a `text`, `[]` for a `list`, `{}` for a `map`
- Errors can be caught with `On error`

**Example with error handling:**

```
a list called items is [1, 2, 3].

a number called bad is element 100 of items.
On error print "Cannot access element 100 - out of bounds!".
```

### Number Properties

| Property | Description | Type |
| --- | --- | --- |
| `even` | Whether the number is even | Boolean |
| `odd` | Whether the number is odd | Boolean |
| `positive` | Whether the number is > 0 | Boolean |
| `negative` | Whether the number is < 0 | Boolean |
| `zero` | Whether the number is 0 | Boolean |
| `absolute` | Absolute value | Number |
| `sign` | -1, 0, or 1 | Number |

**Example:**

```
a number called x is -42.

If x's negative then,
    print "x is negative".

print x's absolute.
```

## Opening Files

Open files for reading, writing, or appending:

```
open a file for reading called source at "./data.txt".
open a file for writing called output at "./result.txt".
open a file for appending called log at "./log.txt".
```

You can also open an existing file descriptor directly by number:

```
open a file for reading called stdin_handle at 0.
open a file for writing called stdout_handle at 1.
open a file for writing called stderr_handle at 2.
```

When `at` is numeric, Vox treats it as a borrowed file descriptor instead of a filesystem path.

**Flexible argument order:** The clauses `for reading/writing/appending`, `called <name>`, and `at <path>` can appear in any order:

```
open a file at "./data.txt" for reading called source.
open a file called output for writing at "./result.txt".
open a file at "./log.txt" called log for appending.
```

**Modes:**

- `reading` - Read from existing file
- `writing` - Create/overwrite file
- `appending` - Add to end of file

**`at` value rules (compile-time validation):**

- Use text for filesystem paths: `at "/path/to/file"`
- Use integers for file descriptors: `at 0`, `at 1`, `at 2`
- File descriptor literals must be in range `0..2147483647`
- Invalid types (for example `at 1.5` or `at true`) are compile-time errors

## Reading

**At a glance:**

- Use **`Read from ... into ...`** when you want to read raw bytes in chunks.
- Use **`Read line from ... into ...`** when you want one logical line at a time.

High-level behavior:

- `Read` replaces the buffer's contents with the bytes read; each `Read` continues from the file's current position, so it is best for bulk/stream processing.
- `Read line` replaces the buffer with the next line and is best for line-by-line loops.
- Both can read from files or standard input.

Read from files or standard input into a buffer:

```
Read from standard input into buf.
Read from source into contents.
```

Read one logical line (up to `\n` or EOF) into a buffer:

```
Read line from source into linebuf.
Read line from standard input into linebuf.
```

**`Read line` behavior:**

- Includes the trailing newline in the buffer (when a newline is present)
- Returns an empty buffer at EOF
- Resets buffer contents before each read (replace, not append)
- For fixed-size buffers, overlong lines are truncated and set the error flag

## Seeking

Move a file descriptor position before reading:

```
Seek source to line 1.
Seek source to byte 1.
Seek source to bytes 128.
```

**Seeking rules:**

- Positions are **1-indexed** (`line 1` = start of file, `byte 1` = file offset 0)
- `Seek ... to line N` moves to the first byte of line `N`
- `Seek ... to byte N`/`bytes N` moves to byte position `N`
- Invalid targets (e.g. line past EOF, position < 1, invalid fd) set the error flag, which `On error` catches
- Line `N` exists when the file holds at least `N-1` newlines before it, so a file that ends in a newline has one empty last line to seek to; anything beyond that is past EOF and sets the flag

## Writing

Write strings, buffers, or special values to files:

```
Write "Hello, World!" to output.
Write buf to output.
Write a newline to output.
```

`Write` takes a text, a buffer, or a format string; a bare number, float, or boolean is a compile error, because a scalar holds a value where `Write` needs the address of some bytes. Render it with a format string instead:

```
a number called n is 72.
Write "{n}" to output.
```

A `value` is refused for the same reason — its type is only known at runtime, so the compiler cannot tell a text it could write from a number it could not. Copy it into a typed variable and write that:

```
a value called anything is "dynamic".
a text called settled is anything.
Write settled to output.
```

**Writing rules:**

- A failed `Write` sets the error flag and is catchable with `On error` — a write the system refused (no space, a handle opened for reading, a closed or never-opened handle) or one that transferred fewer bytes than asked for:

```
Write buf to output.
On error print "Write failed!", exit 1.
```

## Closing Files

Close file handles when done:

```
Close the source.
Close output.
```

## File Operations

Check if a file (or any path) is available:

```
If "data.txt" is available then,
    print "File found.".
```

`is available` (compiles to `access(2)` with `F_OK`) is the correct, current form of this check. It works on any path expression - string literal, text variable, or buffer - and is not limited to plain files; see [Directories, Mounting, and Process Control](https://vox-lang.dev/docs/directories-mounting-and-process-control/) for how it is used to poll for a device node.

Negate with `is not available`:

```
While the root_device is not available,
    Sleep for 100 milliseconds.
```

Delete a file:

```
Delete the file "data.txt".
```

## Error Handling

Operations that can fail (file reads, buffer operations, out-of-bounds access) set an error flag.

### On Error Handler

Check for errors after specific operations with `On error`:

```
Read from source into buf.
On error print "Read failed or buffer overflow!".
```

**Catchable Errors:**

- Out-of-bounds list/buffer access
- Fixed buffer overflow (data exceeds capacity)
- File operation failures — opening, seeking, reading, writing and deleting alike. A failed `Write` sets the flag, and so does a `Read from`, a `Read line from` or a `Write` on a handle whose own `open` failed.

**Error Handling Patterns:**

```
(Handle file read errors)
Read from file into buffer.
On error print "Read failed!", exit 1.

(Handle out-of-bounds access)
a number called item is element 100 of mylist.
On error print "Index out of bounds!".

(Check buffer state manually)
If buffer's size is equal to buffer's capacity then,
    print "Warning: buffer may have been truncated".
```

## Resource Safety

`vox` provides **memory safety** through automatic resource management.

### Memory Safety Guarantees

| Guarantee | How It's Enforced |
| --- | --- |
| No buffer overflows | Buffers grow dynamically as needed |
| No use-after-free | Resources tracked and cleaned at exit |
| No resource leaks | Automatic cleanup of all FDs and buffers |
| No manual memory management | Compiler handles allocation/deallocation |

### Automatic Cleanup

All resources are automatically cleaned up on program exit:

```
a buffer called data.                    (Auto-freed on exit)
open a file for writing called log at "x". (Auto-closed on exit)
(Even if you forget to close - it's handled!)
```

### Dynamic Buffers

Buffers start at zero capacity and grow automatically. No size specification needed:

```
a buffer called inputbuf.     (Grows as needed - never overflows)
Read from source into inputbuf. (Safe regardless of file size)
```

**Internal structure:**

- 8 bytes: capacity (current allocation size)
- 8 bytes: length (bytes used)
- N bytes: data (grows via reallocation)

### File Descriptor Tracking

Files are tracked at runtime for guaranteed cleanup:

1. **On open**: FD registered in tracking table
2. **On close**: FD unregistered from table
3. **On exit**: All remaining FDs automatically closed

This works correctly even with conditional file operations:

```
If condition is true then,
    open a file for writing called log at "debug.log",
    Write "Debug info" to log.
    (Close might be forgotten here - still safe!)
```

### Safety vs C Comparison

| Issue | C Behavior | Vox Behavior |
| --- | --- | --- |
| Buffer overflow | Undefined behavior, security vulnerability | Impossible - buffers auto-grow |
| Forgot to close file | Resource leak | Auto-closed on exit |
| Forgot to free memory | Memory leak | Auto-freed on exit |
| Double free | Undefined behavior | Tracked - can't happen |
| Use after free | Undefined behavior | Not possible by design |
