Program input and output

A few built-ins connect a program to its process, files, and other programs. They are enough to write tools such as the Cuarzo compiler, its test suite, and its bootstrap:

Built-in Result Behavior
args() str[] command-line arguments; index 0 is the program
read_text(path) str the whole file; it must be UTF-8 without NUL bytes
try_read_text(path) Result[str] the same, or an error instead of stopping
write_text(path, text) none replaces the file with the exact bytes of text
append_text(path, text) none adds text to the end of the file, creating it if needed
read_bytes(path) bytes the whole file, whatever it holds
try_read_bytes(path) Result[bytes] the same, or an error instead of stopping
write_bytes(path, data), append_bytes(path, data) none replaces the file with data, or adds data to its end
try_write_text(path, text), try_write_bytes(path, data), try_append_text(path, text) Result[int] the same as the forms without try_, giving the count of bytes written, or an error instead of stopping
write_atomic(path, data) Result[int] replaces the file with text or bytes so that it holds the old contents or the new, never part of them
eprint(...) none like print, but writes to standard error
exit(status) none ends the program with an integer status
run(command) int runs a program and waits for it
run(command, out, err) int the same, with output and errors sent to files
make_dir(path) none creates a directory and any missing parents
list_dir(path) str[] the sorted names in a directory
exists(path) bool whether a file or directory exists
is_dir(path) bool whether a directory exists there
delete_file(path) bool deletes a file and says whether it did
rename_file(from, to) bool renames or moves a file, replacing to
current_dir() str the directory relative paths start from
change_dir(path) bool makes path that directory and says whether it could
temp_dir() str the directory for temporary files
temp_file() str creates a new empty file with a name no other has, in temp_dir(), and gives its path
process_id() i64 the id the system gives this process
find_program(name) Option[str] the full path of a program, as running it finds it on PATH

A file that cannot be read or written stops the program with a runtime error that names it, as in cannot write file: scores.txt: Permission denied; the try_ forms give that error as a Result instead, for files a program can live without, such as a game's best scores. Its kind says what went wrong: "not_found", "permission", "exists", "no_space", or "io", and "invalid" for text that is not UTF-8. write_atomic writes a new file beside the old one, makes sure it reached the disk, and renames it over the old one, as editors must when they save; the file keeps its permissions.

Paths are UTF-8 text, with / or, on Windows, \ between names. On Windows they reach the system as UTF-16, so names outside ASCII work, and a path longer than 260 characters works too.

run takes the program and its arguments as a str[], such as run(["gcc", "--version"]), and passes each argument unchanged, without a shell. It returns the program's exit status, or -1 when the program cannot be started. A name with a directory, such as build/tool, is used as given; a bare name is looked up in the PATH directories only, never in the current directory. On Windows, run starts native programs only: a name without an extension means .exe, and other files such as scripts are not started. In the three-argument form, out and err are file paths that are replaced, and null keeps that stream shared with the running program. To get what a command writes as text, use capture from std/process.

str[] arguments = args()
if len(arguments) != 3 then
    eprint("usage: copy <input> <output>")
    exit(1)
end
write_text(arguments[2], read_text(arguments[1]))

A file or directory that cannot be opened, read, written, or created stops the program with a runtime error that names the path. On Windows, a path given to the file built-ins should be ASCII; the working directory, temporary files, arguments, and the environment handle any text. try_read_text instead returns an error Result, such as cannot open file for reading: notes.txt, so a program can handle a file that may be missing.

Terminals, input, and time

These built-ins are the base for interactive command-line programs. The standard library builds colors, prompts, menus, and argument parsing on them.

Built-in Result Behavior
write(text) none writes text without a line end and shows it at once
ewrite(text) none the same, to standard error
read_line() Option[str] the next line of input without its line end, or NONE at the end
read_input() str all of standard input, as when another program pipes into this one
read_input_bytes() bytes all of standard input as bytes, whatever it holds
read_stdin(n) bytes the next n bytes of standard input, fewer only at its end, for protocols that send lengths
write_out_bytes(data), write_err_bytes(data) none writes bytes to standard output or standard error exactly as they are
read_key() str one key, such as "a", "enter", "up", or "ctrl+c"; "" at the end
key_ready(ms) bool whether a key is waiting, waiting up to ms milliseconds for one
term_events(on, mouse), term_mouse(on), term_read_event(ms), term_key_releases() the terminal's events as text, under std/term's read_event, which programs use instead
desktop_start(name, id, handler), window_create(title, width, height), window_set(id, key, value), window_get(id, key), and the other desktop_ and window_ built-ins the application and its windows, with events as text, under std/desktop, which programs use instead
raw_mode(on) none keys arrive one at a time, unechoed, with Ctrl+C as a key
is_terminal() bool whether standard output is an interactive terminal
is_input_terminal() bool whether standard input is one
terminal_width(), terminal_height() int the terminal's size, or 80 by 24 when unknown
env(name) Option[str] an environment variable, or NONE when it is not set
set_env(name, value) none sets an environment variable, for this program and the ones it starts
unset_env(name) none removes an environment variable
sleep(ms) none pauses for ms milliseconds
clock_ms() i64 milliseconds from a fixed point, for measuring durations
input_waiting() bool whether data from another program waits on standard input, a pipe or a terminal, so reading it would not wait; false for a file. Its first call makes standard input unbuffered
lsp_socket_listen(port), lsp_socket_port(s), lsp_socket_accept(s), lsp_socket_connect(port), lsp_socket_read(s, count), lsp_socket_write(s, data), lsp_socket_close(s) a TCP socket on the loopback address, as the language server and its tests use; the networking of phase 14 will supersede them
unix_time() i64 seconds since 1970-01-01 UTC
unix_time_ns() i64 nanoseconds since 1970-01-01 UTC, from the wall clock
env_names() str[] the names of the variables of the environment, in no order
local_offset(seconds) int how many seconds east of UTC local time is at that time since 1970, as the system's time zone says; 0 when it cannot tell, as for times before 1970 on Windows
random(bound) int a random integer from 0 to bound - 1
random_seed(seed) none makes the random numbers repeat for the same seed
write_on_exit(text) none text written when the program ends, even after an error; "" for none
platform() str "windows", "macos", "linux", "freebsd", "openbsd", "wasi", or "unix" for another POSIX system
arch() str the processor: "x86_64", "arm64", "wasm32", "x86", "arm", "riscv64", or "unknown"
executable_path() str the running program's own file, as an absolute path with links followed; "" when the system will not say
start_process(command) i64 starts a program without waiting and returns its number
poll_process(p) Option[int] SOME(status) once it has finished, NONE while it runs
wait_process(p) int waits for it and returns its exit status

read_key names text keys by their character ("a", "ñ") and other keys by name: enter, tab, backspace, escape, up, down, left, right, home, end, insert, delete, page_up, page_down, f1 to f12, shift+tab, ctrl+a to ctrl+z, and modified keys such as ctrl+right or alt+x. In raw mode each key arrives at once; without it a terminal delivers keys only after Enter. Raw mode has no effect when input is not a terminal, and the terminal is always restored when the program exits, even after a runtime error.

key_ready lets a program such as a game go on when no key is pressed: if key_ready(100) then waits at most 100 milliseconds. Like read_key, it is meant for raw mode; input that is not a terminal is always ready. random seeds itself from the clock; its numbers are for games and samples, not for secrets.

start_process takes the same arguments as run, so a program can show a spinner while another one works:

i64 job = start_process(["gcc", "big.c", "-o", "big"])
int frame = 0
while true do
    match poll_process(job) do
        when SOME(status) then
            print("\rdone:", status)
            break
        when NONE then
            write("\r" + to_str("|/-\\"[frame % 4]))
            frame += 1
            sleep(100)
    end
end

run and start_process also take four arguments, (command, input, output, errors), where input is a file the program reads as its standard input.

On Windows, a program turns on the console's support for colors and cursor codes when it starts, exchanges text as UTF-8, and reads its arguments as UTF-8; the console's settings are restored at exit.

Embedding files

embed(path) is the text of a file, read when the program is compiled rather than when it runs, so the built program carries the text and needs no file beside it:

str help = embed("help.txt")
print(help)

The path must be a string literal, relative to the file that contains the call, like an import path. The file must be UTF-8 without NUL bytes. Line ends are normalized to \n, so a program builds the same on every system. The compiler uses embed to carry its C runtime, which lives as ordinary C files in compiler/runtime/.

embed_bytes(path) is the bytes of a file, whatever it holds, such as an image or a font. embed_dir(path) is every file below a directory, as a map from paths relative to it, with / between names, to their bytes; the map cannot change. A program can carry a whole web application this way and serve it from memory:

const map[str, bytes] SITE = embed_dir("web/dist", compress: true, exclude: ["*.map"])

bytes page = SITE["index.html"]

Its options are given by name:

Option Effect
compress: true stores the files compressed, in the LZ4 block format, and unpacks them when the map is made; a file that would not get smaller is stored as it is
include: [...] takes only the files that match one of the patterns
exclude: [...] leaves out the files that match one of the patterns

In a pattern, * stands for any characters but /, ? for one, and ** for any characters, / too, so assets/**/*.js is every script below assets. A pattern without /, such as *.map, is matched against the name of each file, in any directory. The data is kept in the program as C string literals of at most 4095 bytes, the longest C99 compilers must accept, and joined the first time it is used, so megabytes compile quickly.

Build constants and build information

A constant declared with a default can be given another value when the program is built, so one source builds for development and for release:

const bool DEBUG = false
const str API_URL = "http://localhost:8080"
const int WORKERS = 2

when defined(TRACING) do
    print("tracing on")
end
cuarzo build -D DEBUG -D API_URL=https://example.com -D WORKERS=8 -D TRACING app.cz

-D NAME=value sets the constant NAME, in any file of the program, and -D NAME alone sets a bool to true. The value is read as the constant's type requires: true or false, a decimal integer (with a - if it is negative), a number such as 2.5 or 1e-3, one printable ASCII character, or any text for a str. Other types cannot be set this way, and a value that does not fit is a compile error at the constant. Either way, defined(NAME) is then true in when conditions, also for a name no constant has.

These built-ins tell a program about its build. The first three are written into the program when it is compiled:

Built-in Result Behavior
build_version() str the text given with --build-version, or "dev"
build_commit() str the Git commit of the repository that holds the source, or "" outside one
build_time() i64 when the program was built, in seconds since 1970; SOURCE_DATE_EPOCH sets it
build_optimized() bool whether the C compiler optimized the program, as --optimize and --release ask

The same source and options give the same program, byte for byte, as long as it does not ask for build_time() without SOURCE_DATE_EPOCH, or for build_commit() from another commit. On Windows the linker is told not to stamp the program with the time.

--release optimizes the program, as --optimize does, and leaves its symbols out, so it is smaller. --debug builds it for a debugger, with symbols, unoptimized, and with its C pointing at the .cz files and lines; see Debugging. --gui builds a program that opens windows instead of using a terminal: on Windows it gets no console window, on macOS it becomes an application bundle, name.app, with its Info.plist, and elsewhere it is an ordinary program. --resources app.rc links a program on Windows with a resource script's icon, manifest, and version information, which windres compiles; --universal builds one program for both of macOS's processors. cuarzo bundle uses both; see Projects.

--target <platform>[-<arch>] builds for another system: when platform(...) and the runtime's units choose that system's code, and the program is built with its cross compiler, the usual one for the target unless --cc names another (or CUARZO_CC does): x86_64-linux-gnu-gcc and aarch64-linux-gnu-gcc for Linux, x86_64-w64-mingw32-gcc and aarch64-w64-mingw32-gcc for Windows, and osxcross's o64-clang and oa64-clang for macOS. --sysroot <dir> gives the target's headers and libraries, and points pkg-config at its packages. --static links everything into the program, so it needs no shared library: on Linux with musl's musl-gcc when it is installed and no other compiler is named, and on Windows with MinGW's own libraries linked in; macOS allows no such program.

cuarzo build --target windows-x86_64 tool.cz      # tool.exe, from Linux
cuarzo build --target linux-arm64 --sysroot /srv/arm64 tool.cz
cuarzo build --static --release tool.cz           # one file, nothing else needed