Control Flow in BLISS
Learn conditionals, loops, and case selection in BLISS - the structured, goto-free, expression-oriented control flow that defined a generation of systems software
Control flow is where BLISS makes one of its boldest statements: it has no goto. Created in 1969, BLISS was one of the earliest languages to deliberately omit unconditional jumps, relying entirely on structured constructs like IF-THEN-ELSE, WHILE, INCR/DECR, and CASE. As a structured, imperative systems language, BLISS proves you can write operating systems and compilers without ever jumping to a label.
The second thing that sets BLISS apart is that control flow constructs are expressions - they produce values. An IF is BLISS’s “ternary,” a CASE yields the value of whichever branch ran, and even a loop returns a value. This means control flow can appear on the right-hand side of an assignment, nested inside other expressions, or passed straight to a routine.
Remember two things carried over from earlier tutorials: variable names evaluate to addresses, so you read a value with the dot operator (.x), and comparisons use keyword operators (EQL, NEQ, GTR, LSS, GEQ, LEQ) rather than symbols. Both show up constantly in conditions.
This tutorial covers conditionals, counted and conditional loops, multi-way CASE selection, and loop control - all without a single goto.
Conditionals: IF-THEN-ELSE
BLISS’s IF works as both a statement and an expression. As a statement, it chooses which branch to execute. As an expression, it returns the value of the chosen branch - this is BLISS’s equivalent of the ternary operator found in other languages.
Create a file named conditionals.bli:
MODULE conditionals =
BEGIN
EXTERNAL ROUTINE
print_int;
GLOBAL ROUTINE cond_demo : NOVALUE =
BEGIN
LOCAL n, abs_val;
n = -7;
! IF used as a statement to choose a branch
IF .n LSS 0
THEN print_int(0) ! negative -> print 0
ELSE print_int(1); ! otherwise -> print 1
! IF used as an expression (BLISS's "ternary")
abs_val = (IF .n LSS 0 THEN -.n ELSE .n);
print_int(.abs_val); ! 7
! An IF with no ELSE simply does nothing when false
IF .n GTR 0 THEN print_int(99)
END;
END
ELUDOM
Because every construct is an expression, abs_val = (IF .n LSS 0 THEN -.n ELSE .n) reads almost like math: assign whichever branch matches. The parentheses make the expression boundary explicit, which is good practice in BLISS. A condition is “true” when its value is non-zero (odd, technically - BLISS tests the low bit), and the keyword comparisons return 1 for true and 0 for false.
The C wrapper provides the print_int bridge and the main() entry point. As with the Hello World tutorial, blissc uppercases every identifier, so the C function names must be uppercase.
Create a file named wrapper.c:
| |
Loops: WHILE, INCR, and DECR
BLISS offers two families of loops. Tested loops (WHILE/UNTIL) repeat based on a condition; indexed loops (INCR/DECR) count through a range. The indexed loops automatically declare their loop variable, which you read - like any value - with the dot operator.
Create a file named loops.bli:
MODULE loops =
BEGIN
EXTERNAL ROUTINE
print_int;
GLOBAL ROUTINE loops_demo : NOVALUE =
BEGIN
LOCAL count, sum;
! WHILE loop: repeat while the condition holds
count = 3;
WHILE .count GTR 0 DO
BEGIN
print_int(.count); ! 3, 2, 1
count = .count - 1
END;
! INCR: counted loop from 1 to 5, index 'i' is implicit
sum = 0;
INCR i FROM 1 TO 5 DO
sum = .sum + .i;
print_int(.sum); ! 15
! DECR: counts downward from 3 to 1
DECR j FROM 3 TO 1 DO
print_int(.j) ! 3, 2, 1
END;
END
ELUDOM
The INCR i FROM 1 TO 5 DO loop declares i for you and steps it from 1 to 5; you never assign to it directly. An optional BY clause sets the step (e.g., INCR i FROM 0 TO 10 BY 2). DECR is identical but counts downward. For tested loops, BLISS also offers the post-test forms DO ... WHILE and DO ... UNTIL, which always run the body at least once.
Multi-Way Selection: CASE
When you need to branch many ways on an integer value, CASE is BLISS’s switch. You declare the range of the selector with FROM ... TO, and each label in the SET ... TES block handles one value. Like everything else, CASE is an expression that yields the value of the branch that ran.
Create a file named case_flow.bli:
MODULE case_flow =
BEGIN
EXTERNAL ROUTINE
print_int;
GLOBAL ROUTINE case_demo : NOVALUE =
BEGIN
! Walk a selector from 0 to 2 and dispatch on each value
INCR code FROM 0 TO 2 DO
CASE .code FROM 0 TO 2 OF
SET
[0]: print_int(100);
[1]: print_int(200);
[2]: print_int(300);
TES
END;
END
ELUDOM
The SET ... TES brackets the branches (TES is SET reversed, in the same playful spirit as ELUDOM). Each [value]: label maps a selector value to an expression. BLISS also supports range labels like [2 TO 5]:, the special label [INRANGE]: for any in-range value not listed, and [OUTRANGE]: for selectors outside the declared FROM ... TO bounds - handy as a default case. For non-contiguous or boolean-style tests, the related SELECT construct evaluates each test in turn.
Loop Control: EXITLOOP
Because loops are expressions, BLISS controls them by leaving them with a value rather than with a bare break. EXITLOOP terminates the innermost loop, and the value you supply becomes the value of the whole loop expression. If the loop finishes normally instead, it yields -1. This turns a search loop into a clean expression.
Create a file named loop_control.bli:
MODULE loop_control =
BEGIN
EXTERNAL ROUTINE
print_int;
GLOBAL ROUTINE search_demo : NOVALUE =
BEGIN
LOCAL result;
! Find the first i whose square exceeds 50; the loop
! itself yields the value handed to EXITLOOP.
result = INCR i FROM 1 TO 100 DO
IF .i * .i GTR 50
THEN EXITLOOP .i; ! leave the loop, yielding i
print_int(.result) ! 8 (because 8*8 = 64 > 50)
END;
END
ELUDOM
Here the entire INCR ... DO ... loop is assigned to result. As soon as i*i exceeds 50 (at i = 8, since 7*7 = 49), EXITLOOP .i ends the loop and makes 8 its value. There is no goto, no flag variable, and no separate “found” bookkeeping - the loop is the search. The LEAVE keyword does the same for named blocks, allowing you to exit an outer block from deep inside nested code.
Running with Docker
Each example pairs a .bli file with a C wrapper. The commands below compile and run the conditionals example; swap the filenames and the routine name in wrapper.c to run the others.
| |
Expected Output
Running the conditionals example produces:
0
7
The first line comes from the IF statement (n is negative, so it prints 0), and the second from the IF expression that computes the absolute value (7). The final IF has no ELSE and its condition is false, so it prints nothing.
Key Concepts
- No
goto- BLISS relies entirely on structured constructs (IF,WHILE,INCR/DECR,CASE,SELECT); unconditional jumps were deliberately left out in 1969. - Control flow is expressions -
IF,CASE, and even loops return values, so they can sit on the right of an assignment or nest inside other expressions. IFis the ternary -(IF cond THEN a ELSE b)both branches and produces a value; anIFwith noELSEyields nothing useful when false.- Two loop families - tested loops (
WHILE/UNTIL, with optional post-testDO ... WHILE) and indexed loops (INCR/DECR, with an optionalBYstep). - Indexed loops declare their own variable - the index in
INCR i FROM ... TO ...is implicit and read with the dot operator (.i). CASEis the switch -CASE x FROM lo TO hi OF SET [labels]: expr; TES, with[INRANGE]and[OUTRANGE]for defaults.EXITLOOPleaves with a value - loop control returns a value from the loop expression instead of just breaking, making search loops concise.- Conditions use keyword operators -
EQL,NEQ,GTR,LSS,GEQ,LEQtest machine words, and any non-zero (odd) result counts as true.
Running Today
All examples can be run using Docker:
docker pull codearchaeology/bliss:latest
Comments
Loading comments...
Leave a Comment