Skip to main content

Week 6: DEFINE and the shape of a call

User-defined functions, the save/restore discipline behind them, and recursion that needs no special-casing at all

29 March 2027

Outline

DEFINE is a function call, not syntax

Nothing in SNOBOL4’s grammar is dedicated to declaring a function. DEFINE is an ordinary primitive function, called like any other:

DEFINE('F(X,Y)L1,L2', 'FENTRY')

declares a function named F with formal arguments X and Y, local variables L1 and L2, and a body reached at the statement labelled FENTRY. Both the locals list and the entry-point label are optional: omit the entry point and the function’s own name doubles as its entry label; omit locals and that part of the prototype string is simply blank. DEFINE itself succeeds by returning the null string, and it has to actually execute — typically once, near the top of the program — before anything can call F. Functions can even be redefined later at runtime, by calling DEFINE again with a different entry point, which is a natural consequence of function declaration being an ordinary statement rather than a compile-time fact about the program.

The function body itself is ordinary SNOBOL4 code, reached only by a call (falling into it via normal control flow is not something the language provides for), which returns by transferring to one of three system labels rather than a return keyword:

Save, call, restore

Week 3 called this “not lexical scoping, but an explicit save/restore discipline” and left the details for later. Here are the details. Around every call, the interpreter saves the current values of the function-name variable, all formal arguments, and all declared locals — in that order — then binds the formals to the argument values the call supplied (too few arguments pad with the null string; extra arguments are evaluated, for any side effects, and discarded), then transfers to the entry label. On any of the three returns, those saved values are restored in reverse order before control returns to the caller. This is described in the manual in essentially the terms a compiler course already uses for activation records — save on entry, restore on exit — which makes DEFINE a ready-made bridge to that vocabulary for students who haven’t met an explicit call stack before.

The reason this gives recursion “for free,” with no special case anywhere in the runtime, follows directly: a recursive call is just another call, and another call is just another save frame pushed on top of whatever’s already there. Nothing distinguishes the outermost call to F from a call to F that happens to occur while a previous call to F is still executing — the save/restore discipline doesn’t know or care how deep it is. A tree-walking interpreter that implements save/call/restore correctly once has therefore already implemented recursion, without writing a line of code that mentions it.

Where this leaves Movement II

Put together, weeks 5 and 6 replace every control-flow and procedural construct a mainstream language would need separate syntax for — branches, loops, function calls, and recursion — with two ideas: every statement succeeds or fails, and the goto field is the only thing that reads that signal. A DEFINEd function call is, from its caller’s perspective, one more statement that can succeed or fail, dispatched into and out of by machinery this course already has by the end of week 5. Movement III then spends three weeks on the part of the language that actually needed new machinery: the pattern sublanguage itself.

Further reading

Teaching team