I/O Operations in BLISS
How BLISS handles input and output - console formatting, reading from stdin, and file I/O through external C bridge routines with the blissc compiler
Input and output are where BLISS shows its systems-programming heritage most clearly: the language has no I/O statements at all. There is no print, no read, no file-handling syntax built into the language. This is deliberate. BLISS was designed to be minimal and platform-independent, so every byte that enters or leaves a program travels through library routines or operating-system calls.
On the original DEC hardware, that meant VMS system services like LIB$PUT_OUTPUT and LIB$GET_INPUT. With the modern blissc cross-compiler, we bridge to the C standard library instead. The pattern is the same one you saw in Hello World: BLISS declares an EXTERNAL ROUTINE, a small C wrapper implements it (using an uppercase name, because blissc uppercases every identifier), and gcc links the two object files together.
The upside of this design is a clean separation of concerns that is characteristic of good systems software: BLISS handles computation, and a thin wrapper handles platform-specific I/O. In this tutorial you’ll write BLISS that produces formatted console output, reads a value from standard input, and writes to and reads from a file — all by orchestrating C bridge routines while keeping the interesting logic in BLISS.
Formatted Console Output
Hello World called a single fixed-string function. Real programs pass computed values to their output routines. Here BLISS computes a table of squares and hands each pair of integers to a formatting routine implemented in C. Note that all values are BLISS fullwords, which map cleanly to C’s long on a 64-bit target.
Create a file named output.bli:
MODULE io_output =
BEGIN
EXTERNAL ROUTINE
print_header,
print_pair;
GLOBAL ROUTINE show_squares : NOVALUE =
BEGIN
LOCAL square;
print_header();
INCR i FROM 1 TO 5 DO
BEGIN
square = .i * .i;
print_pair(.i, .square)
END
END;
END
ELUDOM
Create a file named output_wrapper.c:
| |
The BLISS side owns the arithmetic (.i * .i) and the loop; the C side owns nothing but the printf format strings. This is printf-style formatted output — the formatting lives in the bridge because BLISS has no format facility of its own.
Reading From Standard Input
Reading input is just another external routine, except this one returns a value. A BLISS routine declared without NOVALUE returns a fullword, and a C function returning long matches that convention. Here we read a number, compute its factorial in BLISS, and print the result.
Create a file named input.bli:
MODULE io_input =
BEGIN
EXTERNAL ROUTINE
read_int,
print_result;
GLOBAL ROUTINE run : NOVALUE =
BEGIN
LOCAL n, fact;
n = read_int();
fact = 1;
INCR i FROM 1 TO .n DO
fact = .fact * .i;
print_result(.n, .fact)
END;
END
ELUDOM
Create a file named input_wrapper.c:
| |
Notice how n = read_int(); stores the returned value at the address n, and every later use reads it back with the dot operator (.n). This is the BLISS convention you’ve seen throughout the series: the bare name is an address, .name is the contents. The if (scanf(...) != 1) check in the wrapper is the natural place for I/O error handling — because I/O is external, error detection lives in the bridge, not in the BLISS logic.
Writing To and Reading From a File
File I/O follows the same bridge pattern, with the C wrapper holding the FILE * handle. BLISS drives the process — opening the file, looping to write entries, closing it, then asking the wrapper to read the whole file back so we have deterministic console output to verify.
Create a file named filereport.bli:
MODULE io_file =
BEGIN
EXTERNAL ROUTINE
open_report,
write_entry,
close_report,
dump_report;
GLOBAL ROUTINE make_report : NOVALUE =
BEGIN
LOCAL total;
total = 0;
open_report();
INCR i FROM 1 TO 5 DO
BEGIN
total = .total + .i;
write_entry(.i, .total)
END;
close_report();
dump_report()
END;
END
ELUDOM
Create a file named filereport_wrapper.c:
| |
The BLISS module maintains a running total across loop iterations and calls write_entry for each line. The file is opened for writing, filled, closed, then reopened for reading inside dump_report — the classic open/write/close then open/read/close file lifecycle, expressed entirely through bridge routines.
Running with Docker
Each example compiles its BLISS source to an object file with blissc, then links it against its C wrapper with gcc, exactly as in Hello World.
| |
The -i flag on the input example keeps stdin open so the piped 5 reaches scanf. The file example writes report.txt into the mounted working directory, so it remains on your host after the container exits.
Expected Output
Example 1 (output):
n n squared
1 1
2 4
3 9
4 16
5 25
Example 2 (input) with 5 piped to stdin:
factorial(5) = 120
Example 3 (filereport):
After adding 1, running total = 1
After adding 2, running total = 3
After adding 3, running total = 6
After adding 4, running total = 10
After adding 5, running total = 15
Key Concepts
- BLISS has no built-in I/O — there is no print or read syntax in the language. Every input and output operation is an
EXTERNAL ROUTINEbridged to a library or OS call. - The C wrapper does the platform work — with
blissc, output routines wrapprintf/fprintf, input routines wrapscanf, and file routines hold theFILE *handle. On classic VMS you would callLIB$PUT_OUTPUT,LIB$GET_INPUT, and RMS services instead. - Uppercase symbol names are mandatory —
blisscuppercases every identifier, so a BLISS routinewrite_entrymust be implemented in C asWRITE_ENTRY. - Input routines return fullwords — a BLISS routine declared without
NOVALUEreturns a value, matching a C function that returnslongon a 64-bit target. Assign it withn = read_int();and read it back with.n. - The dot operator still rules — you store computed values at a variable’s address and dereference with
.every time you read them, even in I/O-driven code. - Error handling lives in the bridge — because I/O is external, checks like verifying
scanf’s return value or a non-nullfopenresult belong in the C wrapper, not the BLISS logic. - Separation of concerns is the payoff — BLISS owns computation and control flow; the wrapper owns bytes in and bytes out. This is the same discipline that made BLISS effective for writing operating systems and compilers.
Running Today
All examples can be run using Docker:
docker pull codearchaeology/bliss:latest
Comments
Loading comments...
Leave a Comment