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:
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:
OPENthe source forREAD.OPENthe destination for stagedCREATEorREPLACE.- Repeat
READandWRITEat explicit offsets. Advance byREAD.gotandWRITE.put; retain and retry any short-progress suffix. CLOSE(commit = 1)the destination and require its successful commit reply.- Close the source on every path.
- 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¶
- Use
FILEfor named bytes; use DATA for records/revisions. - Query each mount; never infer its powers from a name.
- Page listings and restart if generation changes.
- Carry handle and generation together.
- Respect the advertised maximum span.
- Treat
CLOSE(commit)as the durable Save point. - Use expected generations for destructive edits.
- Spell cross-mount transfer as copy, then delete.
- Close on success, cancellation, refusal, and window exit.