Mica’s file surface lives in the files unit, and everything about it follows rules you already know: a File is a value wrapping one open stream, mutation travels through address, cleanup is a defer, and going wrong is split into the same two kingdoms as everywhere else — the environment fails on the channel, the program’s own bugs trap.

The working example is examples/Files: it writes a small file beside itself, reads it back, interrogates a file that is not there, walks the positions, appends, and erases — leaving the directory as it found it.

make -C examples/Files run

Writing: three verbs and a defer

procedure WriteTheFile() fails OsError;
var
    f : File;
begin
    f := Open(Path, ModeWrite) on fail leave;
    defer Close(address f);

    WriteLine(address f, "alpha") on fail leave;
    WriteLine(address f, "beta") on fail leave;
    WriteLine(address f, "gamma") on fail leave;
end;

Open takes one of three modes — ModeRead opens what exists, ModeWrite creates or truncates, ModeAppend creates or extends — and answers a File, or fails with the reason it cannot: Enoent for a missing file, Eacces for a permission wall. The modes are deliberately plain; there is no read-write hybrid, and the example’s last section shows what that means for updates.

Two details in those six lines carry most of the design:

  • defer Close belongs to the activation, not the success path. A failure leaving through on fail leave still closes the stream. And Close itself never fails, by design — a failing teardown would poison every scope exit, so it is the nil-safe, repeatable defer partner.
  • WriteLine writes the text and its newline as one transfer. Two writes would be two separately locked stream operations, and a second task writing the same file could splice two lines into one.

Text leaves as UTF-8 interchange bytes regardless of the program’s own encoding — a UTF-32 build and a UTF-8 build write byte-identical files.

Reading: one loop, one builder

count := 0;
got := ReadLine(f, address buf) on fail leave;

while got do
begin
    count := count + 1;
    WriteLn("  line %lld: %ls", count, ToString(buf));
    Clear(address buf);
    got := ReadLine(f, address buf) on fail leave;
end;

ReadLine answers True when a line was consumed — an empty line included — and False at end of file with nothing consumed. It appends into the caller-owned stringbuffer; clearing between rounds is your job, and that is a feature: one builder serves the whole file, and once it has grown to the longest line the loop allocates nothing further. The newline is consumed and never appended, a carriage return before it is stripped, and the last line needs no trailing newline.

For the read-a-whole-configuration one-liner, ReadAllText slurps the remainder, newlines included, through the same builder discipline.

The two kingdoms, drawn through one surface

This is the part worth slowing down for, because the files unit is the clearest worked instance of the split the failure channel article introduces:

Going wrongKingdomWhy
Open on a missing pathfailureEnoentthe environment’s business; absence is ordinary
the disk fills mid-writefailureEnospcditto
Seek on a pipefailureEspipea stream without positions is a fact, not a bug
writing through a closed Filetrapfile_write_failedtransferring through state that is not there is a program error
reading past the end your own AtEnd check disprovedtrapfile_read_past_endsame

The probes stay probes: Exists, IsOpen, and AtEnd answer softly, for callers who would rather ask than handle. A missing file arrives with a name you can select on:

f := Open("no-such-file.txt", ModeRead) on fail caught do
begin
    case caught of
        Enoent : WriteLn("  the missing-file reason, by name")
        else     WriteLn("  code %lld", Ord(caught))
    end;
end;

No -1, no errno, no nil handle — the error domains machinery lifts the C convention behind the scenes, and the full trap vocabulary is the reference chapter Traps and tiers.

Positions count bytes

Size, Position, Seek, and Rewind speak byte offsets from the start of the file. In the example, three lines of 6+5+6 bytes make Size answer 17, Seek(address f, 6) lands on the second line, and Rewind is the seek-to-zero idiom under its Pascal-heritage name. Bytes — not characters, not lines — because the same unit is what the typed transfer moves, and that is the next article’s whole subject.

Housekeeping

Erase removes a file by path and Rename moves one — both on the channel, both with the reason they cannot. A caller for whom absence is ordinary writes on fail continue, the deliberate swallow. The standard streams arrive as values too: StdIn(), StdOut(), StdErr().

Next

Typed transfersWriteValue and ReadInto: whole values written and read as their own bytes, the plain constraint that makes C’s fread corruption unrepresentable, and a file you index like an array.