Standard Library & Runtime

Files & Resources

Choose one-shot FS operations or a lifetime-bearing File, then handle Result and cleanup precisely.

File access crosses an explicit host boundary. Choose a one-shot FS operation when one call completes the task. Choose File when several reads or writes must share one open resource and a visible lifetime.

Choose FS or File

TaskUseExact result
Read a text file in one callFS.readText(path)Result<string, string>
Replace a text file in one callFS.writeText(path, text)Result<(), string>
Read or write immutable bytesFS.readBytes, FS.writeBytesResult<Bytes, string> or Result<(), string>
List one directoryFS.list(path)Result<Array<{ kind: string, name: string, sizeBytes: Option<int> }>, string>
Keep one file open for several operationsopen(path)Result<File, string>

The FS calls accept Path | string. The lifetime-bearing surface is deliberately smaller:

open(path: string) -> Result<File, string>
file.read() -> Result<string, string>
file.write(text: string) -> Result<(), string>
file.close() -> ()

There is no canonical File.open, File.create, File.append, readLine, writeLine, or flush.

Read through one managed File

Create note.txt containing ready, then save this program as files-resources.tpz:

TOPAZ
function readNote(path: string) -> Result<string, string> {
    let mut result: Result<string, string> = Err("not read")
    using file = open(path)? {
        result = file.read()
    }
    return result
}

match readNote("note.txt") {
    case Ok(text) => print(text.trim())
    case Err(error) => print(error)
}
Output
ready

open may fail before a File exists, so it returns Result. Postfix ? propagates that error before cleanup is registered. After acquisition, using creates one immutable File binding in its child scope and registers one implicit close. The result is copied to the outer mutable binding before the scope closes.

Choose using or defer

  • Prefer using file = open(path)? { ... } when one block owns exactly one File. The binding cannot escape its child scope as an open lifetime.
  • Prefer let file = open(path)? followed immediately by defer { file.close() } when the owning function needs the File across several nested blocks.
  • An explicit file.close() inside using does not cancel the registered close. Repeated-close behavior is not portable.

Both forms clean up on normal exit and when return, ?, break, or continue crosses the owning scope. They are not a general finally guarantee: an ordinary runtime fault aborts evaluation without a language promise that lexical cleanup runs.

Common correction: do not describe file.write as append. Append modes, binary File overloads, buffering, permissions, and line-oriented File methods are outside the portable File contract. Use the exact FS byte operations when whole-file binary data is the task.

Capability and value boundaries

  • File, FS, std.fs, and std.io require a host profile with filesystem access.
  • Importing a module grants no path, environment, process, network, or database authority.
  • Acquisition, read, and write failure stays in Result; it is not a catchable exception.
  • File is opaque, non-comparable, and has no portable post-close behavior beyond the listed signatures.
  • User-defined disposable protocols, multiple-resource using, asynchronous cleanup, cancellation cleanup, and module-lifetime finalization are not current language contracts.

Continue with Defer & Resources, Null, Option, Result & Faults, or Standard Library Overview.