The Machines · Data & storage

recio

Record-Oriented Binary I/O — machines/recio.shoddy

the recio machine's icon

Summary

recio turns a binary file into an evenly-spaced grid of records — like a car park with numbered bays, all exactly the same size. Every record takes up the same fixed number of bytes. So record number 5 always lives at the same spot no matter what's in it, and jumping straight to it is simple arithmetic: skip over the four bays in front and park in the fifth. That's the whole job of this machine — it does the offset sums for you. (An offset is a distance from the start of the file, counted in bytes.) You supply the record size in bytes and a pair of little functions that read and write one record. recio then handles seeking to record number k, reading it back, overwriting it in place, adding a new one on the end, or slurping the lot into a list.

It sits directly on Shoddy's binary builtins — Seek, BSize, and the GetStr / GetNum / PutStr / PutNum family. That is the same foundation isam builds its keyed files on, each machine standing on the builtins side by side. (Those builtins aren't only for records, either. Used freehand they serialize whole values — a header, then a payload — the way neural's NetSaveBin/NetLoadBin save a trained network. The guide's chapter 10 walks through both uses.)

A Brief History of the Unit Record

Before files there were cards, and a card was a record: eighty columns, fixed fields at fixed positions, one fact of the world per card. That is why the tabulators, sorters and collators that processed them were sold, for half a century, as unit record equipment. When the data moved from drawers of cards onto tape and then disk, the shape came with it, because the shape is what makes arithmetic possible. If every record is the same size, record k lives at offset (k − 1) × size. Finding it is a multiplication instead of a search.

That multiplication is the entire foundation of random-access data processing. It is what BASIC's random files, COBOL's fixed layouts, and every indexed file since have stood on, including isam one floor up from here. This machine is the shape itself with nothing added: you say how wide a bay is, and it does the multiplication.

Why It's Useful

Sometimes you don't want a text file full of lines — you want a file of uniform records you can reach into by number. Every record is the same size, so you can read or replace any one of them without touching the rest. Change record 900 in a file of a million, and you write exactly one record's worth of bytes — not the whole file. That's the payoff of fixed-size records, and it's exactly the trick that makes random-access data files fast.

On its own, recio is the right tool when your records already have a natural numbering — row 1, row 2, row 3 — and you want to seek to one by that number. The moment you'd rather look records up by a key (a name, an ID) instead of a bay number, that's where isam comes in. It keeps a sorted index on disk that translates a key into a record number, then leans on this very machine to fetch the bytes. Learn recio and you've learned the floor that isam stands on.

User's Guide

To use recio you decide three things and stick to them: the byte layout of one record, a reader that pulls one record out of an open file, and a writer that lays one back down. The reader and writer must agree on the exact same size — that's the contract that makes the bays line up. Records are numbered from 1.

Include "recio.shoddy"

Type Student
    Name  As String
    Score As Number

Def RdStudent(f As Number) As Student
    Student(GetStr(f, 12), GetNum(f))       ' 12 bytes name + 8 bytes score

Def WrStudent(f As Number, s As Student)
    PutStr(f, Name(s), 12)
    PutNum(f, Score(s))

Def Main()
    Let f = BOpen("class.dat")
    AppendRec(f, 20, WrStudent, Student("ADA", 96))
    AppendRec(f, 20, WrStudent, Student("LIN", 78))
    AppendRec(f, 20, WrStudent, Student("GRACE", 91))

    Print(RecCount(f, 20))                          ' 3
    Print(Score(GetRec(f, 2, 20, RdStudent)))       ' 78  (record 2 is LIN)

    PutRec(f, 2, 20, WrStudent, Student("LIN", 85)) ' overwrite record 2 in place
    Print(Score(GetRec(f, 2, 20, RdStudent)))       ' 85

    Each(AllRecs(f, 20, RdStudent), Fn(s) => Print(Name(s)))
                                                     ' ADA LIN GRACE
    BClose(f)

The 20 threaded through every call is the record size: 12 bytes for the name plus 8 for the score. A few things worth remembering:

If you find yourself wanting to fetch a record by name rather than by bay number, don't build the index by hand — reach for isam, which does exactly that on the same binary builtins.

Builtins

The twelve binary-file words the runtime dispatches — not defined here, documented here

These are not recio's Defs. The engine dispatches them, and a Def whose name is a builtin is refused. They are listed on this page because recio is the machine whose domain they belong to — this whole machine is a layer over exactly these twelve. And a reader who opens the record-I/O machine looking for GetStr should find it here, rather than be expected to know that the flat reference exists. They need no Include. The same twelve are documented word for word in machines/recio.shoddy's own header block.

Classic BASIC record files. A handle is a Number, drawn from the same table of sixteen that sockets use. Byte positions are 1-based — counted from 1, not from 0. Every transfer word advances the position by exactly the width it moved, and that is what makes record layouts computable. A Name(12) + Score(8) record is 20 bytes, so record k starts at 1 + (k-1)*20. That sum is RecPos below, and it is the whole of what this machine adds. Reading past end of file aborts, and so does over-running a PutStr field. All twelve are effectful.

Opening and closing

WordDescription
BOpen(path)Open path read/write and answer its handle, creating the file if it is absent; up to 16 handles at once. Aborts if it cannot open. The open-or-create is a trap worth naming: asking whether a binary file is there by opening it leaves an empty file behind when it was not.
TryBOpen(path)Ok(handle), or Err(why, 0) with why = CANNOT OPEN 'path' (…) from the same closed set TryReadFile uses, plus TOO MANY OPEN FILES. It differs from BOpen twice over, and both differences are the point. The failure is reported rather than fatal. And it opens an existing file only. Nothing can learn a file's size or its leading bytes without opening it, so this is the only way to pre-flight a binary read at all.
BClose(h)Close the handle and free its slot.

Position

WordDescription
Seek(h, pos)Move to byte position pos, counting from 1.
BPos(h)Where the handle is now, counting from 1.
BSize(h)How many bytes the file holds. RecCount below is this divided by the record size.

Typed transfer

WordDescription
PutNum(h, x)Write x as an 8-byte IEEE double — the standard binary spelling of a number — and advance 8.
GetNum(h)Read an 8-byte double and advance 8.
PutBool(h, b)Write b as one byte and advance 1.
GetBool(h)Read one byte as a Boolean and advance 1.
PutStr(h, s, width)Write exactly width bytes: s, zero-padded out to the field. An over-length string aborts rather than being silently truncated, which is what keeps a fixed layout honest.
GetStr(h, width)Read exactly width bytes and strip the zero padding back off.

Word Reference

Every word this machine exports

Locating a record

WordDescription
RecPos(k, size)The byte position where record k begins, computed as 1 + (k - 1) * size. Pure arithmetic — it doesn't touch any file. This is the offset sum the rest of the machine is built on.
RecSeek(f, k, size)Moves the open file f's position to the start of record k, so the next read or write lands on that record.
RecCount(f, size)How many whole records the file f holds: its byte length (BSize) divided by size, rounded down. Instant — reads no records.

Reading and writing one record

WordDescription
GetRec(f, k, size, reader)Seeks to record k and returns it, using your reader function to turn the bytes into a value. The record's type is whatever your reader produces.
PutRec(f, k, size, writer, v)Seeks to record k and writes value v there with your writer function, overwriting whatever was in that slot.
AppendRec(f, size, writer, v)Writes value v as a brand-new record just past the current end of the file — at position RecCount(f, size) + 1.

Reading them all

WordDescription
AllRecs(f, size, reader)Reads every record in the file, in order from 1 to RecCount, and hands them back as a list. Uses your reader for each one.

Who Uses It

UserHow
neuralGetRec / PutRec are the binary model file — the bit-exact save/load contract between trainer and predictor.

The Machines It Uses

None — a thin layer over the binary-file builtins, standalone by design.