Backtracking in
Time and Space
Quinn Wilton / @wilton_quinn
Robert Virding / @rvirding
Welcome 👋
- Hi, I'm Quinn!
- Applied Researcher @ Fission
- Building a planetary scale database for local-first applications
- Programming language anthropologist
-
@wilton_quinn on Twitter
- Applied Researcher @ Fission
- ...and I'm Robert!
- Principal Language Expert @ Erlang Solutions
- Co-inventor of Erlang
- Still trying to make Lisp happen
- @rvirding on Twitter
There will always be things we wish to say in our programs that in all known languages can only be said poorly
- Alan Perlis, Epigrams on Programming (1982)
Old Stockholm Telephone Tower, 1890
5,500 Telephone Lines of Fire Hazard
AXD 301, 1998
240,000 Simultaneous Calls
Ellemtel, 1988
The Computer Science Laboratory
2012 2017 2022
Discord
Adobe
EEF
10,000+
Elixir
Gleam
LFE
2,000
"Web-Scale"
Open Source
10,000+ (forums, Slack, etc)
(* List of BEAM languages and users not comprehensive!)
The Unique Challenges of Telecom
Strand: New Concepts in Parallel Programming (1990)
1963
Högertrafikomläggningen
Håll dig till höger,
Svensson, håll
dig till höger
Annars slutar det
bara med en smäll
Keep to the right, Svensson
"Otherwise it just ends with a bang"
Stockholm, September 3rd, 1967
The first recorded instance of hot-code swapping
Montreal, 1970
TAUM group, including Alain Colmeraur
Traduction Automatique de l’Université de Montréal
(From "Fifty Years of Prolog and Beyond", 2022)
(From "Metamorphosis Grammars", 1978)
(̵F̵r̴o̸m̷ ̸"̴M̴e̸t̵a̶m̷o̴r̸p̷h̸o̷s̴i̴s̴ ̷G̸r̵a̸m̴m̵a̷r̴s̵"̸,̷ ̶1̵9̵7̶8̸)̵
Prolog was not created as a programming language [...] we wanted to tell a story in French to the computer, then we would ask questions and it was supposed to answer back. [...] Prolog was created as a tool to do that.
- Alain Colmerauer, Profession scientifique (1991)
From the start, Erlang was designed as a practical tool for getting the job done—this job being to program basic telephony services on a small telephone exchange. Programming this exchange drove the development of the language.
- Joe Armstrong, A History of Erlang (2007)
calc(n(X), X).
calc(add(X0, Y0), Z) :-
calc(X0, X1),
calc(Y0, Y1),
Z #= X1 + Y1.
calc(sub(X0, Y0), Z) :-
calc(X0, X1), calc(Y0, Y1), Z #= X1 - Y1.
calc(mult(X0, Y0), Z) :-
calc(X0, X1), calc(Y0, Y1), Z #= X1 * Y1.
add/2
n(2)
n(2)
n(2)
mult/2
?- calc(add(n(5), mult(n(2), n(3))), Z).
Z = 11.
?- calc(mult(n(3), add(n(X), n(Y))), 9),
X in 0..10,
Y in 0..10,
label([X, Y]).
X = 0,
Y = 3 ;
X = 1,
Y = 2 ;
X = 2,
Y = 1 ;
X = 3,
Y = 0.
More algebra than a peasant in the 1400s would get in their whole lifetime
Prolog Meta-Interpreters (MI)
vanilla(true).
vanilla((A,B)) :-
vanilla(A),
vanilla(B).
vanilla(g(G)) :-
clause(G, Body),
vanilla(Body).
Vanilla MI
(Adapted from "The Power of Prolog")
Prolog Meta-Interpreters (MI)
(Adapted from "The Power of Prolog")
vanilla(true).
vanilla((A,B)) :-
vanilla(A),
vanilla(B).
vanilla(g(G)) :-
clause(G, Body),
vanilla(Body).
Vanilla MI
"Reversed" MI
reversed(true).
reversed((A,B)) :-
reversed(B).
reversed(A),
reversed(g(G)) :-
clause(G, Body),
reversed(Body).
(Adapted from "Use of Prolog for developing a new programming language", 1992)
reduce([]).
reduce([g(G) | T]) :-
clause(G, Body),
vanilla(Body),
reduce(T).
reduce([Lhs | More]) :-
erlang2prolog(Lhs, Rhs),
append(Rhs, More, More1),
reduce(More1).
"Suspendable" MI
Early Erlang Meta-Interpreter (1988)
(Adapted from "Use of Prolog for developing a new programming language", 1992)
reduce([]).
reduce([g(G) | T]) :-
clause(G, Body),
vanilla(Body),
reduce(T).
reduce([Lhs | More]) :-
erlang2prolog(Lhs, Rhs),
append(Rhs, More, More1),
reduce(More1).
"Suspendable" MI
?- reduce([
factorial(3, F),
g(write(result(F))),
]).
result(6)
F = 6
Printing factorial(3)
Early Erlang Meta-Interpreter (1988)
Early Erlang Meta-Interpreter (1988)
(Adapted from "Use of Prolog for developing a new programming language", 1992)
erlang2prolog(factorial(0, 1), [])
erlang2prolog(factorial(N, F), [
g(N1 is N - 1),
factorial(N1, F1),
g(F is N * F1)
])
Definition of factorial/2
A programming language is low level when its programs require attention to the irrelevant.
- Alan Perlis, Epigrams on Programming (1982)
(Adapted from a 1988 implementation of Erlang 🤫)
/*
* /als/user/joe/tos/slurp.pro
*
* Author: Joe Armstrong
* Creation Date: 1988-03-25
* Purpose:
* slurp in erlang code
*
*/
Term rewriting!
:- op(1200,xfy,(--->)).
:- op(500,yfx,'=>').
:- op(1198,xfy,(#)).
:- op(800,xfy,(:=)).
convert((N # Lhs ---> Rhs), (Lhs :- erlang(N,L1,X))) :-
l2c(L,Rhs),
append(L,X,L1).
1 # d1(H,Tm) --->
link(H),
Self := self,
link_hw(H,Self),
send(H,activate_elu5(8)),
receive([
msg(H,X) =>
write(msg(H,X))
]),
reset(H),
link(Tm),
put(tone, none),
d1_loop(H,Tm).
1 # d1 --->
init_hw,
simulate(off),
Tm := spawn(
switch_manager,
10
),
Id1 := spawn(
d1(dts(66),Tm),
10
),
Id2 := spawn(
d1(dts(67),Tm),
10
),
exit(normal).
(Adapted from a 1988 implementation of Erlang 🤫)
Meta-Interpreters in Elixir!
(https://github.com/elixir-nx/nx)
defmodule Example do
import Nx.Defn
defn multiply(x, y) do
x * y
end
end
Nx's defn macro
Scalar Multiplication
iex(1)> Example.multiply(
2,
~V[1 2 3]
)
#Nx.Tensor<
s64[3]
[2, 4, 6]
>
(https://github.com/elixir-nx/nx)
defmodule Nx.Defn do
defmacro defn(call, do: block) do
define(call, block)
end
defp define(call, block) do
quote do
def unquote(call) do
use Nx.Defn.Kernel
unquote(block)
end
end
end
end
(Simplified) Nx.Defn.defn/2
(https://github.com/elixir-nx/nx)
defmodule Nx.Defn.Kernel do
defmacro __using__(_opts) do
quote do
import Kernel, only: []
import Nx.Defn.Kernel
alias Nx.Defn.Kernel, as: Kernel
end
end
def left * right when is_number(left) and is_number(right) do
Kernel.*(left, right)
end
def left * right, do: Nx.multiply(left, right)
end
(Simplified) Nx.Defn.Kernel
The disadvantage of this syntax is that error messages, run time diagnostics, etc. are in terms of Prolog data structures — not Erlang.
- Use of Prolog in developing a new programming language (1992)
1 # toggle_tone(dts(H), none, Tone, Tm) --->
send(Tm, start_tone(H, Tone)),
put(tone, Tone).
2 # toggle_tone(dts(H), X, Y, Tm) --->
send(Tm, stop_tone(H)),
put(tone, none).
Ellemtel, 1993
Robert and his trains
lock(Train, OldSpeed) ->
receive {
_ ? block_locked(_, Speed, SensorNo, NewTime) =>
driver ! set_speed(OldSpeed, Train),
^{Speed, NewTime, SensorNo};
_ ? stop =>
driver ! set_speed(0, Train),
throw(normal);
_ ? halt =>
driver ! set_speed(0, Train),
lock(Train, OldSpeed);
}.
(Taken from "Erlang och realtidskontronllerad järnväg", 1990)
A PIQUE at Definite Clause Grammars
sentence --> noun_phrase, verb_phrase.
noun_phrase --> det, noun.
verb_phrase --> verb, noun_phrase.
det --> "the".
det --> "a".
noun --> ...
verb --> ...
A DCG for simple sentences
(Whitespace stripped for clarity)
?- phrase(sentence, "the computer parsed a sentence").
true.
?- phrase(sentence, "the physicist said, 'Hello, Mike'").
false.
Parsing with the DCG
?- phrase(sentence, X).
X = "the viking befriended a ninja" ;
X = "a conductor aggrieved the telephone" ;
X = "the cloud weathered the storm" ;
...
Generating text with the DCG
sentence -->
noun_phrase,
verb_phrase.
noun_phrase -->
det,
noun.
verb_phrase -->
verb,
noun_phrase.
det --> "the".
det --> "a".
sentence(A,Z) :-
noun_phrase(A,B),
verb_phrase(B,Z).
noun_phrase(A,Z) :-
det(A,B),
noun(B,Z).
verb_phrase(A,Z) :-
verb(A,B),
noun_phrase(B,Z).
det(["the"|X], X).
det(["a"|X], X).
Definite Clause Grammar
Definite Clauses
RETRIEVE name, age, occupation WHERE
name = "Mike Williams" AND
occupation = "telecom engineer" AND
NOT age < 29
The PIQUE query language
(Described in "PIQUE: A relational query language without relations", from 1987)
?- phrase_from_file(query(As, Cs), "query.pique").
As = [name, age, occupation],
Cs = [
eq(name, "Mike Williams"),
eq(occupation, "telecom engineer"),
not(lt(age, "29"))
].
Invoking the parser
query(Attrs, Conditions) -->
"RETRIEVE",
attr_list(Attrs),
"WHERE",
condition_list(Conditions).
attr_list([A|As]) -->
attr(A), attr_list_tail(As).
attr_list_tail([]) --> [].
attr_list_tail([A|As]) -->
",", attr(A), attr_list_tail(As).
...
defmodule Parser do
def sentence(a) do
b = noun_phrase(a)
z = verb_phrase(c)
end
def noun_phrase(a) do
b = det(a)
z = noun(c)
end
def verb_phrase(a) do
b = verb(a)
z = noun_phrase(c)
end
def det("the" <> x), do: x
def det("a" <> x), do: x
def noun("witch" <> x), do: x
def noun("demon" <> x), do: x
def verb("summoned" <> x), do: x
end
sentence -->
noun_phrase,
verb_phrase.
noun_phrase -->
det,
noun.
verb_phrase -->
verb,
noun_phrase.
det --> "the".
det --> "a".
noun --> ...
verb --> ...
iex(1)> sentence("the witch summoned the demon")
""
iex(2)> sentence("the witch creeped past slowly")
** (FunctionClauseError) no function clause matching in Parser.det/1
The following arguments were given to Parser.det/1:
# 1
"past slowly"
Prolog had DCGs, Erlang didn't, Elixir has the pipe operator.
- Joe Armstrong, A week with Elixir (2013)
defmodule Parser do
def sentence(a) do
a |> noun_phrase() |> verb_phrase()
end
def noun_phrase(a) do
a |> det() |> noun()
end
def verb_phrase(a) do
a |> verb() |> noun_phrase()
end
end
Parser rewritten to use monads pipes
defmodule Parser do
import DCG.Defdcg
defdcg sentence, do:
noun_phrase ~> verb_phrase
defdcg noun_phrase, do:
det ~> noun
defdcg verb_phrase, do:
verb ~> noun_phrase
defdcg det, do: "the"
defdcg det, do: "a"
defdcg noun, do: "witch" or "demon" or ...
defdcg verb, do: "summoned" or ...
end
A tiny DSL for DCGs
Often the semantics of an Erlang expression were not the result of a conscious design decision but were the accidental result of the implementation in Prolog - we call this phenomena semantic embedding - the semantics of Prolog become accidentally embedded in Erlang.
- Use of Prolog for developing a new programming language (1992)
(From "Strand: New Concepts in Parallel Programming", from 1990)
The phones wouldn't stop ringing.
- Robert Virding
ring_phone
unused_line(1).
unused_line(2).
unused_line(3).
ring_phone :- unused_line(X), shell('say ring'), fail.
ring_phone :- shell('say banana phone').
unused_line(X)
X=1
💍
❌
X=2
💍
❌
X=3
💍
❌
🍌☎️
The bottom line is that the more powerful a language, the harder it is to understand systems constructed in it.
- Ben Moseley and Peter Marks, Out of the Tar Pit (2006)
Simplicity is a great virtue but it requires hard work to achieve it and education to appreciate it. And to make matters worse: complexity sells better.
- Edsger W. Dijkstra, On the Nature of Computing Science (1984)
link(a, b).
link(b, c).
link(c, d).
link(d, a).
link(e, f).
link(f, g).
link(f, h).
link(g, c).
reachable(S, D) :- link(S, D).
reachable(S, D) :-
link(S, Z),
reachable(Z, D).
in_cycle(N) :- reachable(N, N).
?- reachable(f, N).
N = a;
N = b;
N = c;
N = d;
N = g;
N = h.
reachable(S, D) :- link(S, D).
reachable(S, D) :-
link(S, Z),
reachable(Z, D).
?- in_cycle(N).
N = a;
N = b;
N = c;
N = d;
reachable(S, D) :- link(S, D).
reachable(S, D) :-
link(S, Z),
reachable(Z, D).
in_cycle(N) :- reachable(N, N).
(From https://github.com/rust-lang/polonius)
Excerpt from a model of Rust's borrow checker
Neither a Borrower Nor a Lender Be
defmodule Analysis do
use Croline.DSL
defdatalog :analysis do
input inst_name(inst, name)
input inst_arg(inst, i, arg)
input fn_label(f, label)
fact ...
rule ...
end
end
defmodule Example do
def add(x, y)
when is_integer(x) do
x + y
end
end
iex(1)> Decompiler.decompile(Example)
{Example, ..., [],
[
...
{:function, :add, 2, 9,
[
{:label, 8},
{:func_info, {:atom, Example}, {:atom, :add}, 2},
{:label, 9},
{:test, :is_integer, {:f, 8}, [x: 0]},
{:gc_bif, :+, {:f, 0}, 2, [x: 0, x: 1], {:x, 0}},
:return
]},
...
], 14}
[
{:fn_label, [{Example, :add, 2, 9}]},
{:inst_name, [21, :label]},
{:inst_arg, [21, 0, 8]},
{:inst_name, [22, :line]},
{:inst_name, [23, :func_info]},
{:inst_name, [24, :label]},
{:inst_arg, [24, 0, 9]},
{:inst_name, [25, :test]},
{:inst_arg, [25, 0, :is_integer]},
{:inst_arg, [25, 1, {:f, 8}]},
{:inst_arg, [25, 2, {:x, 0}]},
{:inst_name, [28, :gc_bif]},
{:inst_arg, [28, 0, :+]},
{:inst_arg, [28, 1, {:f, 0}]},
{:inst_arg, [28, 2, 2]},
{:inst_arg, [28, 3, [x: 0, x: 1]]},
{:inst_arg, [28, 4, {:x, 0}]},
{:inst_name, [29, :return]}
]
iex(1)> Decompiler.decompile(Example)
{Example, ..., [],
[
...
{:function, :add, 2, 9,
[
{:label, 8},
{:func_info, {:atom, Example}, {:atom, :add}, 2},
{:label, 9},
{:test, :is_integer, {:f, 8}, [x: 0]},
{:gc_bif, :+, {:f, 0}, 2, [x: 0, x: 1], {:x, 0}},
:return
]},
...
], 14}
{:function, :add, 2, 9,
[
{:label, 8},
{:func_info, {:atom, Example}, {:atom, :add}, 2},
{:label, 9},
{:test, :is_integer, {:f, 8}, [x: 0]},
{:gc_bif, :+, {:f, 0}, 2, [x: 0, x: 1], {:x, 0}},
:return
]}
fact inst_exits(:return)
fact inst_exits(:func_info)
rule link(src, dest), do:
inst_name(src, name) and
!inst_exits(name) and
dest = src + 1
rule flow(src, dest), do: link(src, dest)
rule flow(src, dest), do:
flow(src, hop) and
link(hop, dest)
fact inst_exits(:return)
fact inst_exits(:func_info)
rule link(src, dest), do:
inst_name(src, name) and
!inst_exits(name) and
dest = src + 1
rule flow(src, dest), do: link(src, dest)
rule flow(src, dest), do:
flow(src, hop) and
link(hop, dest)
{:function, :add, 2, 9,
[
{:label, 8},
{:func_info, {:atom, Example}, {:atom, :add}, 2},
{:label, 9},
{:test, :is_integer, {:f, 8}, [x: 0]},
{:gc_bif, :+, {:f, 0}, 2, [x: 0, x: 1], {:x, 0}},
:return
]}
rule block_head(label, inst), do:
inst_name(inst, :label) and
inst_arg(inst, 0, label)
rule block_tail(label, inst), do:
block_head(label, head) and
flow(head, inst)
defmodule Example do
def foo(x) do
bar(x)
add(x, x)
end
def bar(x) do
identity(x)
end
def identity(x) do
x
end
def add(x, y)
when is_integer(x) do
x + y
end
end
rule fn_reachable(src, dest), do:
fn_call(src, dest)
rule fn_reachable(src, dest), do:
fn_call(src, hop) and
fn_reachable(hop, dest)
iex(1)> Analysis.run(Example,
query:
"?- fn_reachable({_, :foo, 1}, D)."
)
#MapSet<[
[D: {Example, :bar, 1}],
[D: {Example, :identity, 1}],
[D: {Example, :add, 2}]
]>
(From "Making reliable distributed systems in the presence of software errors", 2003)
fact dangerous({:erlang, :link, 1})
fact dangerous({:erlang, :unlink, 1})
fact dangerous({:erlang, :spawn_link, 1})
fact dangerous({:erlang, :spawn_link, 2})
fact dangerous({:erlang, :spawn_link, 3})
fact dangerous({:erlang, :spawn_link, 4})
fact dangerous({:erlang, :spawn, 1})
fact dangerous({:erlang, :spawn, 2})
fact dangerous({:erlang, :spawn, 3})
fact dangerous({:erlang, :spawn, 4})
rule fn_dangerous(Src), do:
dangerous(Src)
rule fn_dangerous(Src), do:
fn_reachable(Src, Dest) and
fn_dangerous(Dest)
defmodule Example do
def pure_a(x) do
pure_b(x)
end
def pure_b(x) do
x
end
def impure_a(x) do
impure_b(x)
end
def impure_b(x) do
spawn(fn -> x end)
end
end
iex(1)> Analysis.run(Example,
query: "?- fn_dangerous(F).")
)
#MapSet<[
[F: {Example, :impure_a, 1}],
[F: {Example, :impure_b, 1}],
]>
Lua
- “Lua is a powerful, efficient, lightweight, embeddable scripting language. It supports procedural programming, object-oriented programming, functional programming, data-driven programming, and data description.”
- Simple, rather neat little imperative language
- Dynamic language
- Lexically scoped
- Mutable variables/environments/global data
- Common scripting language in games
Luerl
-
Implements Lua 5.3
-
All of it!
-
Shared, mutable, global data
-
Lua handling of code
-
...
-record(luerl, {tabs, %Table table
envs, %Environment table
usds, %Userdata table
fncs, %Function table
g, %Global table
%%
stk=[], %Current stack
cs=[], %Current call stack
%%
meta=[], %Data type metatables
rand, %Random state
tag, %Unique tag
trace_func=none, %Trace function
trace_data %Trace data
}).
%% Table structure.
-record(tstruct, {data, %Data table/array
free, %Index free list
next %Next index
}).
If seriously pressed, good programmers can make an interpreter or compiler for any language they please in any language they are stuck with. This is not very hard, but it is probably the most powerful move a programmer can make.
- Gerald Sussman, Software Design for Flexibility (2021)
Backtracking in Time and Space
By quinnwilton
Backtracking in Time and Space
- 587