Rupa.Codegen (rupa v0.1.0)

Copy Markdown View Source

The module backend: Rupa.IR in, the AST of a module out.

Same IR as Rupa.Closure, so a feature is written once and both backends get it. What changes is what the work costs. A closure reads its constants out of an environment and reaches the next node through an indirect call. Generated code has the constants as literals, calls the next node directly, and tests a scalar field's type and bounds inline — so the common case, a field that is present and valid, allocates nothing at all and the {:ok, value} wrapper never gets built.

Three things follow from generating at runtime rather than at build time:

  • a compiled pattern regex is embedded as a literal, because the OTP that compiled it is the OTP about to run it — the version skew that would make that unsafe in a release cannot happen here
  • recursion needs no table: a ref is a direct call to the function generated for that definition, so the one lookup the closure backend does goes away
  • the module is not part of your release. Rupa.compile/2 says what that means.

The generated module is a handle, not an API. Everything reaches it through Rupa.decode/3, and ctx here is just the mode — :halt or :collect.

Summary

Types

Which of the three walks generated a function.

Functions

What a compiler diagnostic about generated code is talking about, in words.

Builds the module AST for a staged program, and the index of which walk wrote what.

Types

direction()

@type direction() :: :decode | :encode | :encode_json

Which of the three walks generated a function.

Functions

attribute(message, index)

@spec attribute(String.t(), %{required(String.t()) => direction()}) :: String.t()

What a compiler diagnostic about generated code is talking about, in words.

Which name a message is about is not worth a grammar: the generated names are d0, e7, jref_node and nothing an ordinary sentence would contain, so the first identifier the index knows is the one being discussed. A message naming none of them is described in general rather than guessed at.

iex> {:ok, program} = Rupa.Stage.run(%{name: Rupa.T.string()})
iex> {_ast, index} = Rupa.Codegen.module(MyApp.Codecs.Named, program, %{})
iex> Rupa.Codegen.attribute("this clause of defp e1/2 is never used", index)
"the encoder it generated"

iex> Rupa.Codegen.attribute("something about nothing in particular", %{})
"the codec it generated"

module(name, program, schema)

@spec module(module(), Rupa.IR.program(), term()) ::
  {Macro.t(), %{required(String.t()) => direction()}}

Builds the module AST for a staged program, and the index of which walk wrote what.

The schema is embedded so the module can say what it was built from, and hashed so a repeat compile under the same name is a lookup rather than a recompile. The index is how Rupa.compile/2 turns a compiler diagnostic about d17/2 back into a sentence: it names every function this module generated, so a message can be told from an ordinary word.

Rupa.compile/2 with as: is the way in; this only builds the AST, and compiling it is the caller's job:

schema = %{name: Rupa.T.string()}
{:ok, program} = Rupa.Stage.run(schema)
{ast, index} = Rupa.Codegen.module(MyApp.Codecs.User, program, schema)
index["d0"]
#=> :decode