Files meet the grid
In most languages, reading a file means producing a string or a list of lines. Funge-98 does something different, because its data lives in a grid. The input-file instruction, written i, reads a file and places its contents into the grid, starting at a position the program chooses. The output-file instruction, written o, does the reverse: it takes a rectangular region of the grid and writes it out as a file.
This fits the language. A file of text lines becomes a block of rows, one line per row, and a block of cells in the program becomes a file with one line per row. The exact details are set by the specification and by each interpreter, so the description here is general.
What the instructions need
Both instructions take their arguments from the stack. They need:
- A file name, supplied as a zero-terminated string. Because strings are pushed in reverse order in quote mode, programs usually write the name backwards and close it with a zero.
- A flags value that selects options. A commonly documented option is a bit that switches between text and binary treatment.
- A position in the grid. For input, this is where the top-left of the loaded data will go. For output, a position plus a size describe the rectangle to write.
The order in which these are pushed is defined by the specification. Check it in the documentation for your interpreter rather than relying on memory, since getting it wrong is easy and the symptoms are confusing.
If anything goes wrong, such as a missing file or a name the system rejects, the instruction typically reflects: the pointer turns around instead of continuing. That gives a program a way to detect failure by arranging for a reflected pointer to land on error-handling code.
Text versus binary
The two modes matter more than they look.
Text mode treats the file as lines. Line endings become row changes, and the line-ending characters themselves are not stored as cells. When writing, trailing spaces on each row are generally trimmed so the file does not fill up with empty padding. This is the mode you want for source-like data and human-readable output.
Binary mode treats every byte as one cell, including bytes that would otherwise be line breaks. This is the mode for data that is not text. The price is that the loaded cells may include characters that look like instructions, so loaded data should be treated as inert unless you intend otherwise.
Which characters count as line endings can differ across operating systems, and files created on one system may look odd on another. If a loaded file seems to have an extra column of strange cells, mixed line endings are a likely cause.
Using loaded files
A few uses are common:
- Loading data tables. Put numbers or text into a region of the grid and read them back with the get instruction, in the same way as grid variables, only with more cells.
- Loading code. A program can pull in another piece of code and then send the pointer into it. This is a form of self-modifying code, and it is powerful and easy to get wrong.
- Saving results. Writing a region lets a program record the state of a computation, or generate a map or picture drawn with characters.
- Passing data between runs. A program can write a region at the end and read it at the start of the next run, which is an easy way to keep a small score or a configuration.
Remember that these operations change the playfield. A file loaded over existing cells replaces them, and that includes cells that hold your own instructions. A sensible habit is to reserve a region far from the code and always load into it.
Safety and portability
Because file access reaches outside the program, treat it with care.
- Untrusted programs. A program from an unknown source could read private files into its grid and write them elsewhere, or overwrite files you care about. Many interpreters offer an option to disable these instructions, and it is wise to use it when running code you did not write.
- Paths. A relative path depends on where the interpreter was started. Absolute paths depend on the machine. Neither is portable, and a program that assumes a particular folder will fail elsewhere.
- Overwriting. Writing to an existing file typically replaces it. Use a name that cannot clash with something important.
- Support is optional. Not every interpreter implements these instructions, and some limit them to a working folder. The environment query described in the article on system information includes flags that report what is allowed, so a careful program checks first.
For interpreter authors
Implementing file instructions touches several design choices that also appear in interpreter input output more generally: how to convert between bytes and cells when cells are wider than a byte, how to deal with a file larger than the current grid, and what to do with line endings. It also interacts with program loading, since the same routine that reads the main program file often serves the input instruction, and with your playfield data structure, because loading a big file may require the grid to grow. Good tests include empty files, files with no final line ending, very long lines and names that do not exist. A well-behaved implementation reflects rather than crashing in each failure case.
A cautious first experiment
If you want to try these instructions, start in an empty folder with a copy of a small text file. Load it into a spare region, write the same region to a new file, and compare the two. Then try the binary option and compare again. Seeing where the two files differ is the quickest way to understand how your interpreter treats line endings and trailing spaces, and it keeps your experiments away from files you would rather not lose.