Você tem boas respostas até agora; deixe-me dar um exemplo impraticável, mas altamente educacional, de como você pode criar um idioma sem a noção de pilhas ou "fluxo de controle". Aqui está um programa que determina fatoriais:
function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = f(3)
Colocamos este programa em uma string e avaliamos o programa por substituição textual. Então, quando estamos avaliando f(3)
, fazemos uma pesquisa e substituímos por 3 por i, assim:
function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = if 3 == 0 then 1 else 3 * f(3 - 1)
Ótimo. Agora, realizamos outra substituição textual: vemos que a condição do "se" é falsa e substituímos outra string, produzindo o programa:
function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = 3 * f(3 - 1)
Agora, substituímos outra string em todas as subexpressões que envolvem constantes:
function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = 3 * f(2)
E você vê como isso vai; Não vou insistir mais nisso. Poderíamos continuar fazendo uma série de substituições de cordas até chegarmos ao fim let x = 6
e terminarmos.
Usamos a pilha tradicionalmente para variáveis locais e informações de continuação; lembre-se, uma pilha não diz de onde você veio, mas para onde você está indo a seguir com esse valor de retorno em mãos.
No modelo de programação de substituição de cadeia, não há "variáveis locais" na pilha; os parâmetros formais são substituídos por seus valores quando a função é aplicada ao seu argumento, em vez de colocados em uma tabela de pesquisa na pilha. E não há "ir a algum lugar a seguir" porque a avaliação do programa está simplesmente aplicando regras simples para que a substituição de cadeias produza um programa diferente, mas equivalente.
Agora, é claro, realmente fazer substituições de strings provavelmente não é o caminho a percorrer. Porém, linguagens de programação que suportam "raciocínio equacional" (como Haskell) estão logicamente usando essa técnica.