Skip to main content

Week 2: Blanks that mean something

SNOBOL4's blank-sensitive lexing and its one statement grammar — why whitespace changes the parse, not just the layout

1 March 2027

Outline

One statement, not three

The manual introduces assignment, pattern matching, and replacement as if they were different kinds of statement. They aren’t. Every SNOBOL4 statement has the same five optional fields, in the same fixed order:

label   subject   pattern   object   :goto

An assignment is a subject and an object with no pattern (V blank 5, with no = token at all — SNOBOL4 assignment is genuinely two operands separated by blanks). A pattern-match statement is a subject and a pattern with no object. A replacement statement has all three: if the pattern matches somewhere in the subject, the matched substring is replaced by the object’s value. Recognizing this as one grammar production with optional fields, rather than three separate statement forms, is most of what makes SNOBOL4’s statement-level parser small — a recursive-descent function of maybe a hundred and fifty lines, not six weeks of grammar.

No if, no while, no for

The load-bearing design fact for the rest of the front end: SNOBOL4 has no conditional or loop keywords. The only branching primitive is the goto field — :S(label), :F(label), :S(label)F(label)), or an unconditional :(label) — dispatched on whether the statement as a whole succeeded or failed. Not just a pattern match: an INPUT read that hits end of file can fail, an array reference outside its bounds can fail, a user-defined function can fail by returning through a system label. A loop is just a labeled statement with a failure-exit and a success-goto back to itself:

LOOP    OUTPUT = INPUT      :S(LOOP)

reads records and echoes them until INPUT fails, then falls through. There is no boolean type used for branching, and predicate functions like LT don’t return a boolean you store — the fact of success or failure is the entire signal, observable only through a goto field. Once this lands, a lot of what looks like SNOBOL4 idiosyncrasy — predicates returning the null string, array-bounds-as-loop-exit — turns out to be one idea applied consistently rather than a pile of special cases.

Blanks are part of the grammar

This is the fact most lexers get wrong on a first attempt, because it breaks the usual assumption that whitespace is only ever a token separator:

The same character is unary-or-a-concatenation-boundary depending purely on the whitespace next to it, not on any preceding token:

This is a genuinely context-sensitive-to-whitespace grammar, and it is worth sitting with before writing any lexer code: try hand-tokenizing X = 2 ** 3 ** 2, A -B, A - B, and (TENS UNITS) 30 yourself, on paper, before the session builds the rule into an actual lexer. Almost nobody guesses this rule from a regex-shaped mental model of tokenizing on the first try, which is exactly why it is worth an hour now rather than a debugging session in three weeks.

Further reading

Teaching team