Beginner

Control Flow in SNOBOL

Learn how SNOBOL drives program flow with success/failure, conditional gotos, comparison predicates, and pattern-matching loops

Most languages give you if, else, for, and while as dedicated keywords. SNOBOL has none of them. Instead, control flow grows out of a single idea that runs through the entire language: every statement either succeeds or fails, and that result decides where execution goes next.

Coming from the SNOBOL4 lineage at Bell Labs, this is a pattern-directed, imperative model. A statement runs, the interpreter notes whether it succeeded or failed, and an optional goto field at the end of the line — written :S(LABEL) for success, :F(LABEL) for failure, or :(LABEL) unconditionally — sends execution to a labeled statement. There are no block structures and no indentation rules to obey; flow is a graph of labels connected by gotos.

The things that “fail” are richer than a boolean test. A numeric comparison like GT(N, 0) fails when it isn’t true. A pattern match fails when the pattern isn’t found. An input read fails at end of file. Because all of these share the same success/failure signal, the same :S/:F machinery handles conditionals, loops, multi-way branches, and string scanning alike.

In this tutorial you’ll build conditionals from comparison predicates, write loops with nothing but a label and a goto, drive a loop with the success or failure of a pattern match, and use SNOBOL’s idiom for conditional (“ternary”) assignment.

Conditionals: Predicates and Gotos

SNOBOL has no if keyword. Instead it offers predicate functions that succeed or fail. For numbers there are EQ, NE, LT, LE, GT, and GE. For strings — and this surprises newcomers — you do not use = (that’s assignment). You use IDENT (identical) and DIFFER (different). A predicate returns the null string on success and fails otherwise, so you wire it straight into a goto field.

Create a file named control_flow_if.sno:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
*       Conditionals in SNOBOL using comparison predicates
*       Numeric comparison: classify a number
        N = 7
        GT(N, 0)                                  :S(POS)
        EQ(N, 0)                                  :S(ZERO)F(NEG)
POS     OUTPUT = N " is positive"                 :(STR)
ZERO    OUTPUT = N " is zero"                     :(STR)
NEG     OUTPUT = N " is negative"
*       String comparison uses IDENT / DIFFER, never =
STR     NAME = "SNOBOL"
        IDENT(NAME, "SNOBOL")                     :S(SAME)F(DIFF)
SAME    OUTPUT = "Name matches"                   :(END)
DIFF    OUTPUT = "Name differs"
END

GT(N, 0) succeeds for N = 7, so :S(POS) jumps to the POS label. After printing, :(STR) jumps unconditionally past the other branches — SNOBOL has no automatic “end of if”, so you must route around the branches you didn’t take. The IDENT test then compares two strings the SNOBOL way.

Loops: A Label and a Goto

A loop is just a statement that jumps back to its own label. There is no while or for — you repeat by gotoing backward, and you exit when a predicate fails. This file shows two classic shapes: a countdown that loops while a condition holds, and a counter that accumulates a running total.

Create a file named control_flow_loops.sno:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
*       Two loops driven by the success of a comparison
*       Loop 1: countdown from 5, repeat while N > 0
        N = 5
DOWN    OUTPUT = N
        N = N - 1
        GT(N, 0)                                  :S(DOWN)
        OUTPUT = "Liftoff!"
*       Loop 2: accumulate a sum from 1 to 10
        SUM = 0
        I = 0
UP      I = I + 1
        SUM = SUM + I
        LE(I, 9)                                  :S(UP)
        OUTPUT = "Sum 1..10 = " SUM
END

In the countdown, GT(N, 0) keeps succeeding — and looping back to DOWN — until N reaches 0, at which point it fails, execution falls through, and “Liftoff!” prints. The summation loop runs UP while LE(I, 9) holds, so it processes I from 1 through 10 and stops once I becomes 10.

Multi-Way Branching: FizzBuzz

When you need several mutually exclusive cases — the equivalent of switch/case — you chain predicate tests, each with a success goto to its own labeled action. Combined with a backward loop, this gives a compact FizzBuzz. The REMDR(a, b) built-in returns the remainder of a divided by b.

Create a file named control_flow_fizzbuzz.sno:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
*       FizzBuzz from 1 to 15 - looping plus multi-way branching
        N = 0
LOOP    N = N + 1
        GT(N, 15)                                 :S(END)
        EQ(REMDR(N, 15), 0)                       :S(FIZZBUZZ)
        EQ(REMDR(N, 3), 0)                        :S(FIZZ)
        EQ(REMDR(N, 5), 0)                        :S(BUZZ)
        OUTPUT = N                                :(LOOP)
FIZZBUZZ OUTPUT = "FizzBuzz"                       :(LOOP)
FIZZ    OUTPUT = "Fizz"                            :(LOOP)
BUZZ    OUTPUT = "Buzz"                            :(LOOP)
END

Each iteration tests the divisibility cases in order, jumping to the first one that succeeds. The :(LOOP) after every action sends control back to the top, and GT(N, 15) ends the program by jumping to END once the count is exhausted. Note that the most specific case (REMDR(N, 15)) is tested first — order matters because the first success wins.

Pattern Matching as Control Flow

This is where SNOBOL diverges most sharply from other languages: a pattern match is a control-flow test. The match succeeds or fails just like a comparison, so you loop by repeatedly matching and use :F to detect when there’s nothing left to match. Here we peel words off a sentence one at a time. BREAK(" ") matches everything up to the next space, the . W operator captures that matched text into W, and assigning null (= with nothing after it) deletes the matched portion from the subject.

Create a file named control_flow_patterns.sno:

1
2
3
4
5
6
7
8
*       Pattern-match success/failure driving a loop
*       Split a sentence into words by consuming it
        TEXT = "the quick brown fox"
        WORD = BREAK(" ") . W
NEXT    TEXT WORD " " =                           :F(LAST)
        OUTPUT = W                                :(NEXT)
LAST    OUTPUT = TEXT
END

Each pass through NEXT finds a word followed by a space, prints the captured W, and shaves it off TEXT. When only "fox" remains there’s no trailing space, so BREAK(" ") fails, the match fails, and :F(LAST) jumps out to print the final leftover word. The loop’s termination is the failure of a pattern match — pure SNOBOL.

Conditional Expressions: SNOBOL’s “Ternary”

SNOBOL has no ? : ternary operator, but it doesn’t need one. Because a failed statement performs no assignment, you can guard an assignment with a predicate: if the predicate fails, the variable keeps its previous value. Concatenating a predicate’s null result with a string yields a conditional value.

Create a file named control_flow_ternary.sno:

1
2
3
4
5
6
7
8
9
*       Conditional assignment - SNOBOL's take on the ternary
*       A predicate returns null on success and fails on failure,
*       and a failed statement leaves the target unchanged.
        N = -4
        LABEL = "zero"
        LABEL = GT(N, 0) "positive"
        LABEL = LT(N, 0) "negative"
        OUTPUT = N " is " LABEL
END

LABEL starts as "zero". The line LABEL = GT(N, 0) "positive" fails because GT(-4, 0) fails, so the assignment never happens and LABEL is untouched. The next line succeeds — LT(-4, 0) returns null, null "negative" is just "negative" — so LABEL becomes "negative". This guarded-assignment pattern is how SNOBOL programmers express “set this value only when a condition holds.”

Running with Docker

1
2
3
4
5
6
7
8
9
# Pull the official image
docker pull esolang/snobol:latest

# Run each control flow example
docker run --rm -v $(pwd):/app -w /app esolang/snobol:latest snobol control_flow_if.sno
docker run --rm -v $(pwd):/app -w /app esolang/snobol:latest snobol control_flow_loops.sno
docker run --rm -v $(pwd):/app -w /app esolang/snobol:latest snobol control_flow_fizzbuzz.sno
docker run --rm -v $(pwd):/app -w /app esolang/snobol:latest snobol control_flow_patterns.sno
docker run --rm -v $(pwd):/app -w /app esolang/snobol:latest snobol control_flow_ternary.sno

Expected Output

control_flow_if.sno:

7 is positive
Name matches

control_flow_loops.sno:

5
4
3
2
1
Liftoff!
Sum 1..10 = 55

control_flow_fizzbuzz.sno:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

control_flow_patterns.sno:

the
quick
brown
fox

control_flow_ternary.sno:

-4 is negative

Key Concepts

  • Success and failure are the engine — every statement reports one or the other, and that single signal powers conditionals, loops, branching, and string scanning alike.
  • Gotos are the syntax of control flow:S(LABEL) on success, :F(LABEL) on failure, and :(LABEL) unconditionally; there are no blocks, so you route explicitly around branches you don’t take.
  • Predicates replace ifEQ, NE, LT, LE, GT, GE test numbers and succeed or fail. Compare strings with IDENT and DIFFER, never with = (which is assignment).
  • Loops are backward gotos — repeat by jumping to a label and exit when a predicate fails; there is no dedicated for or while.
  • Multi-way branching is a chain of tests — list predicate-plus-:S lines in priority order; the first one to succeed wins, so order the most specific case first.
  • Pattern matches are control-flow tests — a match that fails (:F) is the idiomatic way to end a string-processing loop, the language’s signature technique.
  • A failed statement assigns nothing — guarding an assignment with a predicate gives you conditional (“ternary”) assignment without any special operator.
  • Labels live in column 1; statements are indented — every labeled action and every goto target hangs off a name in the first column, while ordinary statements start with whitespace.

Running Today

All examples can be run using Docker:

docker pull esolang/snobol:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining