MALDA™ Reference Manual

The AI-First Programming Language - Version 1.0.39

12. Input/Output

Learning the language? Hello World and console input are on Learn MALDA. This chapter is the full I/O how-to.

This chapter is the I/O how-to: console, files, paths, environment variables, host process facts, and capability tokens (permission objects so a tool cannot invent a path). Prefer the io.* namespace. Flat aliases such as print and readFile still run, but the editor warns malda-style. Signatures, git helpers, glob / grep, document extractors, and Spectre.Console live in 13. Built-in Functions.

File I/O is a host capability. The interpreter and C# transpile support it; the JavaScript backend does not (file-io is no on the backend capability matrix).

12.1 Console Output

Hello World is one call: io.print("Hello, World!"); — same file: Examples/Basics/hello_world.malda.

io.print(value) takes exactly one argument, converts it with string(value), and writes a line (a trailing newline). There is no println. Concatenate or interpolate before the call if you need several pieces on one line.

io.print("Hello, World!");

var name = "Alice";
io.print($"Hello, {name}!");   // interpolation — the $ is required
io.print("n is {n}");          // literal braces: a plain string does not interpolate

12.2 Console Input

io.input(prompt?) writes an optional prompt (no extra newline) and reads one line from stdin. The result is always a string. Convert it if you need a number.

var name = io.input("Enter your name: ");
var age = int(io.input("Enter your age: "));
io.print($"Hello, {name}. Next year you will be {age + 1}.");
End of input is an empty string, not null. At EOF every later io.input also returns "". A loop that only advances on non-empty input never terminates. Treat blank as quit:
while (true) {
    var raw = io.input("? ");
    if (str.trim(raw) == "") {
        break;
    }
    io.print(raw);
}

The call is asynchronous: execution pauses until a line arrives, then resumes at the same statement. It works inside loops, functions, and nested control structures.

For malformed numbers prefer toIntOr(raw, fallback) or toIntOrNull(raw) over bare int(...), which throws. See 13.1 Type Conversion.

12.3 Files

Paths are strings (or a matching capability token). Ordinary failures return null or false; they do not throw. Check the result. If a tool must not invent a path, do not pass a string — start with 12.6.

12.3.1 Read and write

io.writeFile(path, content) overwrites the file and returns true on success, false on I/O failure. Strings are written as UTF-8. Objects and arrays are serialized with toJSON first. io.readFile(path) returns the whole file as a string, or null when the path is missing or unreadable.

io.writeFile("_rm_io_notes.txt", "hello");
io.print(io.hasFile("_rm_io_notes.txt"));
io.print(io.readFile("_rm_io_notes.txt"));
io.deleteFile("_rm_io_notes.txt");
io.print(io.hasFile("_rm_io_notes.txt"));
io.writeFile("_rm_io_json.txt", dict { "ok": true });
var obj = parseJSON(io.readFile("_rm_io_json.txt"));
io.print(obj.ok);
io.deleteFile("_rm_io_json.txt");
io.print(io.readFile("_rm_io_missing.txt") == null);

12.3.2 Line ranges and line arrays

Line numbers are 1-based.

io.writeFile("_rm_io_lines.txt", "a\nb\nc\nd");
io.print(io.readFile("_rm_io_lines.txt", 2, 3));
io.deleteFile("_rm_io_lines.txt");

12.3.3 Directories, delete, and binary

CallResult
io.hasFile(path)true when a file exists (disk or embed:)
io.hasDirectory(path)true when a directory exists
io.ensureDir(path)Creates the directory and missing parents; no-op if it already exists. Throws on failure. Rejects embed:
io.listDirectory(path)Array of { name, type, path }. type is "file" or "directory". Missing directories yield []. Empty path means "."
io.deleteFile(path)true if the file was deleted or was already absent; false on an empty path or I/O failure
io.copyFile(src, dest)Binary-safe copy (overwrites dest). Returns true on success, false on I/O failure (missing source, missing parent directory). Rejects embed: source and dest. Does not create parent directories
io.readFileBase64(path) / io.writeFileBase64(path, b64)Raw bytes as base64. Read returns null when missing; write returns false on invalid base64
io.writePng(path, width, height, pixels) / io.readPng(path)8-bit RGBA. pixels is a flat array of length width * height * 4. Write returns false on I/O failure, rejects embed:, and throws above 16 megapixels. Read returns { width, height, pixels }, or null when the file is missing or not a decodable 8-bit non-interlaced PNG. Interpreter and C# transpile only
var px = [255, 0, 0, 255];
var ok = io.writePng("_rm_io_px.png", 1, 1, px);
var img = io.readPng("_rm_io_px.png");
io.print(ok);
io.print(img.width);
io.print(img.pixels[0]);
io.print(img.pixels[3]);
io.deleteFile("_rm_io_px.png");
io.writeFile("_rm_io_list.txt", "x");
var items = io.listDirectory(".");
var found = false;
for (var item in items) {
    if (item.name == "_rm_io_list.txt") {
        found = true;
    }
}
io.print(found);
io.deleteFile("_rm_io_list.txt");

insertAtLine, replaceInFile, and editFile edit text in place — catalog and failure style in 13.8. insertAtLine is interpreter-only.

Digital text from documents: pdf.extractText(path, password?) (PdfPig text layer, no OCR) and doc.extractText(path) (.docx only). Those do throw on a missing file. See 13.8.1.

12.3.4 Embedded folders (embed:)

Compile with malda compile … --embed-folder <dir[=alias]> to pack a directory into the executable. At runtime, read with the virtual scheme embed:<alias>/<relative>. Nothing is extracted to disk. Writes (io.writeFile, io.ensureDir, io.deleteFile, …) reject embed: paths. io.hasEmbeddedFolder(alias) / io.embeddedFolderRoot(alias) inspect what was packed. Full example in 13.8.0.

12.4 Paths

Join and inspect paths instead of concatenating slashes. io.pathJoin uses the host separator. io.pathNormalize returns an absolute path.

io.print(io.getFileName("docs/notes.md"));
io.print(io.pathGetExtension("docs/notes.md"));
io.print(io.isPathUnder("brain", "brain/notes/a.md"));

12.5 Environment and Host

These read the process environment and the machine the program is running on. They are not file I/O, but they are how programs find keys, args, and a place to load sidecar files.

io.print(io.getEnvOr("MALDA_RM_IO_MISSING_VAR_7D05", "fallback"));
var host = getHostPlatform();
io.print(str.length(host.pathSeparator));

Assistant config under ~/.malda (getMaldaHome, getMaldaConfig) is documented with the personal assistant.

12.6 Capability Tokens

A capability token is a permission object that your program (the host) creates. Treat it as a key to one file, HTTP origin, MCP tool, or argv prefix: whoever holds the key can use that permission. Nobody can make a working key by writing down a description of the lock.

That is the whole idea. The rest of this section is the why, the API, and the pattern you should copy for tools.

Start here if you write agents. When an LLM tool receives a path, URL, MCP tool name, or command as a string, the model can invent /etc/passwd, any host, or any argv. A token binds the permission to a value you chose, before the model runs. Signature catalog: 13.19.3. First-contact scaffold: malda new agent.

12.6.1 Why @effects("io") is not enough

@effects("io") is a name allow-list for the type checker. It says “this function may do I/O”. It does not say which files. A tool that takes path: string can still read anything the model puts in that string:

@effects("io")
function readNote(path) {
    return io.readFile(path);  // the model can pass "/etc/passwd"
}

A token closes that hole. The host creates the token for a path it trusts. The tool consumes the token. A string or a look-alike object is rejected.

12.6.2 Three different words “capability”

The manual uses “capability” in three places. They are not the same thing:

PhraseWhat it meansWhere
cap.fileRead / capability tokenAn unforgeable permission object for one path, URL prefix, MCP tool, or argv prefixThis section
Backend capability matrixWhich backends implement a product feature (file I/O, agents, DOM, …)backend-capability-matrix.md
@requires("file-io")A property test that skips backends that lack that feature36. Property Testing

12.6.3 Kinds

A token has readable fields kind and path (plus name for an MCP tool). You cannot assign to them. There is no flat cap() or fileRead() alias — always write cap.….

MintkindConsume with
cap.fileRead(path)fileReadcap.read(token) or io.readFile(token)
cap.fileWrite(path)fileWritecap.write(token, content) or io.writeFile(token, content)
cap.dirList(path)dirListcap.list(token) or io.listDirectory(token)
cap.httpGet(origin)httpGetcap.fetch(token, urlOrPath?) or webFetch(token)
cap.mcpCall(server, tool?)mcpCallcap.invoke(token, server, args?) or callTool(token)
cap.shell(prefix)shellcap.run(token, extraArgs?) or runCommand(token)

A fileWrite token is not a read token. Passing the wrong kind throws. cap.confine joins a relative file path, HTTP path, MCP tool name, or extra argv — escapes throw before consume.

var http = cap.httpGet("https://example.com/docs");
io.print(http.kind);
io.print(http.path);
io.print(cap.confine(http, "guide").path);
var escaped = false;
try {
    cap.fetch(http, "https://evil.com");
} catch (e) {
    escaped = true;
}
io.print(escaped);
io.print(cap.mcpCall("local", "add").name);
io.print(cap.confine(cap.shell("git"), "status").path);

12.6.4 Mint, inspect, consume

Mint means “create a real token in the host”. The result is a sealed host object, not a dict. cap.is is the only safe type test — typeOf will not tell you “this is a capability”.

var notes = cap.fileRead("notes.md");
io.print(notes.kind);
io.print(cap.is(notes, "fileRead"));
io.print(cap.is(notes, "fileWrite"));
io.print(cap.is({ "kind": "fileRead", "path": "notes.md" }));

Consume the matching kind only. Strings and { kind, path } dicts throw — including after toJSON / parseJSON, because JSON cannot rehydrate a token.

var path = "_rm_cap_rw.txt";
cap.write(cap.fileWrite(path), "hello from a token");
io.print(cap.read(cap.fileRead(path)));
var forged = false;
try {
    cap.read({ "kind": "fileRead", "path": path });
} catch (e) {
    forged = true;
}
io.print(forged);
var asString = false;
try {
    cap.read(path);
} catch (e) {
    asString = true;
}
io.print(asString);
io.deleteFile(path);

12.6.5 Narrow a token with cap.confine

cap.confine(token, relativePath) returns a new token of the same kind whose path is the parent path plus the relative path. Use it to hand a tool a workspace root, then lock each call down to one child.

.., an absolute path, or anything else that would escape the parent throws. The host never “fixes” the path for you.

io.writeFile("_rm_cap_welcome.txt", "welcome");
var root = cap.fileRead(".");
var child = cap.confine(root, "_rm_cap_welcome.txt");
io.print(str.trim(cap.read(child)));
var escaped = false;
try {
    cap.confine(root, "../secret.txt");
} catch (e) {
    escaped = true;
}
io.print(escaped);
io.deleteFile("_rm_cap_welcome.txt");

12.6.6 Pattern: the host mints, the model passes a relative path

Do not send a token through LLM JSON. A token cannot survive toJSON. The model only ever sees a relative path string. The host keeps the root token in its own memory and confines each call:

io.writeFile("_rm_cap_note.txt", "welcome");
var notesRoot = cap.fileRead(".");

function readNoteWith(rootCap, relativePath) {
    var child = cap.confine(rootCap, relativePath);
    return cap.read(child);
}

io.print(str.trim(readNoteWith(notesRoot, "_rm_cap_note.txt")));
var escaped = false;
try {
    readNoteWith(notesRoot, "../secret.txt");
} catch (e) {
    escaped = true;
}
io.print(escaped);
io.deleteFile("_rm_cap_note.txt");

On a live agent, wrap that helper with @Tool, validate the args with a schema, and mark the handler @effects("cap"). The model’s tool argument is relativePath only. Full walk-through: 19.8. Offline starter: malda new agent (host-minted notes/).

12.6.7 Common mistakes

MistakeWhat happensDo this instead
Give the tool a path: string and rely on @effects("io")The model can invent any path. The decorator only names the effect.Host-mint a root token; take a relative path; cap.confine
Pass the token through LLM JSON / toJSONThe other side gets a dict. cap.read throws.Keep the token in host memory. Send only a relative path.
Build { "kind": "fileRead", "path": "…" } by handLooks like a token. cap.is is false. Consume throws.Call cap.fileRead / httpGet / mcpCall / shell
Call cap.read("notes.md")Throws. Consume APIs reject strings.cap.read(cap.fileRead("notes.md")), or io.readFile("notes.md") in non-tool code
Pass a fileWrite token to cap.readThrows (wrong kind).Mint the kind you will consume
Treat createReadFileTool(workingDir) as a tokenThe factory only checks that paths stay under a string directory. It is not unforgeable.Fine for built-in file tools. Custom @Tool file handlers should use cap.*

12.6.8 Backends and @effects("cap")

Mark handlers that mint or consume tokens @effects("cap") (or list both "io" and "cap"). @pure() functions cannot mint or consume tokens under --strict-types.

The interpreter and C# transpile implement the full surface. JavaScript can mint, cap.is, and cap.confine; consume (cap.read / write / list / fetch / invoke / run) is host-only — same as other file I/O and agents on the backend capability matrix.

Worked examples: Examples/Tools/capability_tokens.malda, Examples/Tools/capability_http_mcp_shell.malda. Agent-shaped few-shots: docs/llm/few-shot/26_tool_cap_read.malda, docs/llm/few-shot/42_cap_http_mcp_shell.malda.

12.7 Searching Files

io.glob(pattern, dirPath?) finds paths. io.grep(pattern, filePath) searches contents. Both take a required first argument and then optional positional arguments. On a missing path or a bad regex they return an error string, not a collection — guard with typeOf(result) == "string".

io.writeFile("_rm_io_glob.txt", "needle");
var found = io.glob("_rm_io_glob.txt", ".");
io.print(found.count);
io.deleteFile("_rm_io_glob.txt");

Default glob cap is 200 results (hard cap 500); the result is { items, count, truncated } with each item { name, type, path }. grep defaults to recursive search with line numbers. Recursive search does not enter directory junctions or symbolic links, so a link that points back at a parent cannot loop. Argument lists and the count-only shape are in 13.8.2.

12.8 Sleep

sleep(milliseconds) pauses without spinning the CPU. The argument must be a non-negative integer. It is a built-in, not an io.* member. Useful for keep-alive loops around a server.

sleep(0);
io.print("awake");
while (server.isRunning) {
    sleep(1000);
}
Workflows: sleep, io.writeFile, and other deny-listed effects raise WF1001 / WF1002 when they run in a deterministic workflow section — including when nested in a helper. Put clock, sleep, and I/O inside a step. See 23. Durable Workflows.

12.9 Fetching URLs

webFetch(url, maxBytes?, timeoutMs?) fetches an http:// or https:// URL using the same host path as httpGet. Other schemes (including file://), empty URLs, and URLs without a host are rejected. Optional maxBytes defaults to 100000 (hard cap 500000); longer bodies are truncated and truncated is set. Optional timeoutMs defaults to 15000 (hard cap 60000). Returns { ok, status, url, content, truncated, error? }. content is always a string: a text body is used as-is; a JSON object or array body is JSON-stringified. The agent factory is createWebFetchTool (no working directory; parallel-safe).

var page = webFetch("https://example.com/");
print(page.ok);
print(page.status);
print(page.truncated);

For JSON APIs with headers, auth, and retries prefer 30. REST Web Client. A capability token uses cap.httpGet + cap.fetch / webFetch(token) — see 12.6. Name index: 13.5.4.

12.10 What This Chapter Is Not

NeedGo to
HTTP client / REST calls30. REST Web Client (webFetch is 12.9)
HTTP / HTML / ui.* servers25. Web UI Overview
SQL clients17. Database Support
Shell commandsrunCommand in 13.10 (blocked dangerous names, 1-hour timeout cap)
Git13.9
Agent file toolscreateReadFileTool and siblings in 19. Agent Orchestration
Bind a custom tool to one folder12.6 Capability tokens and 19.8

See Also