vision
Install

Language reference

Network & process

Vision opens TCP servers, spawns child programs, and reads the process environment through a small, opaque-handle surface. There is no client connect and no TLS โ€” a Vision server speaks cleartext, and a Vision client that needs to fetch uses the network ask, not a raw socket.

Every rule below describes the current Vision compiler. For the shorter safety model, read Safe by design. The Concurrency reference documents the per-connection model that the network block uses beneath the floor; the Filesystem & data reference covers the same opaque-handle ownership rule this page applies to a server connection and a live child process.

Serve / accept loop

The network surface is server-only. There is no client-side connect to statement: a Vision program can listen for incoming connections, but it cannot open one of its own. For a one-shot fetch over HTTP, use the network ask shape โ€” ask host for path returning name โ€” which is the fetch half of the network surface and is documented in the Concurrency reference. This page covers only the listening half.

A server is a structural block: serve on port <expr> for each connection:. The indented body runs once per accepted connection, on its own strand (a real pthread, hidden beneath the floor), so a slow client cannot block the next. The port expression is a whole number; the runtime binds INADDR_ANY on that port and accepts up to 64 pending connections in the kernel backlog. The serve line is a world-touch (>> required) and the only serve block in a program โ€” nesting serve inside another serve is rejected with the nested-server diagnostic.

Inside the block, an implicit opaque handle named the connection is in scope. It is a T_CONN: the same opaque, library-owned value family as T_DB and T_PROC. It has no surface name, so it cannot be bound to a user variable, sent across a strand, or used outside the block โ€” the opaque-handle guarantee holds structurally. There is no close the connection verb; the runtime closes the socket when the per-connection handler returns or the program exits.

The connection surface has three shapes, all fallible the one honest way a connection can refuse โ€” the peer closed, caught with | closed::

  • read a line from the connection returning <name> | closed: โ€ฆ โ€” read one newline-terminated line as text, without the newline. Used for text protocols (HTTP, line-based RPC, the obvious chat over TCP).
  • read exactly <n> bytes from the connection returning <name> | closed: โ€ฆ โ€” read exactly <n> raw bytes, bound as a list of bytes. <n> must be a whole number. A short read (the peer closed before all n arrived) is the closed failure, not a partial result.
  • write <expr> to the connection | closed: โ€ฆ โ€” write text or a list of bytes. The peer closing mid-write is the closed failure; a write to a long-gone peer is ignored by the OS (SIGPIPE is blocked at the server runtime), so a vanished peer does not kill the server.

None of the three connection acts requires its own >> marker: the enclosing serve line's >> already covers them, exactly the way a process handle's acts ride start's marker. Only the serve line itself โ€” and any nested line whose own expression independently touches the world โ€” needs the marker. A | closed: clause is required on every connection read and write; the compiler rejects the program otherwise.

Process control

Process control has two spawn shapes and a small set of handle acts. Both spawn shapes accept an argv vector as with "A" and "B" and "C" โ€” the same and-joined value list the database with clause uses, and the same injection-safe model: the program name and each argument are distinct strings, there is no shell, and no word-splitting or glob expansion happens between them. A program that runs and exits non-zero is not a failure โ€” its exit status is the code field of the bound result, exactly the way June ruled in the source.

The run shape spawns a child, waits for it to exit, and binds a built-in process result described thing (output: text, errors: text, code: whole number) in one step: >> run "git" with "commit" and "-m" and "wip" returning the result. The only failure is the one way a spawn can fail โ€” the program could not be started (not found, not executable, fork failed), caught with | unrunnable:.

The start shape is the long-running twin. It spawns the same argv and binds an opaque T_PROC handle to the named result: >> start "cat" returning the child . The only failure is the spawn itself (| unstartable:); once started, the process is a live child the runtime reaps automatically at scope close. The handle is closed at scope close the same way T_DB and T_CONN are, so there is no close the process verb and no way to inspect the handle's interior from the surface.

Four acts name a started handle: write, read a line from, wait for, and stop. None of them requires its own >> โ€” they ride start's marker, exactly the way a connection's acts ride serve's.

  • write <text> to <ref> โ€” best-effort to the child's standard input. Never fallible; a gone child silently drops the write.
  • read a line from <ref> returning <name> | ended: โ€ฆ โ€” read one line of the child's standard output as text. The ended failure fires when the child closes its output (end-of-file), like a connection's closed failure.
  • wait for <ref> returning <name> | unwaitable: โ€ฆ โ€” block until the child exits, then bind a process result with the same shape the run shape returns. | unwaitable: fires if the wait cannot be collected. wait for also accepts a | timeout N <unit>: or but if it takes longer than N <unit>, clause; a timed-out wait abandons and kills the child.
  • >> !! stop <ref> โ€” send SIGTERM then SIGKILL if the child ignores the first, reap it, and discard any partial output. !! is required: killing a process is irreversible.

Liveness is a separate pure expression: <ref> is still running is a yes/no read that does not require >> and never fails. It uses the same machinery as a process-handle existence check; a gone child is simply no.

reference-process-run.viscompiler checked
// D4 / ยงB โ€” the process control surface. `run` spawns a child and waits, returning
// a `process result` whose `code` and `output` are read normally; a non-zero exit is
// data, not a failure. `start` is the long-running twin: it binds an opaque T_PROC
// handle that `read a line from`, `write to`, `stop`, and `wait for` all use by name.
// `stop` is `>> !!` (irreversible); the liveness check `โ€ฆ is still running` is a pure
// yes/no expression that never needs its own `>>`. Hermetic: only `echo`, `false`,
// and `sleep` are used, found via PATH, so the program runs on any POSIX host
// without git, nix, or a build tool installed.
>> to begin:
    >> run "echo" with "hi there" returning the greeting
        | unrunnable: say "could not run echo" and stop
    when the output of the greeting contains "hi there":
        say "echo ok"

    >> run "false" returning the verdict
        | unrunnable: say "could not run false" and stop
    when the code of the verdict is 1:
        say "non-zero exit is a normal result"

    >> start "sleep" with "30" returning the sleeper
        | unstartable: say "could not start sleep" and stop
    when the sleeper is still running:
        say "sleep running"
    >> !! stop the sleeper
    when the sleeper is still running:
        say "sleep should have stopped"
    else:
        say "sleep stopped and reaped"

    >> run "definitely-not-a-real-program-xyz" returning the ghost
        | unrunnable: say "ghost could not be run" and stop
    say "this line should never run"
// D4 / ยงB โ€” the process control surface. `run` spawns a child and waits, returning
// a `process result` whose `code` and `output` are read normally; a non-zero exit is
// data, not a failure. `start` is the long-running twin: it binds an opaque T_PROC
// handle that `read a line from`, `write to`, `stop`, and `wait for` all use by name.
// `stop` is `>> !!` (irreversible); the liveness check `โ€ฆ is still running` is a pure
// yes/no expression that never needs its own `>>`. Hermetic: only `echo`, `false`,
// and `sleep` are used, found via PATH, so the program runs on any POSIX host
// without git, nix, or a build tool installed.
>> to begin:
    >> run "echo" with "hi there" returning the greeting
        | unrunnable: say "could not run echo" and stop
    when the output of the greeting contains "hi there":
        say "echo ok"

    >> run "false" returning the verdict
        | unrunnable: say "could not run false" and stop
    when the code of the verdict is 1:
        say "non-zero exit is a normal result"

    >> start "sleep" with "30" returning the sleeper
        | unstartable: say "could not start sleep" and stop
    when the sleeper is still running:
        say "sleep running"
    >> !! stop the sleeper
    when the sleeper is still running:
        say "sleep should have stopped"
    else:
        say "sleep stopped and reaped"

    >> run "definitely-not-a-real-program-xyz" returning the ghost
        | unrunnable: say "ghost could not be run" and stop
    say "this line should never run"
run echo with arguments, run false to prove a non-zero exit is normal data (not a failure), start sleep 30, prove liveness, stop the child, and run a guaranteed-absent program to prove the unrunnable path fires and ends the program.

Signals and the graceful-shutdown handler

A program can send six signals to a process by pid: stop, kill, interrupt, hangup, user signal 1, and user signal 2. The pid is a whole-number expression; the failure clause distinguishes the two honest send failures โ€” | noproc: for ESRCH (no such process) and | unsignallable: for EPERM (the caller is not allowed to signal it). A send is irreversible, so the line carries both >> and !!.

Liveness by raw pid is a separate pure expression: process <pid> is alive answers yes/no without>> and uses kill(pid, 0) (permission-denied still counts as alive). It is distinct from <ref> is still running โ€” the first works on a raw pid the caller learned somehow, the second works on a handle a start bound.

A program can declare one graceful-shutdown handler at the top level: when this program is asked to stop: followed by an indented body. The handler runs when the program receives SIGTERM or SIGINT, then the program exits. It is shaped like a verb (empty parameters, body in statement form) so the checker's body passes reach it without a new machinery. There is one handler, not six โ€” per-signal handlers are not built. A long-running serve or respond loop drains the signal between iterations or frames, so the handler runs at a safe point rather than mid-write.

Environment

The whole environment is iterable: each <var> in this program's environment: walks environ, binding the built-in variable described thing (name: text, value: text). The loop is a world-touch (>> on the loop line) and never fails โ€” there is no failclause slot, because reading environ cannot refuse. The loop body reads the name of the variable and the value of the variable as ordinary field accesses. The runtime copies the key (terminated at the first =) for each iteration and frees it at the loop foot; the value borrows the live environ string, stable for the program's life.

A single environment variable is read as an expression: the value of the "PATH" environment variable is a world-touching text-or-nothing read. The or nothing shape is implicit โ€” a missing key reads back as nothing, tested with <name> is nothing or the standard when there is <name>: presence guard. A bare variable form (the value of the PATH variable) is also accepted for backward compatibility; the canonical spelling is the one with environment variable at the end.

A spawned child inherits the parent's environment by default. To set or override entries for one child only, attach a using the environment: block to the run or start line, with one set "KEY" to value per line in the indented body:

>> run "printenv" returning the result
    using the environment:
        set "FOO" to "bar"
        set "BAZ" to "qux"
    | unrunnable: stop

Each set "KEY" to <value> is a text-literal key plus any text value; the child's environment is the parent environment overlaid with these sets.

reference-network-env.viscompiler checked
// ยงI โ€” the environment surface. `for each <var> in this program's environment` walks
// `environ`, binding the built-in `variable` thing (name + value, both text). The
// loop is a world-touch (`>>` on the loop line) and never fails โ€” the body's
// `the name of the variable` and `the value of the variable` reads are ordinary
// field accesses on a sealed described thing. `the value of the "NAME" environment
// variable` is a separate read shape (a world-touching text-or-nothing expression)
// that joins the absence model: a missing key reads back as `nothing`, tested with
// `<name> is nothing`.
>> to begin:
    the count is 0
    >> each var in this program's environment:
        the count is the count plus 1
    when the count is greater than 0:
        say "env iterated"

    >> the path is the value of the "PATH" environment variable
    when there is the path:
        say "PATH read"

    >> the missing is the value of the "VISION_TEST_DEFINITELY_NOT_SET_X9Z" environment variable
    when the missing is nothing:
        say "missing var is nothing"
// ยงI โ€” the environment surface. `for each <var> in this program's environment` walks
// `environ`, binding the built-in `variable` thing (name + value, both text). The
// loop is a world-touch (`>>` on the loop line) and never fails โ€” the body's
// `the name of the variable` and `the value of the variable` reads are ordinary
// field accesses on a sealed described thing. `the value of the "NAME" environment
// variable` is a separate read shape (a world-touching text-or-nothing expression)
// that joins the absence model: a missing key reads back as `nothing`, tested with
// `<name> is nothing`.
>> to begin:
    the count is 0
    >> each var in this program's environment:
        the count is the count plus 1
    when the count is greater than 0:
        say "env iterated"

    >> the path is the value of the "PATH" environment variable
    when there is the path:
        say "PATH read"

    >> the missing is the value of the "VISION_TEST_DEFINITELY_NOT_SET_X9Z" environment variable
    when the missing is nothing:
        say "missing var is nothing"
Iterate the process environment and prove the count is non-zero, read a single env var (PATH) with the presence guard, then read a guaranteed-absent name and prove it reads back as nothing.

Ownership and cleanup

Three opaque, library-owned handle types share the same ownership rule: T_CONN (a server connection), T_PROC (a started child), and T_DB (a database connection, from the previous reference). None can be inspected from the surface; none can be sent across a strand; all are reclaimed at scope close by the same scope-walk the filesystem and text results ride. A connection is closed when its per-connection handler returns; a child is reaped (with SIGTERM then SIGKILL if it ignores the first) when the verb that started it closes. There is no close verb for any of them, and the handles are hoisted to the verb's top-level scope so every exit path โ€” an answer, a stop, a pass โ€” releases the resource correctly.

The connection handle has no surface name. It is implicitly in scope inside a serve block and cannot be bound to a user variable, passed to a helper verb, or used outside the block โ€” the compiler rejects every such use. A started process handle, by contrast, does have a surface name (the result of start); the name lets the four handle acts name it, but the value it binds is still opaque, and the handle is closed at scope close even if the program never explicitly stops it.

Read results follow the same B1 return rule the rest of the language uses: a connection's line read hands owned text; a fixed-byte read hands an owned list of bytes; a process run hands an owned process result (the output and errors fields are owned text, the code field is a whole number); a wait for hands the same process result shape. The environment loop's per-iteration name is freed at the loop foot, and its value is a borrow of the live environ string โ€” the body's say or interpolation copies it, exactly as a directory-entry name does.

Current limitations

The compiler's network and process surface is small, deliberate, and has several known gaps. The following should be treated as gaps, not as supported behavior.

  • No TLS or SSL. A serve block speaks cleartext TCP. On a trusted LAN that is fine; on the public internet a Vision server must sit behind a TLS-terminating reverse proxy. The runtime comment in src/emit.c says it plainly: // PLAINTEXT, by design and stated plainly (ยง6.4, ยง7 of the brief): a bare Vision server speaks cleartext. The future serve securely shape is not built.
  • No client connect. The compiler has no`connect to <host:port>` statement. A program can listen for incoming connections but cannot open one of its own. For a one-shot fetch, use the network ask shape from the Concurrency reference, not a raw socket. A program that needs a long-running client side must spawn a child whose process does the socket work and read its output, or useby hand: with the system C socket API.
  • No host:port bind. The serve block binds INADDR_ANY on the named port. There is no surface for binding a specific interface, an IPv6 address, or a Unix-domain socket. A program that needs any of these must drop to by hand: with the system C socket API.
  • Serve nesting is rejected. A serve block cannot be nested inside another serve โ€” the compiler rejects the inner one with the nested-server diagnostic. In practice serve's accept loop runs forever; the program ends on SIGTERM / SIGINT (which the drain path handles) or when an integration test pins VIS_SERVE_MAX_CONNS to bound the accept loop. There is no special "serve exits the program" behavior; if the loop does end, execution continues after the serve block.
  • One graceful-shutdown handler. A program declares at most one when this program is asked to stop: handler, and it covers SIGTERM and SIGINT together. There is no per-signal handler, no SIGHUP hook, and no way to ignore one signal but act on another. A program that needs different behavior per signal must drop to by hand: with signal + a manual sigaction.
  • Connection read/write ride serve's marker. A connection read or write inside a serve block does not need its own >>. A separate >> is still required if the act's own operand expression independently touches the world (an interpolation that reads a clock, for example).
  • Process handle acts ride start's marker. Exactly the same rule: a write, read a line from, wait for, or stop naming a started handle does not need its own >>; the start line's marker covers them. stop separately needs !! because it is irreversible.
  • The environment loop cannot be set. for each <var> in this program's environment is read-only. The set "KEY" to <value> shape is only available inside a using the environment: block attached to a run or start line, and it sets the child's environment, not the running program's. There is no setenv for the current process.
  • Signal handler body is single-purpose. The graceful-shutdown handler runs the body, then the program exits. There is no way to return from the handler and continue; the body is the last code the program runs. There is also no SIGCHLD hook for automatic child reaping โ€” a program that starts many short-lived children should wait for each one explicitly.
  • No UDP, no SCTP, no raw sockets. The serve runtime uses SOCK_STREAM over AF_INET. There is no surface for datagram protocols or raw sockets; a program that needs either must drop to by hand: with the system C socket API.