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 errorcatches 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 errorto 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 textare 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:
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 destinationadds source bytes to the end of destination.copy source to destinationreplaces destination contents with source bytes.clear destinationsets destination length to0and preserves destination capacity.- When destination is a buffer, format-string sources are supported for
set,is,append, andcopy. - 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 —0for anumber, the empty text for atext,[]for alist,{}for amap - 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 filewriting- Create/overwrite fileappending- 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.5orat 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:
Readreplaces the buffer's contents with the bytes read; eachReadcontinues from the file's current position, so it is best for bulk/stream processing.Read linereplaces 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 Nmoves to the first byte of lineNSeek ... to byte N/bytes Nmoves to byte positionN- Invalid targets (e.g. line past EOF, position < 1, invalid fd) set the error flag, which
On errorcatches - Line
Nexists when the file holds at leastN-1newlines 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
Writesets the error flag and is catchable withOn 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 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
Writesets the flag, and so does aRead from, aRead line fromor aWriteon a handle whose ownopenfailed.
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:
- On open: FD registered in tracking table
- On close: FD unregistered from table
- 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 |