Intermediate

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>

/* blissc uppercases all identifiers, so BLISS names become uppercase symbols */
void SHOW_SQUARES(void);

void PRINT_HEADER(void) {
    printf("%-6s %s\n", "n", "n squared");
}

void PRINT_PAIR(long n, long square) {
    printf("%-6ld %ld\n", n, square);
}

int main(void) {
    SHOW_SQUARES();
    return 0;
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

void RUN(void);

/* Returns a fullword value read from stdin */
long READ_INT(void) {
    long n = 0;
    if (scanf("%ld", &n) != 1) {
        n = 0;   /* basic error handling: treat bad input as 0 */
    }
    return n;
}

void PRINT_RESULT(long n, long fact) {
    printf("factorial(%ld) = %ld\n", n, fact);
}

int main(void) {
    RUN();
    return 0;
}

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:

 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
29
30
31
32
#include <stdio.h>

void MAKE_REPORT(void);

static FILE *report;

void OPEN_REPORT(void) {
    report = fopen("report.txt", "w");
}

void WRITE_ENTRY(long n, long total) {
    fprintf(report, "After adding %ld, running total = %ld\n", n, total);
}

void CLOSE_REPORT(void) {
    fclose(report);
}

/* Reopen the file for reading and echo it to stdout */
void DUMP_REPORT(void) {
    FILE *f = fopen("report.txt", "r");
    int c;
    while ((c = fgetc(f)) != EOF) {
        putchar(c);
    }
    fclose(f);
}

int main(void) {
    MAKE_REPORT();
    return 0;
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the BLISS compiler image
docker pull codearchaeology/bliss:latest

# Example 1: formatted console output
docker run --rm -v $(pwd):/app -w /app codearchaeology/bliss:latest \
    sh -c 'blissc -o output.o output.bli && gcc -o output output_wrapper.c output.o && ./output'

# Example 2: reading from stdin (note -i and the piped input)
echo 5 | docker run --rm -i -v $(pwd):/app -w /app codearchaeology/bliss:latest \
    sh -c 'blissc -o input.o input.bli && gcc -o input input_wrapper.c input.o && ./input'

# Example 3: file write then read-back
docker run --rm -v $(pwd):/app -w /app codearchaeology/bliss:latest \
    sh -c 'blissc -o filereport.o filereport.bli && gcc -o filereport filereport_wrapper.c filereport.o && ./filereport'

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 ROUTINE bridged to a library or OS call.
  • The C wrapper does the platform work — with blissc, output routines wrap printf/fprintf, input routines wrap scanf, and file routines hold the FILE * handle. On classic VMS you would call LIB$PUT_OUTPUT, LIB$GET_INPUT, and RMS services instead.
  • Uppercase symbol names are mandatoryblissc uppercases every identifier, so a BLISS routine write_entry must be implemented in C as WRITE_ENTRY.
  • Input routines return fullwords — a BLISS routine declared without NOVALUE returns a value, matching a C function that returns long on a 64-bit target. Assign it with n = 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-null fopen result 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
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining