Language reference
Filesystem & data
Vision reads and writes files, walks directories, and talks to SQLite through a sealed, library-owned handle. Every act on the disk is checked, fallible, and owned; the compiler never silently guesses a path or a type.
Every rule below describes the current Vision compiler. For the shorter safety model, read Safe by design. The Host and C interop reference covers the raw C escape hatch and library discovery that this page builds on; the Effects and failure reference documents the failure-clause grammar this page uses.
File I/O
A file is addressed by a path, which is a plain text expression — a string literal, a variable, an interpolation, or anything else that yields text. The file itself is one of two types in the language: reading returns a list of bytes (its raw bytes), and writing takes one. There is no "open" verb, no file handle, and no separate close: every act is one statement that opens, performs one operation, and releases.
Each act is fallible the one honest way its operation can refuse. read raises | missing: when the file is not there and | unreadable: when the bytes cannot be read; read returns raw bytes, so the UTF-8 check lives one step later on the bytes-to-text bridge — <bytes> decoded as text returning <name> raises | badtext: on invalid UTF-8. The encoded as bytes shape that turns text into a byte list is pure. The write family raises | unwritable: for any refusal — a missing directory, a full disk, a permission denial. The failure clauses ride at the end of the same indented block as a verb call; the grammar is identical. The Effects and failure reference documents the full failure-tag table.
Every disk-touching line must carry the >> marker (read, write, mkdir, remove, rename, save). The compiler refuses the program otherwise with a specific message naming the line. A line that merely reads file size or mtime also carries >>; only the in-process encoded as bytes conversion between text and bytes is pure.
The lifecycle of the bytes you read is the same as any owned result: the call transfers the bytes to the named result, and they are freed at scope close. The path itself is just text, borrowed or owned according to the rules of the expression that produced it. There is no handle to close and no resource to release by hand.
Directory iteration
each entry in the directory at <path> walks one level of a directory. The loop variable binds a built-in described thing, directory entry, with two fields: name (text, the file or sub-directory name — never including the parent path) and kind (the entry kind enum, currently one of file, directory, or symlink). The each opener runs the loop body once per immediate child, in sorted name order; the entries. and .. are skipped.
The walk is a world-touch (>>) and can fail the one way reading a directory can refuse — it cannot be opened. The required clause is | unreadable:, and it rides in the same indented block as the body. A missing or unreadable directory raises | unreadable:; there is no separate "absent" tag for directory iteration.
The entry-kind enum is total but optionally absent: a path that does not exist reads back as nothing, so reading the kind requires the same when there is presence guard as any other nullable value. Inside the guard, the kind interpolates directly. The built-in enum and described thing are sealed — you cannot add members or fields, and the compiler does not let you redeclare entry kind or directory entry.
>> to begin:
>> make the directory at "tree/sub"
| unwritable: stop
the hello is "hello"
the data is the hello encoded as bytes
>> write the data to the file at "tree/a.txt"
| unwritable: stop
the more is "world"
the bytes is the more encoded as bytes
>> write the bytes to the file at "tree/sub/b.txt"
| unwritable: stop
the greeting is "hi"
the mark is the greeting encoded as bytes
>> write the mark to the file at "tree/c.txt"
| unwritable: stop
say "src entries:"
>> each entry in the directory at "tree":
say " {the name of the entry} is a {the kind of the entry}"
| unreadable: stop
say "recursive walk:"
the total is 0
>> each file under "tree":
>> the size of the file at the file returning the sz
| missing: stop
the total is the total plus the sz
| unreadable: stop
say " total bytes: {the total}"
>> !! remove everything under "tree/sub"
| unwritable: stop
say "after rmtree:"
>> each file under "tree":
say " {the file}"
| unreadable: stop>> to begin:
>> make the directory at "tree/sub"
| unwritable: stop
the hello is "hello"
the data is the hello encoded as bytes
>> write the data to the file at "tree/a.txt"
| unwritable: stop
the more is "world"
the bytes is the more encoded as bytes
>> write the bytes to the file at "tree/sub/b.txt"
| unwritable: stop
the greeting is "hi"
the mark is the greeting encoded as bytes
>> write the mark to the file at "tree/c.txt"
| unwritable: stop
say "src entries:"
>> each entry in the directory at "tree":
say " {the name of the entry} is a {the kind of the entry}"
| unreadable: stop
say "recursive walk:"
the total is 0
>> each file under "tree":
>> the size of the file at the file returning the sz
| missing: stop
the total is the total plus the sz
| unreadable: stop
say " total bytes: {the total}"
>> !! remove everything under "tree/sub"
| unwritable: stop
say "after rmtree:"
>> each file under "tree":
say " {the file}"
| unreadable: stopRecursive walk
each file under <path> yields, recursively, every regular file and symlink path under the named directory. The loop variable binds a text — the full path of each yielded entry — so reading the path inside the body is just the file. The walk descends directories depth-first inreaddir order (the order the underlying filesystem reports them, not sorted) and only sorts the final collected file list with the same comparator the directory-iteration form uses (vis_name_cmp). A recursive walk therefore yields files in sorted name order overall, even though the descent itself walks each level in readdir order.
The walk is symlink-SAFE: a symlink inside the tree is yielded as a path and is never descended. The runtime checks each child with lstat, so a symlink is reported as a symlink, not as the target's kind, and the walk does not follow it. A symlink that points outside the tree therefore cannot escape the walk's bounds.
The walk is one world-touch (>>) and fails the same way as the one-level form: | unreadable: when the named root cannot be opened. A directory that becomes unreadable after the walk begins is silently skipped; the walk does not fail mid-stream.
File stat / metadata
Two metadata reads bind a whole-number result: the size of the file at <path> returning <name> and when the file at <path> was last changed returning <name>. Both are fallible value reads shaped like read the file at ... returning ...: the value rides a statement with its own | missing: block, because Vision has no fallible expression — a value that might fail must be carried by a statement with a failure clause. mtime is whole seconds since the Unix epoch, the same unit as the time now, so the two are directly comparable.
Three more reads are total expressions, not statements: the kind of the thing at <path> (the entry-kind enum, or nothing if the path is absent — read with when there is), when there is a file at <path> (yes/no existence), and when the file at <path> can be read (yes/no R_OK). Each is a world-touch (>>) but is never fallible; the kind read is nullable and requires a presence guard, the existence and access reads are yes/no.
Filesystem acts beyond the basics
Three additional acts sit beside the basic file verbs:
>> !! remove everything under <path>— the recursivermtree. It deletes the named path and everything beneath it. Both>>(it touches the disk) and!!(it is irreversible; deleted files cannot be recovered). Failure tag:| unwritable:("it can't be removed"). The walk is symlink-SAFE: a symlink inside the tree is unlinked, never descended, so a link inside the tree can never delete its target outside the tree.the real path of <path> returning <name>— resolve symlinks and..to an absolute path. The result is owned text. Failure tag:| missing:. The line is a world-touch (>>) and is not marked!!— resolution is a read.>> make a link at <linkpath> pointing to <target>— make a symlink at the named link path pointing to the target. Failure tag:| unwritable:. The line is a world-touch (>>) but is not marked!!— a new link overwrites nothing.
>> to begin:
>> make the directory at "real"
| unwritable: stop
the hello is "hi"
the data is the hello encoded as bytes
>> write the data to the file at "real/file.txt"
| unwritable: stop
>> make a link at "real/link" pointing to "file.txt"
| unwritable: stop
>> the real path of "real/link" returning the linktarget
| missing: say "missing" and stop
when the linktarget ends with "real/file.txt":
say "symlink real path ends with real/file.txt"
when the linktarget starts with "/":
say "symlink real path is absolute"
>> the linkkind is the kind of the thing at "real/link"
when there is the linkkind:
say "link reports as a {the linkkind}"
>> the filekind is the kind of the thing at "real/file.txt"
when there is the filekind:
say "file reports as a {the filekind}">> to begin:
>> make the directory at "real"
| unwritable: stop
the hello is "hi"
the data is the hello encoded as bytes
>> write the data to the file at "real/file.txt"
| unwritable: stop
>> make a link at "real/link" pointing to "file.txt"
| unwritable: stop
>> the real path of "real/link" returning the linktarget
| missing: say "missing" and stop
when the linktarget ends with "real/file.txt":
say "symlink real path ends with real/file.txt"
when the linktarget starts with "/":
say "symlink real path is absolute"
>> the linkkind is the kind of the thing at "real/link"
when there is the linkkind:
say "link reports as a {the linkkind}"
>> the filekind is the kind of the thing at "real/file.txt"
when there is the filekind:
say "file reports as a {the filekind}"Database (T_DB)
SQLite is reached through a library discovery binding, exactly like every other C library: this program understands sqlite3 at the top of the file. The compiler discovers the installed header and emits the matching #include <sqlite3.h> and link flag (the build also reads the //vision:link directive the compiler emits). Without the binding, every database act is rejected with the db-needs-binding diagnostic, which points straight at the missing understands sqlite3 clause; the understands clause is not optional.
Three statements cover the whole database surface:
>> open the database at <path> returning <db>— open (or create) a connection.<path>is a text expression; pass":memory:"for an in-memory database. The handle binds to<db>and is closed automatically at scope close — the same rule that governs every other opaque, library-owned value (T_DB, T_CONN, T_PROC). There is nocloseverb, no handle field, and no way to read the connection's interior from the surface;<db>is opaque and never inspectable. Failure tag:| failed:("the database can't be opened").>> ask <db> to run <sql>— execute a raw SQL statement that yields no rows (create, insert, update, delete).<sql>is a string literal or an interpolation template. Failure tag:| failed:. Theto runverb names the action explicitly so the grammar is unambiguous with the returning form below.>> ask <db> to run <sql> returning <rows> as a list of <thing>— execute a SELECT and fill a list of rows.<thing>must be a described-thing kind whose field names match the result column names; the compiler fills each row by name (case-sensitive, column-by-column). Failure tag:| failed:. The result is an ownedlist of <thing>, freed at scope close. The selector and the described thing must both resolve to declared kinds — a misspelled thing name is rejected withunknown-row-thing.
Argument values are bound through ? placeholders in the SQL text: ask <db> to run <sql> with <v> and <v> .... The values are joined with and, the same connective the run-children verb uses for its argv, and they are bound positionally through sqlite3_prepare_v2 + sqlite3_bind_* — not spliced into the SQL text, so a user value cannot produce SQL injection. The with clause is optional; a statement with no placeholders omits it entirely.
The database handle is a T_DB, the same opaque-handle family as T_CONN (a network connection inside a serve block) and T_PROC (a live child process). None of them can be inspected from the surface, none of them can be sent across a strand, and all of them are reclaimed at scope close — the compiler's scope walk closes the database, reaps the child, and frees the owned text the same way. The handle is hoisted to the verb's top-level scope so every exit path (an answer, a stop, a pass) releases it correctly.
this program understands sqlite3
kind row:
msg is text
n is whole
>> to begin:
>> open the database at ":memory:" returning the db
| failed: say "open failed" and stop
>> ask the db to run "create table greeting (msg text, n integer)"
| failed: say "create failed" and stop
>> ask the db to run "insert into greeting (msg, n) values (?, ?)" with "hello" and 1
| failed: say "insert failed" and stop
>> ask the db to run "insert into greeting (msg, n) values (?, ?)" with "world" and 2
| failed: say "insert failed" and stop
>> ask the db to run "select msg, n from greeting order by n" returning the rows as a list of row
| failed: say "select failed" and stop
say "rows: {how many are in the rows}"
each item in the rows:
say " {the msg of the item}={the n of the item}"this program understands sqlite3
kind row:
msg is text
n is whole
>> to begin:
>> open the database at ":memory:" returning the db
| failed: say "open failed" and stop
>> ask the db to run "create table greeting (msg text, n integer)"
| failed: say "create failed" and stop
>> ask the db to run "insert into greeting (msg, n) values (?, ?)" with "hello" and 1
| failed: say "insert failed" and stop
>> ask the db to run "insert into greeting (msg, n) values (?, ?)" with "world" and 2
| failed: say "insert failed" and stop
>> ask the db to run "select msg, n from greeting order by n" returning the rows as a list of row
| failed: say "select failed" and stop
say "rows: {how many are in the rows}"
each item in the rows:
say " {the msg of the item}={the n of the item}"Ownership and cleanup
The filesystem and database surface hands ownership back through the same B1 return rule every other Vision verb uses: an already-owned heap result transfers to the caller, while a borrowed parameter, field, or list item is cloned at the boundary so the caller owns an independent copy. A read ... returning ... hands an owned list of bytes; a the real path of ... returning ... hands owned text; a database ask ... returning ... as a list of <thing> hands an owned list of owned rows.
A caller-owned result that is ignored, overwritten, or leaves its scope is reclaimed by the same scope-close walk. The path itself is text, owned or borrowed according to where it came from; an interpolation that produces a path is owned text, freed by the same walk. The directory loop's snapshot names and the recursive walk's collected paths are freed by a post-loop sweep (the diriter walks its sorted name array and frees each entry after the body closes; the walk calls vis_walk_free after the body for-loop closes), so no per-iteration allocation outlives its body and the whole surface is ASan-clean.
The database handle is the only resource that follows a different rule: it is closed at scope close by an explicit vis_db_close call the scope-close walk emits for every handle registered when the handle was opened. Reopening the same handle name within a verb first closes the previous one (NULL-safe) so a loop that opens-and-closes a database per iteration does not leak. The close path is automatic and cannot be performed by hand; there is no close the database verb, and the handle's opaque type prevents inspection from the surface.
Current limitations
The compiler surface covers nine disk-touching acts (six file verbs plus the three fsext acts), two iterators (the one-level diriter and the recursive walk), and five metadata reads (size, mtime, kind, exists, access). The following are not yet built and should be treated as gaps rather than normal operations:
- The recursive walk yields regular files and symlinks but never directories;
each entry in the directory at <path>is the only form that yields a directory (and only the immediate one-level children). A future walk form may yield directories too. - The mtime read returns whole seconds since the Unix epoch, with no sub-second precision. Programs that need millisecond resolution can read
the time now in millisecondsand compute a delta themselves, but cannot read sub-second mtime from the filesystem. - The database handle is opaque: there is no way to inspect its state, run a prepared statement by name, or hand a handle to a function. Every program that needs database access opens a new handle inside the verb that needs it.
- The SQL text is raw SQL passed through
sqlite3_prepare_v2. There is no query builder and no placeholder for table or column names — only for values. A program that needs dynamic column names must build the SQL string itself with care. - Transactions are exposed only through raw
BEGIN/COMMITSQL insideask <db> to run. There is nowith the transactionblock yet. - The rmtree walk is symlink-SAFE but not permission-tolerant: a single unreadable file under the tree causes the failure to surface as
| unwritable:. The walk does not skip individual refusals and continue. - The directory-iteration and recursive-walk forms both iterate in sorted name order; neither yields entries in the order the underlying filesystem reports them. Programs that need directory-creation order or modification order must read the metadata themselves.