Intermediate

I/O Operations in Forth

Learn console and file I/O in Forth - EMIT, KEY, TYPE, ACCEPT, formatted numbers, and the ANS file word set with Docker-ready examples

Input and output in Forth follow the same rule as everything else in the language: I/O is performed by words that take their arguments from the data stack and leave their results there. There is no printf format string and no file object — instead you compose small words like EMIT, TYPE, and WRITE-LINE, feeding them addresses, lengths, and character codes through the stack.

This stack-oriented approach makes Forth I/O remarkably explicit. A string is not a first-class object; it is a pair of numbers on the stack — an address and a length ( addr len ). A character is just its numeric code. Once you internalize that data flows through the stack, the whole I/O vocabulary becomes a handful of composable words rather than a sprawling standard library.

In this tutorial you will learn how Forth writes characters and strings to the terminal, formats numbers, reads a line of input with ACCEPT, and uses the ANS Forth file word set (CREATE-FILE, WRITE-LINE, OPEN-FILE, READ-LINE) to write and read files. All examples run under Gforth.

Console Output: EMIT, TYPE, and Numbers

The Hello World page introduced ." for printing a literal string. Here we go deeper: EMIT prints a single character from an ASCII code, TYPE prints a string given as ( addr len ), and . prints a number from the stack. Words like SPACE and SPACES control layout.

Create a file named output.fth:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
\ Console output in Forth

\ EMIT prints one character from its ASCII code ( c -- )
72 EMIT 105 EMIT CR          \ prints "Hi" then a newline

\ TYPE prints a string given as an address/length pair ( addr len -- )
S" Stack-based output" TYPE CR

\ . prints the number on top of the stack, followed by a space
42 . CR
2 3 + . CR                   \ arithmetic result printed

\ SPACES inserts blank columns for simple layout
." A" 3 SPACES ." B" CR

\ Values print top-of-stack first
1 2 3 . . . CR               \ prints 3 then 2 then 1

bye

Key points:

  • EMIT consumes one number and prints the character with that code. 72 is H, 105 is i.
  • S" ..." pushes a string as ( addr len ); TYPE consumes that pair and prints those bytes.
  • . (“dot”) prints the top stack item as a signed number followed by a trailing space. Because 1 2 3 . . . pops from the top down, it prints 3 2 1.

Formatted Number Output

Forth formats numbers by controlling the numeric base and field width rather than with format specifiers. .R right-justifies a number in a fixed-width column, and switching BASE with HEX/DECIMAL changes how numbers are displayed.

Create a file named format.fth:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
\ Formatted number output in Forth

\ .R prints a number right-justified in a field ( n width -- )
42 5 .R CR                   \ "   42" (width 5)
7  5 .R CR                   \ "    7"

\ Change the numeric base to print in hexadecimal
HEX
255 . CR                     \ prints "FF "
DECIMAL
255 . CR                     \ back to base 10: "255 "

\ Compose your own labeled-output word
: LABEL ( n -- )  ." Value = " . CR ;
100 LABEL

bye

Notice that .R does not append a trailing space (unlike .), which is exactly why it is useful for aligning columns. The HEX and DECIMAL words simply set the global BASE variable, so every subsequent numeric conversion — input and output alike — uses that base until you change it back.

Reading Input with ACCEPT

To read a line of text you first reserve a buffer with CREATE ... ALLOT, then call ACCEPT, which reads up to max characters into the buffer and returns the actual number of characters read. Because Forth strings are ( addr len ) pairs, the buffer address plus that returned length is everything TYPE needs to echo the input back.

Create a file named input.fth:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
\ Reading a line of text from standard input

CREATE name-buf 80 ALLOT     \ reserve an 80-byte input buffer

." Enter your name: "
name-buf 80 ACCEPT           ( addr max -- len )
." Hello, "
name-buf SWAP TYPE           ( len -- ; print len bytes from name-buf )
." ! Welcome to Forth." CR

bye

Walking the stack: ACCEPT consumes the buffer address and the maximum length, leaving just len. Pushing name-buf gives ( len addr ), and SWAP reorders it to ( addr len ) — the exact shape TYPE expects. KEY is the single-character counterpart to ACCEPT: it waits for one keystroke and pushes its ASCII code, which pairs naturally with EMIT for character-at-a-time interaction.

Writing and Reading Files

Gforth implements the ANS Forth file word set. Each file word returns an I/O result code (ior) that is zero on success and non-zero on error, which pairs perfectly with ABORT" — a word that aborts with a message when the flag on the stack is true (non-zero). This single example writes a file, closes it, then reopens it and reads it back line by line.

Create a file named file_io.fth:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
\ File I/O in Forth: write a file, then read it back

\ --- Writing ---
\ CREATE-FILE ( addr len fam -- fileid ior ); W/O = write-only
S" notes.txt" W/O CREATE-FILE ABORT" create failed"
CONSTANT out-file
\ WRITE-LINE writes a string plus a newline ( addr len fileid -- ior )
S" Forth reads and writes files" out-file WRITE-LINE DROP
S" using the ANS file word set"  out-file WRITE-LINE DROP
out-file CLOSE-FILE DROP

\ --- Reading it back ---
CREATE line-buf 256 ALLOT        \ buffer for one line at a time
S" notes.txt" R/O OPEN-FILE ABORT" open failed"
CONSTANT in-file

." --- notes.txt ---" CR
\ READ-LINE ( addr max fileid -- len flag ior )
\   flag is true while a line was read, false at end of file
BEGIN
    line-buf 256 in-file READ-LINE ABORT" read failed"
WHILE                            ( -- len ; loop while flag was true )
    line-buf SWAP TYPE CR        ( print the line just read )
REPEAT
DROP                             \ drop the leftover length at EOF

in-file CLOSE-FILE DROP
bye

The reading loop is a classic Forth idiom: READ-LINE leaves ( len flag ior ); ABORT" consumes ior and bails on any error; WHILE consumes flag and keeps looping as long as a line was actually read. When the file ends, flag is false, the loop exits, and the final DROP discards the leftover length count.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Pull the Gforth image
docker pull forthy42/gforth:latest

# Console output examples
docker run --rm -v $(pwd):/app -w /app forthy42/gforth:latest gforth output.fth
docker run --rm -v $(pwd):/app -w /app forthy42/gforth:latest gforth format.fth

# File I/O example (creates notes.txt in your current directory)
docker run --rm -v $(pwd):/app -w /app forthy42/gforth:latest gforth file_io.fth

# Input example: pipe a name into standard input with -i
echo "Ada" | docker run --rm -i -v $(pwd):/app -w /app forthy42/gforth:latest gforth input.fth

Each file ends with bye, so Gforth exits cleanly after running it. For the input example, the -i flag keeps standard input open so ACCEPT can read the piped line.

Expected Output

Running output.fth produces:

Hi
Stack-based output
42 
5 
A   B
3 2 1 

Running format.fth produces:

   42
    7
FF 
255 
Value = 100 

Running file_io.fth writes notes.txt and then prints:

--- notes.txt ---
Forth reads and writes files
using the ANS file word set

Running input.fth with Ada supplied on standard input produces:

Enter your name: Hello, Ada! Welcome to Forth.

Key Concepts

  • Strings are ( addr len ) pairs — there is no string object; S" pushes an address and a length, and TYPE consumes that pair to print it.
  • EMIT/KEY work on single characters — they move one ASCII code between the stack and the terminal, while TYPE/ACCEPT handle whole buffers.
  • . adds a trailing space; .R does not — use .R with a width when you need aligned columns, and switch BASE with HEX/DECIMAL for other radixes.
  • Input needs a buffer — reserve space with CREATE ... ALLOT, then let ACCEPT fill it and return the real length.
  • File words return an ior — a zero result means success; pairing each call with ABORT" turns a failed open, write, or read into an immediate, readable error.
  • READ-LINE drives EOF loops — its flag result feeding a BEGIN ... WHILE ... REPEAT loop is the idiomatic way to read a file to the end.
  • I/O is just more words — because every I/O primitive composes through the stack, you can wrap your own vocabulary (like the LABEL word) to build higher-level, application-specific output.

Running Today

All examples can be run using Docker:

docker pull forthy42/gforth:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining