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 runWriting: 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 Closebelongs to the activation, not the success path. A failure leaving throughon fail leavestill closes the stream. AndCloseitself never fails, by design — a failing teardown would poison every scope exit, so it is the nil-safe, repeatabledeferpartner.WriteLinewrites 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 wrong | Kingdom | Why |
|---|---|---|
Open on a missing path | failure — Enoent | the environment’s business; absence is ordinary |
| the disk fills mid-write | failure — Enospc | ditto |
Seek on a pipe | failure — Espipe | a stream without positions is a fact, not a bug |
writing through a closed File | trap — file_write_failed | transferring through state that is not there is a program error |
reading past the end your own AtEnd check disproved | trap — file_read_past_end | same |
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 transfers — WriteValue 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.