3 Worked Example: Triangular Numbers
The file "pit/triangular.i" is a small but non-trivial worked example. It prints the first ten triangular numbers:
I |
III |
VI |
X |
XV |
XXI |
XXVIII |
XXXVI |
XLV |
LV |
The point of this example is not merely to show a finished INTERCAL program. The more useful lesson is how to translate a familiar Racket program into an INTERCAL-shaped state machine.
3.1 Start with an ordinary Racket function
The direct Racket version is the sort of code most readers would naturally write first:
(define (triangular-list n) (let loop ([k n] [sum 0] [inc 1] [acc '()]) (if (zero? k) (reverse acc) (define next-sum (+ sum inc)) (loop (sub1 k) next-sum (add1 inc) (cons next-sum acc)))))
This is clear, but it hides the control flow inside function calls and local bindings. INTERCAL does not encourage that style. It wants:
named storage locations,
explicit updates to those locations, and
control flow that moves between labels.
So the first translation step is not “write INTERCAL syntax.” The first step is “rewrite the Racket into an explicit state machine.”
3.2 Rewrite the Racket into effectful stateful code
The same computation can be written in a style that is much closer to INTERCAL:
(define (emit-triangular! n) (define count n) (define sum 0) (define inc 1) (let loop () (unless (zero? count) (set! sum (+ sum inc)) (displayln sum) (set! inc (+ inc 1)) (set! count (- count 1)) (loop))))
This version already exposes most of the structure that will appear in the INTERCAL program:
count, sum, and inc are long-lived pieces of state.
Each iteration performs a fixed sequence of updates.
The loop condition is an explicit check that decides whether to jump back to the body.
Once you can write this version cleanly, the move to INTERCAL is mostly a question of replacing Racket primitives with INTERCAL mechanisms.
3.3 Separate helpers from the loop body
There is one more Racket rewrite that makes the INTERCAL correspondence even clearer: factor arithmetic into helper procedures. That mirrors the way INTERCAL programs call into "syslib.i".
(define (add2 a b) (+ a b)) (define (sub1* a) (- a 1)) (define (emit-triangular!/helpers n) (define count n) (define sum 0) (define inc 1) (let loop () (unless (zero? count) (set! sum (add2 sum inc)) (displayln sum) (set! inc (add2 inc 1)) (set! count (sub1* count)) (loop))))
This matters because the INTERCAL translation will not inline addition or subtraction directly. Instead, it will call standard library routines through NEXT and receive results through RESUME.
3.4 Map Racket variables to INTERCAL variables
For "pit/triangular.i", the long-lived state is mapped like this:
count becomes .9.
sum becomes .10.
inc becomes .11.
.1, .2, and .3 are scratch registers used by syslib helper calls.
That gives the initialization block:
DO .9 <- #10 |
DO .10 <- #0 |
DO .11 <- #1 |
This is exactly the stateful Racket setup
(define count 10) (define sum 0) (define inc 1)
just written in INTERCAL storage syntax.
3.5 Translate helper calls to NEXT and RESUME
This is the conceptual leap that matters most.
In Racket, a helper call such as
(set! sum (add2 sum inc))
looks like a direct expression. In this INTERCAL implementation, the analogous pattern is:
DO .1 <- .10 |
DO .2 <- .11 |
PLEASE (1009) NEXT |
DO .10 <- .3 |
The correspondence is:
move the arguments into the helper calling convention registers .1 and .2,
NEXT to the helper label, here 1009 for addition,
let the helper compute and eventually RESUME, and
read the returned value from .3.
For a Racket programmer, the clean mental model is:
(define saved-k continuation-after-call) (jump-to add-routine)
That is what NEXT and RESUME are doing operationally. NEXT saves “where to come back to” on the NEXT stack and transfers control to the target label. RESUME pops one or more saved continuations and jumps back.
3.6 Translate one loop iteration
Now the body of one stateful Racket iteration:
(set! sum (add2 sum inc)) (displayln sum) (set! inc (add2 inc 1))
becomes the INTERCAL block at label (1):
(1) DO .1 <- .10 |
DO .2 <- .11 |
PLEASE (1009) NEXT |
DO .10 <- .3 |
PLEASE READ OUT .10 |
DO .1 <- .11 |
DO .2 <- #1 |
PLEASE (1009) NEXT |
DO .11 <- .3 |
The structure is the same as the stateful Racket:
compute the next sum,
emit it,
increment the step size.
The only added complexity is the helper-call protocol through .1, .2, .3, NEXT, and RESUME.
3.7 Translate the loop back-edge
The stateful Racket loop ends with:
(set! count (sub1* count)) (unless (zero? count) (loop))
In INTERCAL, that becomes a separate control block:
(4) DO .1 <- .9 |
DO .2 <- #1 |
PLEASE (1010) NEXT |
DO .9 <- .3 |
DO .1 <- '.9~.9'~#1 |
PLEASE (1020) NEXT |
DO RESUME .1 |
There are two ideas here:
Use syslib 1010 to perform the decrement.
Use syslib 1020 to transform the resulting counter into a RESUME count that either continues the loop or exits it.
This is the part that looks least like idiomatic Racket and most like real INTERCAL. The important thing for a translator is not the exact bit trickery, but the control-flow role:
state update first,
branch decision second,
and the final choice expressed as a stack-sensitive RESUME.
3.8 Assemble the final program
The complete source is:
#lang reader "../intercal.rkt" |
DO .9 <- #10 |
DO .10 <- #0 |
DO .11 <- #1 |
|
(1) DO .1 <- .10 |
DO .2 <- .11 |
PLEASE (1009) NEXT |
DO .10 <- .3 |
PLEASE READ OUT .10 |
DO .1 <- .11 |
DO .2 <- #1 |
PLEASE (1009) NEXT |
DO .11 <- .3 |
|
DO (3) NEXT |
DO (1) NEXT |
|
(3) DO (4) NEXT |
PLEASE GIVE UP |
|
(4) DO .1 <- .9 |
DO .2 <- #1 |
PLEASE (1010) NEXT |
DO .9 <- .3 |
DO .1 <- '.9~.9'~#1 |
PLEASE (1020) NEXT |
DO RESUME .1 |
3.9 How the labels correspond to the Racket structure
This program uses a small set of registers and labels:
.9 is the loop counter.
.10 is the running triangular sum.
.11 is the increment that gets added each iteration.
.1, .2, and .3 are the syslib call registers.
Label (1) is the loop body.
Label (3) is the exit trampoline.
Label (4) is the loop controller.
This is a common pattern. Keep your long-lived state in higher-numbered variables and reserve .1/.2/.3 for helper calls.
3.10 A translation recipe
For a Racket programmer, the practical recipe is:
Write the computation first in direct Racket.
Rewrite it into explicit stateful Racket with mutable variables and a visible loop.
Identify helper operations that should become syslib calls.
Assign stable INTERCAL variables to the long-lived state.
Translate each helper call into:
argument moves, then NEXT, then a read from the return register.
Move loop-control decisions into their own labeled controller block.
Add I/O only after the control flow is already correct.
That process is much more reliable than trying to translate directly from high-level functional Racket to INTERCAL syntax in one pass.