Hello World in BLISS
Your first BLISS program - the classic Hello World example with Docker setup using the blissc compiler
Every programming journey starts with Hello World. BLISS is a systems programming language with no built-in I/O, so our Hello World requires two files: a BLISS module and a small C wrapper. Let’s see how it works.
The Code
BLISS programs are organized as modules and have no built-in print function. To output text, we declare an external C function and call it from BLISS. The blissc compiler uppercases all identifiers, so the C wrapper must use uppercase function names to match.
Create a file named hello.bli:
MODULE hello =
BEGIN
EXTERNAL ROUTINE
print_hello;
GLOBAL ROUTINE hello_world : NOVALUE =
BEGIN
print_hello()
END;
END
ELUDOM
Create a file named wrapper.c:
| |
Understanding the Code
The BLISS Module
MODULE hello =- Declares a module namedhello. Every BLISS source file is wrapped in a module.BEGIN ... END- Block delimiters (like curly braces in C)EXTERNAL ROUTINE print_hello- Declares a routine defined elsewhere (in our C wrapper)GLOBAL ROUTINE hello_world : NOVALUE =- Defines a routine visible to the linker.NOVALUEmeans it returns no value (like C’svoid).print_hello()- Calls the external C function to print our messageELUDOM- Ends the module. It’s “MODULE” spelled backwards - a BLISS tradition!
The C Wrapper
Since BLISS has no built-in I/O and the blissc compiler’s MODULE (MAIN=...) attribute is not yet implemented, we need a C file that provides:
main()- The program entry point that calls our BLISS routinePRINT_HELLO()- The I/O bridge function. Note the uppercase name - blissc uppercases all BLISS identifiers in the object file, soprint_helloin BLISS becomes the symbolPRINT_HELLO
Symbol Name Convention
The blissc compiler converts all identifiers to uppercase during compilation. This means:
- BLISS
hello_world→ object symbolHELLO_WORLD - BLISS
print_hello→ object symbolPRINT_HELLO
The C wrapper must use these uppercase names for the linker to resolve the symbols correctly.
Running with Docker
The easiest way to run BLISS without installing the compiler locally uses our custom Docker image with the blissc compiler. Since BLISS needs a C wrapper for I/O, create both files in the same directory before running:
| |
Understanding the Docker Command
The compilation happens in two steps:
blissc -o hello.o hello.bli- The blissc compiler compiles the BLISS source to a native object file (.o) via LLVMgcc -o hello wrapper.c hello.o- GCC compiles the C wrapper and links it with the BLISS object file to produce an executable./hello- Runs the resulting program
Expected Output
Hello, World!
Why Two Files?
BLISS was designed for systems programming on DEC hardware, where I/O was always handled through operating system calls rather than language primitives. On the original VMS systems, you would use LIB$PUT_OUTPUT to print text. With the modern blissc cross-compiler, we bridge to the C standard library instead.
This two-file pattern is actually common in systems programming - the language handles computation, and a thin wrapper handles platform-specific I/O.
The Dot Operator
One of BLISS’s most distinctive features is that variable names evaluate to their address, not their contents. To get the value stored at a variable, you use the dot (.) operator:
OWN x;
x = 42; ! Store 42 at the address of x
y = .x; ! y gets the contents of x (42)
y = .x + 1; ! y gets 43
This is the opposite of most languages where x gives you the value. In BLISS, x gives you the address, and .x gives you the value. This makes pointer manipulation natural for systems programming.
Module Structure
Every BLISS source file follows this structure:
MODULE name =
BEGIN
! Declarations
OWN counter;
EXTERNAL ROUTINE some_function;
! Routines
GLOBAL ROUTINE my_routine : NOVALUE =
BEGIN
! statements
END;
END
ELUDOM
Key elements:
OWN- Declares a static (module-level) variableLOCAL- Declares a stack-allocated variable (inside routines)GLOBAL ROUTINE- A routine visible outside the moduleROUTINE- A routine private to the moduleNOVALUE- Indicates the routine doesn’t return a value
Control Flow
BLISS uses structured control flow with no goto:
! If-then-else (produces a value!)
result = (IF .x GTR 0 THEN .x ELSE -.x);
! While loop
WHILE .count GTR 0 DO
BEGIN
count = .count - 1
END;
! Counted loop
INCR i FROM 0 TO 9 DO
BEGIN
! i goes from 0 to 9
END;
Notice the keyword comparison operators (GTR, EQL, LSS, etc.) instead of symbolic ones.
Compilation Process
When you compile a BLISS program with blissc, the process is:
hello.bli → [blissc/LLVM] → hello.o (object file)
wrapper.c → [gcc] → wrapper.o (object file)
→ hello (linked executable)
The blissc compiler uses LLVM as its code generation backend, producing optimized native code for x86 systems.
A Bit of History
BLISS was created in 1969, three years after BCPL — the common ancestor of both BLISS and C — was designed, and three years before C appeared at Bell Labs in 1972. Both languages aimed to replace assembly for systems programming, and both drew inspiration from BCPL. While C went on to become ubiquitous through Unix, BLISS became the backbone of DEC’s VAX and OpenVMS ecosystem, where it remains in active use through VMS Software, Inc.’s (VSI) ongoing compiler releases.
The original BLISS compiler at CMU was renowned for its optimization techniques. The book The Design of an Optimizing Compiler (1975), written by Wulf and colleagues based on this work, influenced a generation of compiler designers.
Key Takeaways
- BLISS modules are wrapped in
MODULE name = BEGIN ... END ELUDOM - No built-in I/O - use external C functions for output
- The dot operator (
.x) reads a variable’s value; the bare name is its address - blissc uppercases all identifiers - C wrappers must use uppercase names
- Two-step compilation - blissc produces object files, gcc links them
- Keyword operators like
GTR,EQL,LSSreplace symbolic comparisons - Everything is an expression - even
IFandCASEproduce values
Running Today
All examples can be run using Docker:
docker pull codearchaeology/bliss:latest