Lockstep.ErlangRewriter (Lockstep v0.1.0)

Copy Markdown View Source

Rewrites Erlang source (.erl) so that vanilla OTP calls (gen_server:call, erlang:spawn, Pid ! Msg, bare receive) go through Lockstep's controller. Mirrors Lockstep.Rewriter but on Erlang's abstract format instead of Elixir's macro AST.

Why

Most of the BEAM ecosystem's foundational libraries are pure Erlang: :pg, gen_stage, gen_statem, :dets, sleeplocks, the Erlang internals of :gen_server, :supervisor, etc. Without an Erlang rewriter, libraries that depend on these (Phoenix.PubSub, GenStage, Cachex's transaction layer, libcluster, ...) fall outside Lockstep. This module is the bridge.

Mappings

vanilla Erlang                    rewritten
-------                           ---------
gen_server:call(S, M)             'Elixir.Lockstep.GenServer':call(S, M)
gen_server:cast(S, M)             'Elixir.Lockstep.GenServer':cast(S, M)
gen_server:start_link(M, A, O)    'Elixir.Lockstep.GenServer':start_link(M, A, O)
gen_server:reply(F, R)            'Elixir.Lockstep.GenServer':reply(F, R)
gen_server:stop(S, R, T)          'Elixir.Lockstep.GenServer':stop(S, R, T)
erlang:spawn(F)                   'Elixir.Lockstep':spawn(F)
erlang:spawn_link(F)              'Elixir.Lockstep':spawn_link(F)
erlang:send(D, M)                 'Elixir.Lockstep':send(D, M)
erlang:monitor(process, P)        'Elixir.Lockstep':monitor(P)
erlang:demonitor(R)               'Elixir.Lockstep':demonitor(R, [])
erlang:demonitor(R, O)            'Elixir.Lockstep':demonitor(R, O)
erlang:link(P)                    'Elixir.Lockstep':link(P)
erlang:unlink(P)                  'Elixir.Lockstep':unlink(P)
erlang:process_flag(F, V)         'Elixir.Lockstep':flag(F, V)
erlang:is_process_alive(P)        'Elixir.Lockstep':'alive?'(P)
erlang:send_after(T, D, M)        'Elixir.Lockstep':send_after(D, M, T)   ** arg reorder! **
erlang:cancel_timer(R)            'Elixir.Lockstep':cancel_timer(R)
Pid ! Msg                         'Elixir.Lockstep':send(Pid, Msg)
receive Cls end                   case 'Elixir.Lockstep':recv_first(matcher) of Cls end
receive Cls after T -> Body end   timer + recv_first dispatch (see below)

Also handles bare BIFs (spawn(F) without erlang: prefix), which are the auto-imported BIFs from the Erlang module.

Limitations (v0.1)

  • gen_server:start_link({local, Name}, M, A, O) 4-arg form not yet supported. Use the 3-arg form (no name) and pass the pid around explicitly.
  • Pre-bound variables in receive patterns lose their "pin" semantics (the matcher does structural match only). Most code doesn't rely on this; use guards explicitly if you do.
  • gen_statem, supervisor, :pg are not yet wrapper-rewritten individually. They could be added by extending the call mappings.

Summary

Functions

Rewrite a .erl file's forms and compile directly to a .beam binary. Skips the .erl round-trip. Returns {:ok, module, binary} or {:error, errors, warnings}.

Read .erl file, parse, rewrite, write back to output_path. Returns {:ok, output_path} on success.

Walk a list of Erlang forms and rewrite. Returns the new forms.

Functions

rewrite_and_compile(input_path, opts \\ [])

@spec rewrite_and_compile(
  Path.t(),
  keyword()
) :: {:ok, atom(), binary()} | {:error, list(), list()}

Rewrite a .erl file's forms and compile directly to a .beam binary. Skips the .erl round-trip. Returns {:ok, module, binary} or {:error, errors, warnings}.

rewrite_file(input_path, output_path, opts \\ [])

@spec rewrite_file(Path.t(), Path.t(), keyword()) ::
  {:ok, Path.t()} | {:error, term()}

Read .erl file, parse, rewrite, write back to output_path. Returns {:ok, output_path} on success.

rewrite_forms(forms)

@spec rewrite_forms([tuple()]) :: [tuple()]

Walk a list of Erlang forms and rewrite. Returns the new forms.