This is the predecessor of Mazeppa.
Supercompilation is a deep program transformation technique due to V. F. Turchin, a prominent computer scientist, cybernetician, physicist, and Soviet dissident. He described the concept as follows 1:
A supercompiler is a program transformer of a certain type. The usual way of thinking about program transformation is in terms of some set of rules which preserve the functional meaning of the program, and a step-by-step application of these rules to the initial program. ... The concept of a supercompiler is a product of cybernetic thinking. A program is seen as a machine. To make sense of it, one must observe its operation. So a supercompiler does not transform the program by steps; it controls and observes (SUPERvises) the running of the machine that is represented by the program; let us call this machine M1. In observing the operation of M1, the supercompiler COMPILES a program which describes the activities of M1, but it makes shortcuts and whatever clever tricks it knows in order to produce the same effect as M1, but faster. The goal of the supercompiler is to make the definition of this program (machine) M2 self-sufficient. When this is acheived, it outputs M2 in some intermediate language Lsup and simply throws away the (unchanged) machine M1.
A supercompiler is interesting not only as a program transformer but also as a very general philosophical concept:
The supercompiler concept comes close to the way humans think and make science. We do not think in terms of rules of formal logic. We create mental and linguistic models of the reality we observe. How do we do that? We observe phenomena, generalize observations, and try to construct a self-sufficient model in terms of these generalizations. This is also what the supercompiler does. ... A supercompiler would run M1, in a general form, with unknown values of variables, and create a graph of states and transitions between possible configurations of the computing system. ... To make it finite, the supercompiler performs the operation of generalization on the system configurations in such a manner that it finally comes to a set of generalized configurations, called basic, in terms of which the behavior of the system can be expressed. Thus the new program becomes a self-sufficient model of the old one.
Below is a complete OCaml implementation of the minimalistic supercompiler described by Gluck & Sorensen 2. The supercompiler deals with a first-order functional language called SLL (Simple Lazy Language).
There are several important differences between our supercompiler and the presentation from the paper:
- We do not have the if-then-else form. It is not crucial for supercompilation.
- We do not store terms in the graph. They are unnecessary because we can generate a residual program without them.
- Instead of replacing an ancestor during generalization, we replace a child and continue building the graph. This simplifies the algorithm by avoiding graph rebuilding. This possibility is discussed in the paper.
- We have a residual program generator. The paper mentions it but does not formalize it.
Overall, the implementation comprises ~300 LoC without the residuator (~220 without comments and blank lines) and ~420 LoC with the residuator (~310 without comments and blank lines).
Possible improvements include:
- Using integers instead of string identifiers (faster symbol table access).
- Implementing a user interface.
- Implementing good error reporting with source code positions (consider using Menhir).
- Implementing an alpha equivalence checker for graphs.
- Implementing a compiler from SLL to C (can be useful for benchmarking).
- Finding a sane way to do logging.
The full paper PDF can be accessed here.
TODO: write a newcomer-friendly blog post with complete explanations and link it here.
Show tests
let check ~equal ~show ~expected ~actual =
if not (equal actual expected) then (
Printf.eprintf "Expected:\n%s\n\nGot:\n%s\n" (show expected) (show actual);
(* Prints the code location of the failure. *)
assert false)
let print_subst fmt subst =
subst |> Subst.bindings |> [%derive.show: (string * term) list]
|> Format.pp_print_string fmt
type term_subst = (term Subst.t[@printer print_subst])
[@@deriving eq, show { with_path = false }]
type msg = term * term_subst * term_subst
[@@deriving eq, show { with_path = false }]
let test_subst () =
let check ~expected (t, map) =
var_counter := 0;
check ~equal:equal_term ~show:show_term ~expected
~actual:(subst ~map:(Subst.of_list map) t)
in
check ~expected:(Var "y") (Var "x", [ ("x", Var "y") ]);
check ~expected:(Var "x") (Var "x", [ ("z", Var "y") ]);
check
~expected:(f_call ("f", [ Var "x"; Var "b"; Var "y" ]))
( f_call ("f", [ Var "a"; Var "b"; Var "c" ]),
[ ("a", Var "x"); ("c", Var "y") ] )
let test_match_against () =
let check ~expected (t1, t2) =
var_counter := 0;
check ~equal:[%derive.eq: term_subst option]
~show:[%derive.show: term_subst option] ~expected
~actual:(match_against (t1, t2))
in
check ~expected:(Some (Subst.of_list [ ("x", Var "x") ])) (Var "x", Var "x");
check ~expected:(Some (Subst.of_list [ ("x", Var "y") ])) (Var "x", Var "y");
(* Same variables in lhs. *)
check
~expected:(Some (Subst.of_list [ ("x", Var "y") ]))
(f_call ("f", [ Var "x"; Var "x" ]), f_call ("f", [ Var "y"; Var "y" ]));
(* Unable to match. *)
check ~expected:None
(f_call ("f", [ Var "x"; Var "x" ]), f_call ("f", [ Var "y"; Var "z" ]));
(* Match respective arguments. *)
check
~expected:(Some (Subst.of_list [ ("x", Var "y"); ("a", Var "b") ]))
(f_call ("f", [ Var "x"; Var "a" ]), f_call ("f", [ Var "y"; Var "b" ]));
(* Different function names. *)
check ~expected:None (f_call ("f", []), f_call ("f'", []));
(* Different function kinds. *)
check ~expected:None (f_call ("f", []), ctr_call ("f", []))
let test_reduce () =
let check ?f_rules ?g_rules ~expected t =
var_counter := 0;
let program = create_program ?f_rules ?g_rules () in
check ~equal:(equal_step equal_term) ~show:(show_step pp_term) ~expected
~actual:(reduce ~program t)
in
check ~expected:(Stop "x") (Var "x");
check
~expected:(Decompose ("c", [ Var "x"; Var "y" ]))
(ctr_call ("c", [ Var "x"; Var "y" ]));
check
~f_rules:[ ("f", [ "x"; "y" ], ctr_call ("c", [ Var "x"; Var "y" ])) ]
~expected:(Transient (ctr_call ("c", [ Var "a"; Var "b" ])))
(f_call ("f", [ Var "a"; Var "b" ]));
let g_rules =
[
( "g",
[
( ("c", [ "p"; "q" ]),
[ "x"; "y" ],
ctr_call ("c", [ Var "p"; Var "q"; Var "x"; Var "y" ]) );
] );
]
in
check ~g_rules
~expected:
(Transient (ctr_call ("c", [ Var "m"; Var "n"; Var "a"; Var "b" ])))
(g_call ("g", ctr_call ("c", [ Var "m"; Var "n" ]), [ Var "a"; Var "b" ]));
let g_rules =
[
( "g",
[
( ("c", [ "m"; "n" ]),
[ "a"; "x" ],
ctr_call ("p", [ Var "m"; Var "n"; Var "a"; Var "x" ]) );
( ("b", [ "m" ]),
[ "a"; "x" ],
ctr_call ("q", [ Var "m"; Var "a"; Var "x" ]) );
] );
]
in
check ~g_rules
~expected:
(Variants
( "x",
[
( { c = "b"; fresh_vars = [ "v0" ] },
ctr_call
("q", [ Var "v0"; Var "a"; ctr_call ("b", [ Var "v0" ]) ]) );
( { c = "c"; fresh_vars = [ "v1"; "v2" ] },
ctr_call
( "p",
[
Var "v1";
Var "v2";
Var "a";
ctr_call ("c", [ Var "v1"; Var "v2" ]);
] ) );
] ))
(g_call ("g", Var "x", [ Var "a"; Var "x" ]));
(* Reduce an inner term (transient). *)
check
~f_rules:[ ("f", [], ctr_call ("c", [])) ]
~expected:
(Transient (g_call ("g", ctr_call ("c", []), [ Var "a"; Var "x" ])))
(g_call ("g", f_call ("f", []), [ Var "a"; Var "x" ]));
let g_rules =
[
( "g",
[
(("c", [ "m"; "n" ]), [], ctr_call ("p", []));
(("b", [ "m" ]), [], ctr_call ("q", []));
] );
]
in
(* Reduce an inner term (variants). *)
check ~g_rules
~expected:
(Variants
( "x",
[
( { c = "b"; fresh_vars = [ "v0" ] },
g_call
( "g'",
ctr_call ("q", []),
[ Var "a"; ctr_call ("b", [ Var "v0" ]) ] ) );
( { c = "c"; fresh_vars = [ "v1"; "v2" ] },
g_call
( "g'",
ctr_call ("p", []),
[ Var "a"; ctr_call ("c", [ Var "v1"; Var "v2" ]) ] ) );
] ))
(g_call
(* The variable [x] must be substituted branch-wise. *)
("g'", g_call ("g", Var "x", []), [ Var "a"; Var "x" ]))
let test_he () =
assert (decide_he (Var "x", Var "x"));
assert (decide_he (Var "x", Var "y"));
(* Coupling of respective arguments. *)
assert (
decide_he
(f_call ("f", [ Var "x"; Var "y" ]), f_call ("f", [ Var "x"; Var "y" ])));
(* Different function names. *)
assert (not (decide_he (f_call ("f", []), f_call ("f'", []))));
(* Different function kinds. *)
assert (not (decide_he (f_call ("f", []), ctr_call ("f", []))));
(* Incompatible arguments. *)
assert (
not
(decide_he
( f_call ("f", [ f_call ("f", []) ]),
f_call ("f", [ f_call ("f'", []) ]) )));
(* Diving into at least one argument. *)
assert (decide_he (Var "x", f_call ("f", [ Var "y"; ctr_call ("c", []) ])));
(* Incompatible with the argument. *)
assert (
not (decide_he (ctr_call ("c", []), f_call ("f", [ ctr_call ("c'", []) ]))));
(* A constructor cannot be embedded into a variable. *)
assert (not (decide_he (ctr_call ("c", []), Var "x")))
let test_msg () =
let check ~expected:(g, subst_1, subst_2) (t1, t2) =
var_counter := 0;
let expected = (g, Subst.of_list subst_1, Subst.of_list subst_2) in
let actual = compute_msg (t1, t2) in
check ~equal:equal_msg ~show:show_msg ~expected ~actual
in
(* A common functor. *)
check
~expected:
( ctr_call ("c", [ Var "v1"; Var "v2" ]),
[ ("v1", Var "x"); ("v2", Var "y") ],
[ ("v1", Var "a"); ("v2", Var "b") ] )
(ctr_call ("c", [ Var "x"; Var "y" ]), ctr_call ("c", [ Var "a"; Var "b" ]));
(* A common substitution (after a common functor). *)
check
~expected:
( ctr_call ("c", [ Var "v2"; Var "v2" ]),
[ ("v2", Var "x") ],
[ ("v2", Var "y") ] )
(ctr_call ("c", [ Var "x"; Var "x" ]), ctr_call ("c", [ Var "y"; Var "y" ]))
let test_optimize () =
let check ~expected ?f_rules ?g_rules t =
check ~equal:[%derive.eq: string * string list]
~show:[%derive.show: string * string list] ~expected
~actual:(optimize ?f_rules ?g_rules t)
in
let rec num ?(hole = ctr_call ("Z", [])) n =
if n = 0 then hole else ctr_call ("S", [ num ~hole (n - 1) ])
in
let add_rule =
( "add",
[
(("Z", []), [ "y" ], Var "y");
( ("S", [ "x" ]),
[ "y" ],
ctr_call ("S", [ g_call ("add", Var "x", [ Var "y" ]) ]) );
] )
in
(* Standard addition of Church numerals. *)
let g_rules = [ add_rule ] in
(* add(S(Z()), S(S(Z()))) -> S(S(S(Z()))) *)
check ~expected:("S(S(S(Z())))", []) ~g_rules
(g_call ("add", num 1, [ num 2 ]));
(* add(S(S(Z())), b) -> S(S(b)) *)
check ~expected:("S(S(b))", []) ~g_rules (g_call ("add", num 2, [ Var "b" ]));
(* add(a, b) *)
check
~expected:("g0(a, b)", [ "g0(S(v0), b) = S(g0(v0, b))"; "g0(Z(), b) = b" ])
~g_rules
(g_call ("add", Var "a", [ Var "b" ]));
(* Addition with an accumulator. *)
let g_rules =
[
( "addAcc",
[
(("Z", []), [ "y" ], Var "y");
( ("S", [ "x" ]),
[ "y" ],
g_call ("addAcc", Var "x", [ ctr_call ("S", [ Var "y" ]) ]) );
] );
]
in
(* addAcc(S(S(a)), b) *)
check
~expected:
("g0(a, b)", [ "g0(S(v0), b) = g0(v0, S(b))"; "g0(Z(), b) = S(S(b))" ])
~g_rules
(g_call ("addAcc", num ~hole:(Var "a") 2, [ Var "b" ]));
(* addAcc(a, b) *)
check
~expected:("g0(a, b)", [ "g0(S(v0), b) = g0(v0, S(b))"; "g0(Z(), b) = b" ])
~g_rules
(g_call ("addAcc", Var "a", [ Var "b" ]));
(* Multiplication via addition. *)
let g_rules =
[
( "mul",
[
(("Z", []), [ "y" ], ctr_call ("Z", []));
( ("S", [ "x" ]),
[ "y" ],
g_call ("add", g_call ("mul", Var "x", [ Var "y" ]), [ Var "y" ]) );
] );
add_rule;
]
in
(* mul(S(S(Z())), S(S(S(Z())))) -> S(S(S(S(S(S(Z())))))) *)
check
~expected:("S(S(S(S(S(S(Z()))))))", [])
~g_rules
(g_call ("mul", num 2, [ num 3 ]));
(* mul(S(S(a)), b) *)
(* TODO: have a better understanding why this example causes code bloat. *)
check
~expected:
( "g0(a, b)",
[
"f20(v20, v21) = f20(v20, v21)";
"f28(v36, v37) = f28(v36, v37)";
"g0(S(v0), b) = g3(g1(v0, b), b)";
"g0(Z(), b) = g7(b)";
"g1(S(v25), b) = g2(g1(b, v25), b)";
"g1(Z(), b) = b";
"g2(S(v29), v28) = S(g2(v29, v28))";
"g2(Z(), v28) = v28";
"g3(S(v6), v5) = g5(S(g4(v6, v5)), v5)";
"g3(Z(), v5) = g6(v5)";
"g4(S(v13), v5) = S(g4(v5, v13))";
"g4(Z(), v5) = v5";
"g5(S(v10), v9) = S(g5(v10, v9))";
"g5(Z(), v9) = v9";
"g6(S(v16)) = S(f20(v16, S(v16)))";
"g6(Z()) = Z()";
"g7(S(v32)) = S(f28(v32, S(v32)))";
"g7(Z()) = Z()";
] )
~g_rules
(g_call ("mul", num ~hole:(Var "a") 2, [ Var "b" ]));
(* mul(a, b) *)
check
~expected:
( "g0(a, b)",
[
"g0(S(v0), b) = g1(g0(v0, b), b)";
"g0(Z(), b) = Z()";
"g1(S(v4), v3) = S(g1(v4, v3))";
"g1(Z(), v3) = v3";
] )
~g_rules
(g_call ("mul", Var "a", [ Var "b" ]));
(* The [eq] interpreter (to be specialized!). *)
let g_rules =
[
( "eq",
[
(("Z", []), [ "y" ], g_call ("eqZ", Var "y", []));
(("S", [ "x" ]), [ "y" ], g_call ("eqS", Var "y", [ Var "x" ]));
] );
( "eqZ",
[
(("Z", []), [], ctr_call ("True", []));
(("S", [ "x" ]), [], ctr_call ("False", []));
] );
( "eqS",
[
(("Z", []), [ "x" ], ctr_call ("False", []));
(("S", [ "y" ]), [ "x" ], g_call ("eq", Var "x", [ Var "y" ]));
] );
]
in
(* The first Futamura projection: eq(S(S(Z())), x) *)
check
~expected:
( "g0(x)",
[
"g0(S(v0)) = g1(v0)";
"g0(Z()) = False()";
"g1(S(v1)) = g2(v1)";
"g1(Z()) = False()";
"g2(S(v2)) = False()";
"g2(Z()) = True()";
] )
~g_rules
(g_call ("eq", num 2, [ Var "x" ]));
let g_rules = add_rule :: g_rules in
(* Interpretive inversion: eq(add(x, S(S(Z()))), S(S(S(Z())))) *)
check
~expected:
( "g0(x)",
[
"g0(S(v0)) = g2(S(g1(v0)))";
"g0(Z()) = False()";
"g1(S(v11)) = S(g1(v11))";
"g1(Z()) = S(S(Z()))";
"g2(S(v7)) = g3(v7)";
"g2(Z()) = False()";
"g3(S(v8)) = g4(v8)";
"g3(Z()) = False()";
"g4(S(v9)) = g5(v9)";
"g4(Z()) = False()";
"g5(S(v10)) = False()";
"g5(Z()) = True()";
] )
~g_rules
(g_call ("eq", g_call ("add", Var "x", [ num 2 ]), [ num 3 ]));
(* Theorem proving: eq(add(x, Z()), x) *)
(* It is actually hard to call "theorem proving" due to the obscurity of the
solution. A smarter supercompiler would be apt here. *)
check
~expected:
( "g0(x)",
[
"g0(S(v0)) = g2(S(g1(v0)), S(v0))";
"g0(Z()) = True()";
"g1(S(v7)) = S(g1(v7))";
"g1(Z()) = Z()";
"g2(S(v4), v3) = g3(v3, v4)";
"g2(Z(), v3) = g4(v3)";
"g3(S(v5), v4) = g2(v4, v5)";
"g3(Z(), v4) = False()";
"g4(S(v6)) = False()";
"g4(Z()) = True()";
] )
~g_rules
(g_call ("eq", g_call ("add", Var "x", [ num 0 ]), [ Var "x" ]));
(* List mapping. *)
let f_rules = [ ("f", [ "x" ], ctr_call ("Blah", [ Var "x" ])) ] in
let g_rules =
[
( "map",
[
(("Nil", []), [], ctr_call ("Nil", []));
( ("Cons", [ "x"; "xs" ]),
[],
ctr_call
( "Cons",
[ f_call ("f", [ Var "x" ]); g_call ("map", Var "xs", []) ] ) );
] );
]
in
(* Not very interesting: map(map(xs)) *)
(* Our implementation does not perform list fusion because the standard
homeomorphic embedding is too restrictive. The SPSC supercompiler [1]
implements a more flexible approach to HE, thereby allowing it to transform
this example into a one-pass algorithm.
[1]: https://github.com/sergei-romanenko/spsc
*)
check
~expected:
( "g0(xs)",
[
"g0(Cons(v0, v1)) = g2(Cons(Blah(v0), g1(v1)))";
"g0(Nil()) = Nil()";
"g1(Cons(v9, v10)) = Cons(Blah(v9), g1(v10))";
"g1(Nil()) = Nil()";
"g2(Cons(v4, v5)) = Cons(Blah(v4), g2(v5))";
"g2(Nil()) = Nil()";
] )
~f_rules ~g_rules
(g_call ("map", g_call ("map", Var "xs", []), []));
(* An infinite loop of transitions. *)
let f_rules =
[
("f", [ "x" ], f_call ("f'", [ Var "x" ]));
("f'", [ "x" ], f_call ("f''", [ Var "x" ]));
("f''", [ "x" ], f_call ("f", [ Var "x" ]));
]
in
(* f(x) *)
check
~expected:("f0(x)", [ "f0(x) = f0(x)" ])
~f_rules
(f_call ("f", [ Var "x" ]))
let () =
test_subst ();
test_match_against ();
test_reduce ();
test_he ();
test_msg ();
test_optimize ()
let _ = (equal_graph, pp_graph)
Show the `dune` file
(executable
(public_name supercomp)
(name main)
(libraries supercomp)
(preprocess
(pps ppx_deriving.show ppx_deriving.eq))
(instrumentation
(backend bisect_ppx)))
(env
(release
(ocamlopt_flags
(:standard -O3))))
Footnotes
-
Valentin F. Turchin. 1986. The concept of a supercompiler. ACM Trans. Program. Lang. Syst. 8, 3 (July 1986), 292–325. https://doi.org/10.1145/5956.5957 ↩
-
Robert Glück and Morten Heine Sørensen. 1996. A Roadmap to Metacomputation by Supercompilation. In Selected Papers from the International Seminar on Partial Evaluation. Springer-Verlag, Berlin, Heidelberg, 137–160. ↩
I've found a bug in the implementation. To see, add the following lines to
test_optimize
:The trouble is that
f0
is defined as a single-parameter function, but it is called with two parameters inside itself. This happens because the residualizer counts distinct variables in a function's body (to be created), whilef
is originally defined with bothx
andy
. To fix the problem, there needs to be a pass before residualization that inspects every node that will be residualized into a function, and associates this node's identifier with its signature containing all its "real" parameter names 1.I won't be fixing it in this implementation, but if you're interested, my supercompiler Mazeppa doesn't have this problem.
Footnotes
The parameter names can be derived from the binding list of
Bind
. ↩