Beginner

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>

void COND_DEMO(void);

void PRINT_INT(int n) {
    printf("%d\n", n);
}

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

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.

1
2
3
4
5
# Pull the BLISS compiler image
docker pull codearchaeology/bliss:latest

# Compile and run (assumes conditionals.bli and wrapper.c are present)
docker run --rm -v $(pwd):/app -w /app codearchaeology/bliss:latest sh -c 'blissc -o conditionals.o conditionals.bli && gcc -o conditionals wrapper.c conditionals.o && ./conditionals'

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.
  • IF is the ternary - (IF cond THEN a ELSE b) both branches and produces a value; an IF with no ELSE yields nothing useful when false.
  • Two loop families - tested loops (WHILE/UNTIL, with optional post-test DO ... WHILE) and indexed loops (INCR/DECR, with an optional BY step).
  • Indexed loops declare their own variable - the index in INCR i FROM ... TO ... is implicit and read with the dot operator (.i).
  • CASE is the switch - CASE x FROM lo TO hi OF SET [labels]: expr; TES, with [INRANGE] and [OUTRANGE] for defaults.
  • EXITLOOP leaves 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, LEQ test 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
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining