Skip to content

Files & file manipulation

FILE is bytes at a name. DATA is a revision of an identity.

FILE is D/BASIC's portable filesystem-manipulation family: list a directory, inspect an entry, copy bytes, save an output, rename, move, trash, restore, or make a directory. It is intentionally distinct from the database-equivalent DATA Intent family, and from the tiny immutable DATA / READ tables embedded in source.

Need Correct boundary
flash:/NOTES.TXT and its bytes FILE
a d/Write document identity and revision DATA Intents
fixed lookup values compiled into the program DATA / READ

The FILE family does not make a laptop home directory, browser sandbox, and D/OS volume falsely identical; it lets a runner offer one governed namespace with the same meaning.

Mounts are the top of the namespace

The runner presents a mount table. Local mounts are protected known entries; a Brain, removable medium, or network provider may advertise additional entries but cannot rewrite the local truth.

Example mount Typical role
flash installed/local writable store where the product offers one
dos local FAT or media partition
brain advertised peer storage
ftp0, sftp0, scp0 remote storage published after a governed transfer provider is ready
usb0 reserved removable-media name

People may see flash:/NOTES.TXT. A carrier uses a mount index plus a mount-relative path; the host's configured directory never becomes a program value. Protected areas such as installation/package state never become an editable convenience path.

Most applications begin with the typed D/Works projection:

IMPORT dworks.files BAKED

It supplies mount, entry, stream, and edit builders while retaining the exact FILE semantics below. Imports & Libraries explains BAKED, SHARED, and exact library binding. The raw Intent examples in this chapter expose the underlying contract for library authors and curious readers; application code need not memorize record offsets.

Declare the need, then ask the mount

APP "MORNING REPORT"
  LABEL "Morning Report"
  NEEDS FILES
END APP

DIM MountRequest%(0 TO 1)
DIM MountReply%(0 TO 17)

' Request mount index zero. A typed library normally decodes the reply.
MountRequest%(0) = 0
INTENT FILE.QUERY_MOUNT(MountRequest%(), MountReply%())

PRINT "MOUNT QUERY COMPLETE"
END

QUERY_MOUNT answers more than “is files available?” It reports a particular mount's name, state, permissions, latency class, capacity/space facts, path/name bounds, and the current number of mounts.

State Meaning
PRESENT provider answered and publishes its capabilities
ABSENT absent by evidence; capability word is zero
DEGRADED present and serving, but one or more otherwise declared mount capabilities are currently refused or withdrawn

There is no dead mount that merely lists nothing. “SFTP is absent” and “SFTP has no matching files” are different facts, and scripts need both.

The eight file verbs

Intent Job Retry shape
FILE.QUERY_MOUNT inspect one mount idempotent
FILE.LIST fetch one bounded directory page idempotent
FILE.STAT inspect one named entry idempotent
FILE.OPEN open read, append, create, or replace not idempotent
FILE.READ read a bounded byte span idempotent
FILE.WRITE write a span at an explicit offset idempotent
FILE.CLOSE release a session, optionally committing staged output not idempotent
FILE.EDIT delete, trash, restore, rename, move, or make a directory generation guarded

All calls use rank-one INTEGER carriers, two little-endian bytes per element. Array size is exact; larger buffers are not accepted. The runtime stages replies until a terminal successful decode, so pending, malformed, stale, failed, or aliased calls never publish half a record. Application authors should use a typed wrapper rather than manually write carrier fields.

List first, then act on what you saw

LIST takes a mount-relative folder, filter, optional small match, and ordinal skip; it returns at most eight entries. Matching is host-side, avoiding one whole-string scan per entry on a constrained machine. The pattern can be exact, prefix*, or *.suffix; it is deliberately not a general glob.

DIM ListRequest%(0 TO 107)
DIM Page%(0 TO 167)

' A file-library encoder supplies mount, directory, filter, "*.BAS", and skip.
INTENT FILE.LIST(ListRequest%(), Page%())
END

The reply contains count, more, directory generation, optional total, and up to eight 40-byte entries. Each entry has kind (file/directory/other), flags, length, and a bounded name. An unrepresentable long name is flagged, never truncated into a name that might open the wrong file.

Pages are ordinal rather than lingering sessions, so a script can abandon a walk without leaking a handle. The honest trade-off is that a changed directory can skip/repeat an ordinal between pages. Retain the returned generation; if it changes, restart the walk. STAT answers the smaller question “what is this entry now?” A normal not-found status is distinct from a malformed/protected request refusal.

Open, read, write, close

OPEN has four modes:

Mode Meaning
READ read existing bytes
CREATE stage new output, replacing at successful commit
APPEND append to an existing file
REPLACE stage replacement but refuse if the name is absent

The reply carries a handle and generation, current length, and a per-handle maximum span. A stale handle refuses instead of addressing a recycled slot. READ and WRITE each use bounded byte chunks, up to 256 bytes.

DIM OpenRequest%(0 TO 97)
DIM Handle%(0 TO 7)
DIM ReadRequest%(0 TO 7)
DIM Chunk%(0 TO 131)
DIM CloseRequest%(0 TO 7)

' Typed encoders provide path, mode, handle/generation, offset, and count.
INTENT FILE.OPEN(OpenRequest%(), Handle%())
INTENT FILE.READ(ReadRequest%(), Chunk%())
INTENT FILE.CLOSE(CloseRequest%(), Handle%())
END
Row Request Reply
QUERY_MOUNT 4 B / 2 INTEGERs 36 B / 18
LIST 216 B / 108 336 B / 168
STAT / OPEN 196 B / 98 16 B / 8
READ 16 B / 8 264 B / 132
WRITE 272 B / 136 16 B / 8
CLOSE 16 B / 8 16 B / 8
EDIT 392 B / 196 16 B / 8

Atomic replacement makes Save honest

For CREATE and REPLACE, CLOSE(commit = 1) is the atomic replacement point. CLOSE(commit = 0) abandons staging. A cancelled Save therefore does not replace the old file with a prefix of the new one.

Commit may still refuse—for example, a journal-backed mount might exhaust capacity at that moment. The commit reply, not the final WRITE and not the Save button, is durability. WRITE carries an explicit offset so replaying the same bytes at the same place has the same result.

Copy is a workflow, not a magic verb

A truthful copy is built from the four stream rows:

  1. OPEN the source for READ.
  2. OPEN the destination for staged CREATE or REPLACE.
  3. Repeat READ and WRITE at explicit offsets. Advance by READ.got and WRITE.put; retain and retry any short-progress suffix.
  4. CLOSE(commit = 1) the destination and require its successful commit reply.
  5. Close the source on every path.
  6. If the user requested a cross-mount move, delete the source only after destination commit—and report a delete failure separately.

That last distinction matters. A completed copy followed by a refused delete is still a completed copy; calling the whole operation “failed” would hide where the new bytes went.

Exporting a revisioned database object uses this FILE destination workflow only after reading its exact DATA identity and revision. Importing reads a FILE, validates it privately, then creates a new DATA artifact. These are explicit copies across two authority boundaries, never a live link disguised as a path.

Safe edits: delete, trash, rename, move, mkdir

FILE.EDIT groups namespace changes under one compact record:

Operation Meaning
DELETE remove one entry where permitted
TRASH / RESTORE use the mount's governed trash
RENAME change a name within one mount
MOVE move within the same mount
MKDIR make one directory

An edit includes expect_generation: the generation observed while listing or inspecting. If someone changed the directory first, it refuses stale state instead of editing a surprise replacement. expect_generation = 0 says the script intentionally did not inspect first.

DIM EditRequest%(0 TO 195)
DIM EditReply%(0 TO 7)

' Encoder supplies mount, EDIT_RENAME, observed generation, old path, new path.
INTENT FILE.EDIT(EditRequest%(), EditReply%())
END

Cross-mount MOVE is refused because it is really copy then delete and may fail between those acts. Let the UI say “Copying,” commit the destination, then delete only as a visible separate step.

A file-script phrasebook

Familiar shell idea D/BASIC FILE workflow
ls / dir page FILE.LIST, restarting if its directory generation changes
stat FILE.STAT one mount-relative name
cat / read bytes OPEN(READ) → bounded READ loop → CLOSE
redirect / replace OPEN(CREATE or REPLACE) → bounded WRITE loop → CLOSE(commit = 1)
copy source read loop plus staged destination write loop
rename FILE.EDIT(RENAME) with the observed generation
move FILE.EDIT(MOVE) on one mount; explicit copy-then-delete across mounts
remove FILE.EDIT(DELETE) for one entry
new directory FILE.EDIT(MKDIR)
trash / restore FILE.EDIT(TRASH or RESTORE) when that mount advertises it

There is no hidden recursive delete. A tree walk is a visible, interruptible script, which is exactly where consequential repetition belongs.

Console scripts and GUI applications use the same surface

A D/BASIC program without window/event handlers is a console script, making FILE a good base for compact pipeline work:

APP "LISTING"
  LABEL "BASIC listing"
  NEEDS FILES
END APP

' Query mounts, page FILE.LIST output, PRINT entries, then choose a shell code.
PRINT "LISTING COMPLETE"
END

Local mounts answer on the first poll. Media, peer, and network mounts may park the script's task while the desktop continues. A GUI must never spin in DRAW: keep operation/progress state in its model, invalidate on changes, and leave cancel/close reachable.

Permissions and protected paths stay visible

Files are places, not owner-scoped database records. Every mount publishes its own capabilities:

Capability Permits
LIST, STAT, READ inspect namespace and bytes
WRITE, CREATE append or stage output
DELETE, RENAME, MOVE, MKDIR edit namespace
TRASH, RESTORE governed trash use
ATOMIC_REPLACE atomic staged replacement

A read-only volume can truthfully offer only inspection. A protected system path says PROTECTED PATH, not fake NOT FOUND. Bad matches, capacity, stale generations, missing rows, and cross-mount moves likewise produce named refusals that explain the blocker.

ON REFUSAL GOTO FileProblem

' A FILE operation belongs here.
PRINT "DONE"
END

FileProblem:
PRINT "FILE OPERATION REFUSED. RULE "; ERR
END

One source, several routes

Runner Honest route
browser / WebAssembly an admitted sandbox/provider namespace, never arbitrary browser-visible files
Windows, Linux, macOS configured mount directories through the same bounded rows and atomicity checks
D/OS local store local flash/media route with published limits
D/OS Brain/network extra mount only when the actual provider is ready
no file adapter FILES / FILE.* refuse by name before the program begins

The runner may improve throughput or mount roster. It may not borrow a home directory, invent writability, call a partial copy a move, or silently truncate names and bytes.

Why this is not classic OPEN #1

D/BASIC does not add OPEN #, INPUT #, PRINT #, LINE INPUT #, GET, PUT, or FIELD as synchronous file-number statements. A channel number would hide the mount, parked operation, generation, and refusal—the very facts portable file work needs to keep visible.

Typed D/Works helpers can make the eight FILE rows pleasant to call without inventing a second filesystem model. Raw host paths, process-global current directories, native file descriptors, recursive mutation, and general glob engines do not cross the language boundary.

File checklist

  1. Use FILE for named bytes; use DATA for records/revisions.
  2. Query each mount; never infer its powers from a name.
  3. Page listings and restart if generation changes.
  4. Carry handle and generation together.
  5. Respect the advertised maximum span.
  6. Treat CLOSE(commit) as the durable Save point.
  7. Use expected generations for destructive edits.
  8. Spell cross-mount transfer as copy, then delete.
  9. Close on success, cancellation, refusal, and window exit.

Next: Database & DATA Intents →