Skip to content
Matt Mower edited this page Nov 22, 2025 · 6 revisions

Adding a Lua plugin API

The Rez compiler uses a %Compilation{} struct that is passed between a series of phases that transform it. The first phase reads the source, subsequent phases built & manipulate an AST, the final phase writes output.

Rez has a @pragma directive that can trigger custom behaviours. For example @pragma write_hierarchy("hierarchy.ans") writes out a type hierarchy. This is built-in and implemented as an Elixir function.

The idea is to allow writing a custom @pragma as a Lua script that can modify the %Compilation{}.

defstruct status: :ok,
            game: nil,
            options: %{},
            phase: nil,
            source: nil,
            source_path: nil,
            path: nil,
            dist_path: nil,
            cache_path: nil,
            content: [],
            id_map: %{},
            type_map: %{},
            type_hierarchy: TypeHierarchy.new(),
            defaults: %{},
            aliases: %{},
            constants: %{},
            schema: nil,
            pragmas: [],
            progress: [],
            errors: []

At certain points content becomes a list of nodes (e.g. %Rez.AST.Card{}, %Rez.AST.Actor{}, %Rez.AST.Inventory{}, ...) nodes.

So an example would be a pragma (e.g. @pragma annotate_images) to iterate all %Rez.AST.Asset{} content nodes and pre-processes the images to get them to conform to a certain format and annotate with extra metadata.

This would load "pragmas/annotate_images.lua" and pass the compilation into the Lua script, expecting a modified compilation to be returned.

Here's the current pragma.ex:

defmodule Rez.AST.Pragma do
  @moduledoc """
  Specifies the Pragma AST node.
  """
  defstruct status: :ok,
            game_element: false,
            position: {nil, 0, 0},
            name: "",
            built_in: false,
            values: [],
            script: nil,
            metadata: %{}

  alias __MODULE__
  alias Rez.Compiler.Compilation

  @built_ins ["write_hierarchy"]

  use Lua.API

  deflua io_puts(s) do
    IO.puts("Lua: #{inspect(s)}")
  end

  def build(name, values, position) when name in @built_ins do
    {:ok,
     %Pragma{
       position: position,
       name: name,
       values: values,
       built_in: true
     }}
  end

  def build(name, values, position) do
    with {:ok, lua_script} <- File.read("pragmas/#{name}.lua") do
      {:ok,
       %Pragma{
         position: position,
         name: name,
         values: values,
         script: lua_script
       }}
    end
  end

  def run(
        %Pragma{built_in: true, name: "write_hierarchy", values: [file | _]},
        %Compilation{type_hierarchy: type_hierarchy} = compilation
      ) do
    case File.write(file, Apex.Format.format(type_hierarchy)) do
      :ok ->
        compilation

      {:error, errno} ->
        Compilation.add_error(
          compilation,
          "PRAGMA write_hierarchy: cannot write #{inspect(errno)}"
        )
    end
  end

  def run(%Pragma{name: name, values: values, script: script}, compilation) do
    IO.puts("Run #{name}")

    lua =
      Lua.new()
      |> Lua.load_api(__MODULE__)

    {encoded, lua} = Lua.encode!(lua, {:userdata, compilation})
    lua = Lua.set!(lua, [:compilation], encoded)
    lua = Lua.set!(lua, [:values], values)
    {result, lua} = Lua.eval!(lua, script)

    result = Lua.decode!(lua, result)
    # If the script returns the modified compilation, its decoded representation
    # should
    result
  end
end

defimpl Rez.AST.Node, for: Rez.AST.Pragma do
  def node_type(_pragma), do: "pragma"
  def js_ctor(_pragma), do: raise("@pragma does not support a JS constructor!")
  def js_initializer(_pragma), do: raise("@pragma does not support a JS initializer!")
  def process(pragma, _resources), do: pragma
  def html_processor(_pragma, _attr), do: raise("@pragma does not support HTML processing!")
end

Clone this wiki locally