(lisp 1958)
&
FORTH 1970
Language design · Radical ideas

Two languages that
broke the rules

Lisp (1958) and Forth (1970) share almost nothing syntactically. But both arose from the same conviction: that programming languages are not given facts, but material to be shaped. Both put the programmer in charge of the language itself — and both ideas permeate every serious language that followed.

Lisp
John McCarthy · MIT · 1958
• Code is data (homoiconicity)
• The metacircular evaluator (eval)
• Higher-order functions & closures
• Macros: programs that write programs
• Garbage collection
• The REPL
Forth
Charles Moore · NRAO · 1970
• Stack-based: no registers, no names
• The dictionary: language you extend
• Concatenative programming
• No syntax — words all the way down
• Early systems could be tiny
• The meta-compiler (Forth in Forth)
§01

The shared radical stance

both

Most programming languages place the programmer inside the language — constrained by its grammar, its data model, its operator precedence, its permitted abstractions. You can write new functions; you cannot write new syntax. You can call the compiler; you cannot change it. The language is a given.

Lisp and Forth both refuse this premise. In many Lisp systems, large parts of the compiler and runtime live in the language itself and can be inspected or replaced from the running environment. In Forth, the compiler is often little more than a small interpreter plus a dictionary builder; adding a new word to the dictionary is extending the language. Both traditions treat the language itself as the primary material.

McCarthy in 1960: "Lisp is not just another programming language. It is an attempt to show that programming languages can be based on mathematics." Moore in 1970: "I designed Forth so I could write programs while learning the machine — the language and the application grew together." Both were building languages you could grow.
Lisp's radicalism
Forth's radicalism
Language based on a uniform data structure (the list)
Language with no data structure at all — just the stack
Syntax IS the AST — no parser/AST distinction
No syntax — whitespace-delimited words, period
Extend via macros that run at compile time
Extend via word definitions that run at definition time
Garbage collection: free the programmer from memory
Manual: early systems were tiny and ran close to bare hardware
§02

Homoiconicity: code is data

lisp

The most radical thing about Lisp is not its parentheses — it is what those parentheses mean. A Lisp program is not text that gets parsed into a tree. A Lisp program already is a tree, written out as a list of lists. The notation is the data structure.

This property — that code and data share the same representation — is called homoiconicity. Its consequence: any Lisp program can trivially manipulate any other Lisp program, because both are just lists. The compiler is not special. eval is not magic. They're functions that operate on lists.

S-expression = list = AST — click any sub-expression
homoiconicity.js — code as data in JavaScript
// In Lisp, code IS a list. We simulate this in JavaScript.
// A Lisp s-expression like (+ (* 2 3) (- 10 4)) is literally:
// ['+', ['*', 2, 3], ['-', 10, 4]] — a nested array

const program = ['+', ['*', 2, 3], ['-', 10, 4]]; // (+ (* 2 3) (- 10 4))

// eval: a function that evaluates a list-as-code — no special status
function lispEval(expr, env = {}) {
  if (typeof expr === 'number') return expr;            // number self-evaluates
  if (typeof expr === 'string') return env[expr] ?? expr; // symbol lookup
  if (!Array.isArray(expr)) return expr;

  const [op, ...args] = expr;
  if (op === 'quote') return args[0];  // (quote x) → x unevaluated
  if (op === 'if') return lispEval(args[0], env) ? lispEval(args[1], env) : lispEval(args[2], env);
  if (op === 'lambda') {
    const [params, body] = args;
    return (...vals) => lispEval(body, {...env, ...Object.fromEntries(params.map((p,i)=>[p,vals[i]]))});
  }
  const fn = {'+':(a,b)=>a+b, '-':(a,b)=>a-b, '*':(a,b)=>a*b,
               'car':a=>a[0], 'cdr':a=>a.slice(1),
               'cons':(h,t)=>[h,...t], 'null?':a=>a.length===0}[op];
  if (!fn) throw new Error(`Unknown operator: ${op}`);
  return fn(...args.map(a => lispEval(a, env)));
}

// The KEY insight: the program IS data. We can inspect and transform it.
console.log("Program as data:", JSON.stringify(program));
console.log("Operator:", program[0]);
console.log("First arg:", JSON.stringify(program[1]));  // (* 2 3)
console.log("Evaluated:", lispEval(program));

// Transform the program — e.g. replace all operators
function transformOp(expr, from, to) {
  if (!Array.isArray(expr)) return expr;
  return [expr[0] === from ? to : expr[0], ...expr.slice(1).map(e => transformOp(e, from, to))];
}
const modified = transformOp(program, '*', '+');
console.log("After replacing * with +:", JSON.stringify(modified));
console.log("Evaluated modified program:", lispEval(modified));

// (quote x) prevents evaluation — returns the list itself as data
const quoted = ['quote', ['+', 1, 2]];
console.log("\n(quote (+ 1 2)) →", JSON.stringify(lispEval(quoted)));
console.log("Not 3 — the list itself. This is how macros receive their arguments.");
§03

eval — the metacircular evaluator

lisp

In 1960, Steve Russell noticed something remarkable: McCarthy's paper on Lisp contained an evaluator precise enough to serve as an interpreter. Russell hand-coded that evaluator in machine code, turning the definition into a running system. The important historical point is not that the interpreter literally ran in Lisp first, but that Lisp's definition was already close to executable.

This style of evaluator — eval and apply calling each other recursively — is what Alan Kay called "the Maxwell's equations of software." In a small amount of code you can capture a large fraction of a language's semantics. The demo below is a teaching model in JavaScript, not a literal metacircular Lisp evaluator, but it preserves the core eval/apply idea.

eval answers the question: "what does this expression mean in the current environment?" If the expression is a number, it returns the number. If it is a symbol like x, it looks up x in the environment. If it is a special form like if, quote, or define, it applies the language's special evaluation rules for that form. If it is an ordinary list like (square 7), eval first evaluates the operator square, then evaluates each argument, and hands the results to apply.

apply answers a different question: "now that I already have a function value and a list of argument values, how do I run the call?" For a built-in like +, apply just executes the primitive operation. For a user-defined function, it makes a new environment that binds the parameter names to the argument values, then calls eval on the function body inside that new environment. So for (square 7): eval turns square into a closure and 7 into a value; apply binds x = 7; then eval runs the body (* x x). That back-and-forth is the whole interpreter loop.

The evaluator made Lisp unusually self-describing. If you understand eval and apply, you understand much of the language's core semantics. In Lisp culture, the boundary between language definition and implementation became far thinner than in most contemporaries.
metacircular.js — eval/apply: the Maxwell's equations of software
// A teaching evaluator: Lisp's eval/apply structure shown in JavaScript
// Written in plain JS so the page runner can execute it in older browsers too.

function makeEnv(pairs, outer) {
  if (pairs === undefined) pairs = [];
  if (outer === undefined) outer = null;
  var bindings = {};
  for (var i = 0; i < pairs.length; i++) bindings[pairs[i][0]] = pairs[i][1];
  return {
    lookup: function(name) {
      if (name in bindings) return bindings[name];
      if (outer) return outer.lookup(name);
      throw new Error('Unbound symbol: ' + name);
    },
    bind: function(name, val) { bindings[name] = val; }
  };
}

function lisp_eval(expr, env) {
  // Atoms (self-evaluating)
  if (typeof expr === 'number' || typeof expr === 'boolean') return expr;
  if (typeof expr === 'string') return env.lookup(expr);
  if (!Array.isArray(expr)) throw new Error('Unknown expr: ' + expr);

  var head = expr[0];
  var tail = expr.slice(1);

  // Special forms — handled directly by eval, not apply
  if (head === 'quote') return tail[0];
  if (head === 'if') return lisp_eval(tail[0], env) ? lisp_eval(tail[1], env) : lisp_eval(tail[2], env);
  if (head === 'define') { env.bind(tail[0], lisp_eval(tail[1], env)); return tail[0]; }
  if (head === 'begin') {
    var result = null;
    for (var j = 0; j < tail.length; j++) result = lisp_eval(tail[j], env);
    return result;
  }

  // Lambda: capture the current environment as a closure
  if (head === 'lambda') {
    var params = tail[0];
    var body = tail[1];
    return { type: 'closure', params: params, body: body, env: env };
  }

  // Function call: eval the operator, then apply it to evaluated args
  var fn = lisp_eval(head, env);
  var args = [];
  for (var k = 0; k < tail.length; k++) args.push(lisp_eval(tail[k], env));
  return lisp_apply(fn, args);
}

function lisp_apply(fn, args) {
  if (typeof fn === 'function') return fn.apply(null, args);
  if (fn && fn.type === 'closure') {
    // Extend the closure's captured environment with the arguments
    var pairs = [];
    for (var i = 0; i < fn.params.length; i++) pairs.push([fn.params[i], args[i]]);
    var newEnv = makeEnv(pairs, fn.env);
    return lisp_eval(fn.body, newEnv);
  }
  throw new Error('Not a function: ' + JSON.stringify(fn));
}

// Global environment with primitives
var globalEnv = makeEnv([
  ['+', function(a, b) { return a + b; }],
  ['-', function(a, b) { return a - b; }],
  ['*', function(a, b) { return a * b; }],
  ['=', function(a, b) { return a === b; }],
  ['<', function(a, b) { return a < b; }],
  ['car', function(a) { return a[0]; }],
  ['cdr', function(a) { return a.slice(1); }],
  ['cons', function(h, t) { return [h].concat(t); }],
  ['null?', function(a) { return a.length === 0; }],
  ['list', function() { return Array.prototype.slice.call(arguments); }]
]);

function E(expr) { return lisp_eval(expr, globalEnv); }

// Test the evaluator
E(['define', 'square', ['lambda', ['x'], ['*', 'x', 'x']]]);
console.log("(square 7)       =", E(['square', 7]));

// Factorial — recursive lambda
E(['define', 'fact', ['lambda', ['n'],
  ['if', ['=', 'n', 0], 1, ['*', 'n', ['fact', ['-', 'n', 1]]]]]]);
console.log("(fact 10)        =", E(['fact', 10]));

// Higher-order: map
E(['define', 'mymap', ['lambda', ['f', 'lst'],
  ['if', ['null?', 'lst'], ['quote', []],
         ['cons', ['f', ['car', 'lst']], ['mymap', 'f', ['cdr', 'lst']]]]]]);
console.log("(map square '(1 2 3 4 5)) =", E(['mymap', 'square', ['quote', [1,2,3,4,5]]]));

console.log("\nThe entire evaluator is ~30 lines.");
console.log("eval and apply call each other — that mutual recursion IS the language.");
§04

Closures and the lambda revolution

lisp

Lisp put higher-order functions at the center very early, and the path from early Lisp's FUNARG problems to Scheme's explicitly lexical scope led directly to the modern notion of a closure. A closure is a function that carries its defining environment with it. When the function is called later, in a different context, it still has access to the variables it saw when it was created.

The FUNARG problem appears as soon as functions become values. Suppose a function returns another function that mentions a local variable: conceptually, make-adder creates (lambda (y) (+ x y)) while x is 10, then returns it. Later, somewhere else, you call that returned function. What should x mean now? The original x = 10 from the place where the function was created, or some other x visible where it is called?

If the language uses the caller's environment, the function can suddenly see the wrong variable, or no variable at all. That was the historical difficulty McCarthy described: early Lisp often got dynamic scope where lexical scope was wanted. The solution is the closure: when the function is created, it carries along the environment it needs. Then the returned adder still remembers x = 10 even after make-adder has finished. In other words, a closure solves the FUNARG problem by packaging code + remembered bindings together.

This seems simple. The implications are vast. Closures are how you implement: objects (data + behaviour bundled), iterators, callbacks, promises, partial application, modules, continuations. Every one of these modern abstractions is a closure in disguise.

closures.js — everything a closure can be
// Closures: functions that remember their birth environment
// These are modern closure patterns, shown with JavaScript syntax

// === COUNTER: mutable state via closure ===
function makeCounter(initial = 0) {
  let count = initial;   // captured by closure
  return {
    increment: () => ++count,
    decrement: () => --count,
    value:     () => count,
    reset:     () => { count = initial; },
  };
}
const c = makeCounter(10);
c.increment(); c.increment(); c.decrement();
console.log("Counter (init 10, +2, -1):", c.value());

// === OBJECT: closures ARE objects ===
function makePerson(name, age) {
  return {  // no class needed — closure captures name and age
    greet:    () => `Hi, I'm ${name}`,
    birthday: () => { age++; return age; },
    info:     () => `${name}, age ${age}`,
  };
}
const alice = makePerson("Alice", 30);
console.log(alice.greet()); alice.birthday();
console.log(alice.info());

// === PARTIAL APPLICATION: fix some arguments ===
const partial = (f, ...fixed) => (...rest) => f(...fixed, ...rest);
const add     = (a, b) => a + b;
const add5    = partial(add, 5);
const triple  = partial((a,b)=>a*b, 3);
console.log("add5(12)  =", add5(12));
console.log("triple(7) =", triple(7));

// === ITERATOR: lazy sequence via closure ===
function* fibGen() {
  let [a, b] = [0, 1];
  while (true) { yield a; [a, b] = [b, a+b]; }
}
const fibs = fibGen();
const first10 = Array.from({length:10}, () => fibs.next().value);
console.log("First 10 Fibonacci:", first10.join(" "));

// === Y COMBINATOR: recursion without names (pure lambda calculus) ===
const Y = f => (x => f(v => x(x)(v)))(x => f(v => x(x)(v)));
const factY = Y(self => n => n <= 1 ? 1 : n * self(n - 1));
console.log("Y combinator factorial(8) =", factY(8));
console.log("(Lambda calculus has no names — Y lets recursion happen without them)");
§05

Macros — programs that write programs

lisp

Lisp macros are the most powerful metaprogramming mechanism in mainstream use. A macro is a function that receives its arguments unevaluated (as lists) and returns a new list (code) that is then evaluated. The macro runs at compile time, transforming code before it runs.

This means you can add new control structures, new syntax, new binding forms to the language — and they are indistinguishable from built-ins. when, unless, while, defclass, with-open-file — none of these are special forms in Common Lisp. They're macros. The language is grown by its users.

Macro expansion — see the transformation at compile time
macros.js — implementing macros: compile-time code transformation
// Lisp macros: functions on code (lists) that produce new code (lists)
// They run at compile time, transforming s-expressions before evaluation

const macroTable = {};

function defmacro(name, transformer) {
  macroTable[name] = transformer; // register the macro
}

function macroExpand(expr) {
  if (!Array.isArray(expr)) return expr;
  if (macroTable[expr[0]]) {          // is the head a macro?
    const expanded = macroTable[expr[0]](...expr.slice(1)); // call transformer on raw args
    return macroExpand(expanded);      // recursively expand result
  }
  return expr.map(macroExpand);        // expand inside all subforms
}

// === WHEN: (when condition body...) → (if condition (begin body...) nil) ===
defmacro('when', (condition, ...body) =>
  ['if', condition, ['begin', ...body], null]
);

// === AND: short-circuit conjunction ===
defmacro('and', (...forms) => {
  if (forms.length === 0) return true;
  if (forms.length === 1) return forms[0];
  return ['if', forms[0], ['and', ...forms.slice(1)], false];
});

// === LET*: sequential binding ===
// (let* ((x 1) (y (+ x 1))) body) → nested lambdas
defmacro('let*', (bindings, body) => {
  if (bindings.length === 0) return body;
  const [[name, val], ...rest] = bindings;
  return [['lambda', [name], ['let*', rest, body]], val];
});

// === -> THREADING MACRO: pipe values through functions ===
// (-> x f g h) → (h (g (f x)))
defmacro('->', (val, ...fns) =>
  fns.reduce((acc, fn) =>
    Array.isArray(fn) ? [fn[0], acc, ...fn.slice(1)] : [fn, acc],
    val)
);

// Demonstrate macro expansion
const whenExpr = ['when', ['>', 'x', 0], ['print', 'positive']];
console.log("(when (> x 0) (print 'positive'))");
console.log("expands to:", JSON.stringify(macroExpand(whenExpr)));

const threadExpr = ['->', 5, ['*', 2], ['+', 1], ['*', 3]];
console.log("\n(-> 5 (* 2) (+ 1) (* 3)) ← clean pipeline");
console.log("expands to:", JSON.stringify(macroExpand(threadExpr)));
console.log("which reads: (* (+ (* 5 2) 1) 3) = ", (((5*2)+1)*3));

// GENSYM: macros need fresh names to avoid variable capture
let gensymCounter = 0;
const gensym = prefix => `${prefix}__${gensymCounter++}`;

defmacro('swap!', (a, b) => {
  const tmp = gensym('tmp');  // fresh name — can't collide with user vars
  return ['let*', [[tmp, a]], ['begin', ['set!', a, b], ['set!', b, tmp]]];
});
console.log("\n(swap! x y) expands to:", JSON.stringify(macroExpand(['swap!', 'x', 'y'])));
console.log("Fresh gensym avoids capturing user's 'tmp' variable if it exists");
§06

The stack — computation without names

forth

Forth has no variables in the conventional sense. No registers. No named temporaries. All computation happens on a data stack: you push values, apply operations, and the result replaces the operands. There's also a return stack for call management — and that's it. The entire computational model fits on a whiteboard.

Arithmetic in Forth is postfix (Reverse Polish Notation): 2 3 + pushes 2, pushes 3, then + pops both and pushes 5. This is not just notation — it's the actual execution model. No parsing of operator precedence. No expression tree. The stack IS the computation.

Stack visualiser — step through Forth execution
forth_stack.js — a Forth stack machine interpreter
// A Forth stack machine — the entire computational model
// Written in plain JS so the page runner can execute it reliably.

function ForthMachine() {
  this.stack  = [];
  this.rstack = [];
  this.memory = {};
  this.dict   = this.buildDict();
}

ForthMachine.prototype.push = function(v) { this.stack.push(v); };
ForthMachine.prototype.pop = function() {
  if (!this.stack.length) throw new Error("Stack underflow");
  return this.stack.pop();
};
ForthMachine.prototype.peek = function() {
  return this.stack[this.stack.length - 1];
};

ForthMachine.prototype.buildDict = function() {
  var m = this;
  return {
    // Arithmetic — pop two, push result
    '+': function() { m.push(m.pop() + m.pop()); },
    '-': function() { var b = m.pop(), a = m.pop(); m.push(a - b); },
    '*': function() { m.push(m.pop() * m.pop()); },
    '/': function() { var b = m.pop(), a = m.pop(); m.push(Math.floor(a / b)); },
    'MOD': function() { var b = m.pop(), a = m.pop(); m.push(a % b); },
    'NEGATE': function() { m.push(-m.pop()); },
    'ABS': function() { m.push(Math.abs(m.pop())); },
    'MAX': function() { m.push(Math.max(m.pop(), m.pop())); },
    // Stack manipulation — the "no variables" toolkit
    'DUP': function() { m.push(m.peek()); },
    'DROP': function() { m.pop(); },
    'SWAP': function() { var b = m.pop(), a = m.pop(); m.push(b); m.push(a); },
    'OVER': function() { m.push(m.stack[m.stack.length - 2]); },
    'ROT': function() { var c = m.pop(), b = m.pop(), a = m.pop(); m.push(b); m.push(c); m.push(a); },
    '2DUP': function() { var b = m.stack[m.stack.length - 1], a = m.stack[m.stack.length - 2]; m.push(a); m.push(b); },
    // Comparison
    '=': function() { m.push(m.pop() === m.pop() ? -1 : 0); },
    '<': function() { var b = m.pop(), a = m.pop(); m.push(a < b ? -1 : 0); },
    '>': function() { var b = m.pop(), a = m.pop(); m.push(a > b ? -1 : 0); },
    '0=': function() { m.push(m.pop() === 0 ? -1 : 0); },
    // Output
    '.': function() { console.log(m.pop()); },
    '.S': function() { console.log("Stack:", m.stack.slice()); }
  };
};

ForthMachine.prototype.run = function(source) {
  var tokens = source.trim().split(/\s+/);
  for (var i = 0; i < tokens.length; i++) {
    var token = tokens[i];
    if (token === "") continue;
    if (!isNaN(token)) { this.push(Number(token)); continue; }
    var word = this.dict[token.toUpperCase()];
    if (!word) throw new Error("Unknown word: " + token);
    word();
  }
  return this.stack;
};

var forth = new ForthMachine();

// No variables needed! Stack manipulation suffices.
console.log("=== Forth stack computation ===");
console.log("2 3 + 4 * → ",    forth.run("2 3 + 4 *"));          // (2+3)*4 = 20
forth.stack=[];
console.log("5 DUP * → ",      forth.run("5 DUP *"));            // 5*5 = 25
forth.stack=[];
console.log("10 3 MOD → ",     forth.run("10 3 MOD"));           // 10 mod 3 = 1
forth.stack=[];
// Hypotenuse without naming variables: √(a²+b²)
console.log("Hypotenuse: ", forth.run("3 DUP * 4 DUP * + "));  // 3²+4² = 25
console.log("(√25 not implemented, but 3 DUP * 4 DUP * + gives 25)");

forth.stack=[];
// ROT reorders: a b c → b c a
forth.run("1 2 3");
console.log("\nBefore ROT: [1 2 3]"); forth.run("ROT");
console.log("After ROT:  ", forth.stack);
§07

The dictionary — extending the language

forth

In Forth, the compiler and interpreter share a single structure: the dictionary. Every word — whether a built-in primitive or something you just defined — is an entry in the dictionary. Defining a new word is not "writing a function"; it is adding a word to the language.

The colon definition : SQUARE DUP * ; adds SQUARE to the dictionary. From that point, SQUARE is indistinguishable from any built-in. You can extend Forth with domain-specific words until your code reads like the specification of the problem itself.

Dictionary growth — add words and watch the language expand
forth_dictionary.js — defining words and building a language upward
// Forth's dictionary: define new words from old ones
// The language grows from the bottom up — primitives to domain words

class ForthDict {
  constructor() {
    this.words   = {};
    this.stack   = [];
    this.compiling = false;
    this.newWordName = null;
    this.newWordTokens = [];
    this.addPrimitives();
  }

  addPrimitives() {
    const p = (name, fn) => { this.words[name] = { type: 'primitive', fn }; };
    p('+',    ()=>this.stack.push(this.stack.pop()+this.stack.pop()));
    p('*',    ()=>this.stack.push(this.stack.pop()*this.stack.pop()));
    p('-',    ()=>{const b=this.stack.pop(),a=this.stack.pop();this.stack.push(a-b);});
    p('DUP',  ()=>this.stack.push(this.stack.at(-1)));
    p('DROP', ()=>this.stack.pop());
    p('SWAP', ()=>{const b=this.stack.pop(),a=this.stack.pop();this.stack.push(b,a);});
    p('OVER', ()=>this.stack.push(this.stack.at(-2)));
    p('ROT',  ()=>{const[c,b,a]=[this.stack.pop(),this.stack.pop(),this.stack.pop()];this.stack.push(b,c,a);});
  }

  execute(token) {
    if (!isNaN(token)) { this.stack.push(Number(token)); return; }
    const word = this.words[token.toUpperCase()];
    if (!word) throw new Error(`Undefined: ${token}`);
    if (word.type === 'primitive') word.fn();
    else word.tokens.forEach(t => this.execute(t));
  }

  run(source) {
    const tokens = source.trim().split(/\s+/);
    for (const tok of tokens) {
      if (tok === ':') { this.compiling = true; continue; }  // start definition
      if (tok === ';') {                                         // end definition
        this.words[this.newWordName] = { type: 'defined', tokens: this.newWordTokens };
        console.log(`Defined: ${this.newWordName} = [${this.newWordTokens.join(' ')}]`);
        this.compiling = false; this.newWordName = null; this.newWordTokens = [];
        continue;
      }
      if (this.compiling) {
        if (!this.newWordName) { this.newWordName = tok.toUpperCase(); continue; }
        this.newWordTokens.push(tok.toUpperCase()); continue;
      }
      this.execute(tok);
    }
    return [...this.stack];
  }
}

const f = new ForthDict();

// Build up a vocabulary layer by layer
console.log("=== Building a language from primitives ===");

// Layer 1: Basic combinators
f.run(": SQUARE DUP * ;");       // SQUARE: n → n²
f.run(": CUBE DUP SQUARE * ;");  // CUBE: uses SQUARE
f.run(": NIP SWAP DROP ;");       // NIP: a b → b
f.run(": TUCK SWAP OVER ;");      // TUCK: a b → b a b

// Layer 2: Domain words built from layer 1
f.run(": HYPOTENUSE-SQUARED SQUARE SWAP SQUARE + ;");

f.stack=[];
console.log("\n5 SQUARE =", f.run("5 SQUARE"));
f.stack=[];
console.log("3 CUBE =", f.run("3 CUBE"));
f.stack=[];
console.log("3 4 HYPOTENUSE-SQUARED =", f.run("3 4 HYPOTENUSE-SQUARED"));

console.log("\nDictionary:", Object.keys(f.words).join("  "));
console.log("All words are equal — primitives and defined words are indistinguishable.");
§08

Concatenative programming

forth

Forth is a concatenative language. This means: putting two programs next to each other produces their composition. A B means "run A, then run B on whatever A left on the stack." Function composition is just concatenation of source text.

This has deep algebraic consequences. Programs are not expressions that evaluate to values — they are functions from stack state to stack state, and juxtaposition is their composition operator. The entire language has no syntax beyond this: a program is a sequence of words.

Charles Moore: "Forth is the most invisible language I can devise." He meant: the language itself is never in the way. There's no grammar to learn, no operator precedence to memorise, no type declarations to write. There are only words on the stack.
concatenative.js — programs as stack-to-stack functions, composition as juxtaposition
// Concatenative programming: programs ARE functions Stack→Stack
// Juxtaposition = function composition. No other syntax needed.

// Model a stack-to-stack function
const word = (name, fn) => ({ name, fn });

// Primitives — each transforms a stack
const DUP   = word('DUP',  s => [...s, s.at(-1)]);
const DROP  = word('DROP', s => s.slice(0,-1));
const SWAP  = word('SWAP', s => [...s.slice(0,-2), s.at(-1), s.at(-2)]);
const OVER  = word('OVER', s => [...s, s.at(-2)]);
const PLUS  = word('+',    s => [...s.slice(0,-2), s.at(-2)+s.at(-1)]);
const TIMES = word('*',    s => [...s.slice(0,-2), s.at(-2)*s.at(-1)]);
const MINUS = word('-',    s => [...s.slice(0,-2), s.at(-2)-s.at(-1)]);
const LIT   = n => word(String(n), s => [...s, n]);

// CONCATENATION = composition. Put words next to each other → compose them.
const concat = (...words) => ({
  name: words.map(w=>w.name).join(' '),
  fn:   stack => words.reduce((s, w) => w.fn(s), stack)
});

// DEFINE higher-level words by concatenating lower-level ones
const SQUARE = concat(DUP, TIMES);        // n → n² (= DUP *)
const CUBE   = concat(DUP, SQUARE, TIMES); // n → n³ (= DUP SQUARE *)
const NIP    = concat(SWAP, DROP);         // a b → b
const TUCK   = concat(SWAP, OVER);         // a b → b a b

// Run a "program" = a composed word on an initial stack
const run = (prog, init=[]) => prog.fn(init);

console.log("SQUARE 5 → ", run(concat(LIT(5), SQUARE)));
console.log("CUBE 3 → ",   run(concat(LIT(3), CUBE)));
console.log("TUCK 2 7 → ", run(concat(LIT(2), LIT(7), TUCK)));

// KEY INSIGHT: program text = just a sequence of names. No parse tree.
// "3 DUP * 4 DUP * +" is literally [LIT(3), DUP, TIMES, LIT(4), DUP, TIMES, PLUS]
const hyp_sq = concat(LIT(3), DUP, TIMES, LIT(4), DUP, TIMES, PLUS);
console.log("3 DUP * 4 DUP * + → ", run(hyp_sq), "(= 3² + 4² = 25)");

// Type-level: every Forth word has stack effect ( before -- after )
// DUP  : ( n -- n n )      ← consumes 1, produces 2
// DROP : ( n -- )           ← consumes 1, produces 0
// SWAP : ( a b -- b a )     ← consumes 2, produces 2
// OVER : ( a b -- a b a )   ← consumes 2, produces 3
// +    : ( a b -- a+b )     ← consumes 2, produces 1
// SQUARE: ( n -- n² )       ← derived: (DUP *)
console.log("\nStack effect notation encodes the type of each word.");
console.log("Factor (modern Forth) uses this to infer types statically.");
§09

Meta-compilation — Forth in Forth

forth

Forth's most radical feature is that much of its compiler can be expressed in the language itself. A colon definition like : SQUARE DUP * ; does not invoke a giant separate compiler pipeline; it switches the system into "compile mode," reads words one by one, and appends their behavior to the new definition. In many Forth systems the words that manage this process live in the same dictionary as ordinary words.

Immediate words are the key mechanism. Most words encountered inside a definition are compiled: the compiler records "call this word later at runtime." But an immediate word executes right now while compilation is happening. That lets it modify the code being generated. The classic example is ;: instead of being compiled into the new word, it runs immediately and says "the definition ends here."

Control structures work the same way. In a line like : ABS-SIGN DUP 0= IF DROP 0 ELSE DUP * THEN ;, the arithmetic words such as DUP, DROP, and * are compiled as normal runtime operations. But IF, ELSE, and THEN run during compilation. IF emits a conditional branch with an unknown destination. ELSE fills in the earlier branch target and emits a new jump over the else-part. THEN finally fills in the last missing destination. That "fill in the address later" step is called backpatching.

Read the demo in two phases. Compile time: the immediate words edit the code buffer and resolve jumps. Runtime: the finished word executes the bytecode that compile time produced. The striking part is that Forth exposes both phases to the programmer as ordinary words.
meta_forth.js — immediate words and the Forth meta-compiler
// Forth meta-compilation: control structures built from IMMEDIATE words
// In standard Forth: IF, ELSE, THEN, BEGIN, UNTIL are all defined in Forth itself
// Here we simulate the mechanism that makes this possible

class MetaForth {
  constructor() {
    this.dict       = {};
    this.stack      = [];
    this.compiling  = false;
    this.codeBuffer = []; // instructions being compiled
    this.patches    = []; // addresses needing backpatching (for forward jumps)
    this.addPrimitives();
    this.addControlWords();
  }

  addPrimitives() {
    const add = (name, fn, immediate=false) => { this.dict[name] = {fn, immediate}; };
    add('+',   ()=>{ const[b,a]=[this.stack.pop(),this.stack.pop()]; this.stack.push(a+b); });
    add('-',   ()=>{ const[b,a]=[this.stack.pop(),this.stack.pop()]; this.stack.push(a-b); });
    add('*',   ()=>{ const[b,a]=[this.stack.pop(),this.stack.pop()]; this.stack.push(a*b); });
    add('DUP', ()=>this.stack.push(this.stack.at(-1)));
    add('DROP',()=>this.stack.pop());
    add('0=',  ()=>this.stack.push(this.stack.pop()===0?-1:0));
    add('.',   ()=>console.log(this.stack.pop()));
  }

  addControlWords() {
    // IMMEDIATE words run at COMPILE TIME, not runtime
    // They can insert instructions and patch jump addresses
    const add = (name, fn) => { this.dict[name] = {fn, immediate: true}; };

    // IF: ( flag -- ) branch if false
    // Compiles a conditional jump; records address for ELSE/THEN to patch
    add('IF', () => {
      this.codeBuffer.push({ op: 'BRANCH_FALSE', target: null });
      this.patches.push(this.codeBuffer.length - 1); // remember to backpatch
      console.log(`  [compile-time] IF: emitted BRANCH_FALSE at [${this.codeBuffer.length-1}]`);
    });

    // ELSE: patch the IF's branch, emit unconditional jump for THEN
    add('ELSE', () => {
      this.codeBuffer.push({ op: 'JUMP', target: null });
      const elseAddr = this.codeBuffer.length - 1;
      const ifAddr = this.patches.pop();
      this.codeBuffer[ifAddr].target = this.codeBuffer.length; // patch IF to jump here
      this.patches.push(elseAddr);
      console.log(`  [compile-time] ELSE: patched IF[${ifAddr}]→${this.codeBuffer.length}, emitted JUMP[${elseAddr}]`);
    });

    // THEN: patch IF or ELSE's jump to land here
    add('THEN', () => {
      const patchAddr = this.patches.pop();
      this.codeBuffer[patchAddr].target = this.codeBuffer.length;
      console.log(`  [compile-time] THEN: patched [${patchAddr}]→${this.codeBuffer.length}`);
    });

    // BEGIN/UNTIL: loop until condition is true
    add('BEGIN', () => {
      this.patches.push(this.codeBuffer.length); // remember loop start
    });
    add('UNTIL', () => {
      const loopStart = this.patches.pop();
      this.codeBuffer.push({ op: 'BRANCH_FALSE', target: loopStart });
      console.log(`  [compile-time] UNTIL: emit BRANCH_FALSE→${loopStart}`);
    });
  }

  compileWord(name, tokens) {
    console.log(`\nCompiling: ${name} [ ${tokens.join(' ')} ]`);
    this.codeBuffer = [];
    for (const tok of tokens) {
      if (!isNaN(tok)) { this.codeBuffer.push({op:'LIT',val:Number(tok)}); continue; }
      const w = this.dict[tok.toUpperCase()];
      if (!w) throw new Error(`Unknown: ${tok}`);
      if (w.immediate) w.fn();           // IMMEDIATE: run NOW at compile time
      else this.codeBuffer.push({op:'CALL', word: tok.toUpperCase()});
    }
    this.dict[name] = { fn: () => this.executeCode(this.dict[name].code), code: [...this.codeBuffer] };
    console.log("  Compiled bytecode:", this.codeBuffer.map(i=>i.op+(i.val??i.word??('→'+(i.target??'?')))).join(' '));
  }

  executeCode(code) {
    let ip = 0;
    while (ip < code.length) {
      const ins = code[ip];
      if (ins.op === 'LIT') { this.stack.push(ins.val); ip++; continue; }
      if (ins.op === 'CALL') { this.dict[ins.word].fn(); ip++; continue; }
      if (ins.op === 'BRANCH_FALSE') {
        const flag = this.stack.pop();
        ip = flag === 0 ? ins.target : ip + 1;
        continue;
      }
      if (ins.op === 'JUMP') { ip = ins.target; continue; }
      throw new Error(`Unknown opcode: ${ins.op}`);
    }
    return this.stack;
  }
}

const mf = new MetaForth();

// Compile an IF/ELSE/THEN word — the control structure is built by IMMEDIATE words
mf.compileWord('ABS-SIGN', ['DUP','0=','IF','DROP','0','ELSE','DUP','*','THEN']);
mf.stack = [0]; mf.dict['ABS-SIGN'].fn(); console.log("ABS-SIGN on 0  →", mf.stack);
mf.stack = [3]; mf.dict['ABS-SIGN'].fn(); console.log("ABS-SIGN on 3  →", mf.stack);
console.log("\nIF ELSE THEN are not special syntax — they are IMMEDIATE words");
console.log("that run at compile time and emit/patch jump instructions.");
console.log("Any Forth programmer can write their own control structures the same way.");
§10

Legacy — what every language owes them

both

Many central ideas in modern programming languages passed through Lisp or Forth before spreading more widely. Their influence shows up clearly in later languages and traditions including Smalltalk, Self, JavaScript, Python, Ruby, Lua, Clojure, Factor, and parts of Rust.

IdeaOriginates inNow found in
Garbage collectionLisp (1958)Java, C#, Python, Go, JS, Ruby — virtually everything
First-class functions / lambdasLisp (1958)Python, JS, Haskell, C# (LINQ), Java 8, Rust, Swift
Lexical closuresScheme / Lisp tradition (1970s)JavaScript, Python, Ruby, Go, Rust, Swift, Kotlin
REPLsLisp (1960)Python, Ruby (irb), Node.js, Julia, R, Haskell (ghci)
Macros / compile-time metaprogrammingLisp (1963)Rust (macro_rules!), Elixir, Nim, Racket, Julia
HomoiconicityLisp (1958)Clojure, Racket, Julia, Prolog (terms), Rebol
Stack-oriented language designForth (1970)JVM bytecode, .NET CLR, WebAssembly, PostScript, Factor
Concatenative / point-free styleForth (1970)Factor, Joy, Cat, Haskell (point-free), Unix pipes
Extensible compiler (immediate words)Forth (1970)Rust proc-macros, Racket #lang, Elixir macros
Embedded DSL creationBothRuby (Rails DSL), Groovy, Clojure, Kotlin
Language-as-material philosophyBothSmalltalk, Self, Io, Rebol, Factor, Racket
Alan Kay (Smalltalk inventor): "The greatest single programming language ever designed." He was talking about Lisp. Chuck Moore: "All software is too big, and all software is too slow. There's only one solution." He wrote the control software for a radio telescope in 1,000 words of Forth. Both men were right about the same thing: the right abstraction, applied consistently, reduces everything.