Two units divide one job here, and the division is the lesson.

The paths unit is pure text algebra. Join, DirName, BaseName and Extension rewrite POSIX path spellings without ever asking the operating system whether anything exists — so every one of them is total, and the unit has no failure surface at all. The files unit is where a path finally meets the filesystem, and there every answer rides the failure channel.

A function that cannot fail should not pretend it might; a function that touches the world should say so in its signature. So the two live in different units and read differently at every call site.

The working example is examples/PathsAndDirectories:

make -C examples/PathsAndDirectories run

The algebra, and whose semantics it follows

Every verb takes its definition from the industry precedent with the cleanest textual definition, so each answer is checkable against an outside oracle:

CallAnswersFollowing
Join("/usr/lib", "mica")/usr/lib/micapython os.path.join, C++ operator/
Join("/usr/lib/", "mica")/usr/lib/micaexactly the separator the seam is missing is added
Join("/usr/lib", "/etc")/etcan absolute right side replaces the left
DirName("/usr/lib/mica.a")/usr/libdirname(1)
DirName("/usr/lib/")/usrtrailing separators stripped before the split
BaseName("mica.a")mica.abasename(1) — no separator means all base
DirName("mica.a").…and the current directory
Extension("archive.tar.gz").gzpython splitext — the last dot
Extension(".bashrc")(empty)a run of dots that opens a component never counts

Three consequences worth stating outright:

  • Join never cleans. A doubled separator inside either side, or a trailing separator you passed, survives untouched. Nothing is collapsed or resolved, because resolution is a filesystem question and this unit does not ask filesystem questions.
  • A dotfile is a name, not an extension. .bashrc has no extension.
  • These answers are the same on a machine where none of those paths exist. That is what “pure text” buys you: the algebra is testable without a filesystem, and it cannot fail.

The walk, where the world answers back

names := Entries(WorkDir) on fail leave;
subdirs := Directories(WorkDir) on fail leave;
Sort(address names);

Entries answers everything the directory holds — the . and .. self-references already skipped at the source — and Directories answers only what a descent could enter next. Both fail with the reason they cannot walk: Enoent for a missing path, Enotdir for a file where a directory was named, Eacces for a permission wall.

The order is the filesystem’s, and no filesystem promises it is sorted. So a caller who wants order sorts the answer — which is what the example does, with the arrays unit’s generic Sort: the comparison is the element type’s own <, cloned per element type, so ordering a list of strings needs no comparator and no function pointer.

Then the two units meet. Every name that comes back is a bare component, so Join puts it back onto its directory and Extension classifies it — text algebra applied to filesystem answers:

  3 entries, 1 of them directories
    alpha.txt  extension .txt  full path paths-demo/alpha.txt
    beta.log   extension .log  full path paths-demo/beta.log
    nested     (no extension)  full path paths-demo/nested

The split, in one contrast

The example ends by asking for a directory that is not there:

  DirName of a path that does not exist still answers: /no/such/place
  Entries failed with Enoent — the missing-path reason, by name

Same spelling, two units, two entirely proper answers. DirName answers happily because it never asked anyone. Entries fails with a name because it did. Neither returns a sentinel, and neither traps — this is the environment’s business, which is what the failure channel is for.

Directory creation and removal are still raw POSIX (MkDir, RmDir via imp … : posix), on the very same failure channel — so one idiom covers the curated road and the raw one, as Mica calls Linux shows in full.

Next

Memory classes: hosted, and a fixed arena — the same programs under the OS heap and under one fixed block, for the targets where there is no OS heap at all.