Feel.Backend.Beam.Loader (feel v0.1.0)

Copy Markdown View Source

Compiles generated forms and manages the modules they become.

Generating code has operating costs that interpreting does not, and this module is where they are handled:

  • Atoms. A module name is an atom, and atoms are never freed. Each distinct expression would mint one, so module_name/1 derives the name from a hash of the IR and runtime cache stamp: the same expression on the same stamp gets the same module, and loading it twice is a no-op. New names have a VM-lifetime quota of 50 000, configured with config :feel, :max_generated_modules, count. Unloading and application restarts do not refund it. :infinity disables the quota; atom-table headroom is still checked. Existing names can always be reused.
  • Replacement. The VM keeps at most two versions of a module name, and loading another version kills processes still running the oldest. Names are therefore never reused for different code; a changed expression gets a new name. A module is never replaced: load/2 leaves a loaded module alone, refuses to load over old code that something still runs, and loads of one name are serialized, so two processes that prepare the same expression at once load it once.
  • Unloading. unload/1 retires a module once nothing runs it, by purging it softly and deleting it, and reports when it is still in use. The purge asks every process of the VM, so the time grows with their number; the figures are in the documentation of Feel.Backend.Beam.release/1, and so is the reason that unloading gives back the code of a module and not its atom. Generated code is ephemeral and is rebuilt on upgrade. There is no persisted BEAM-artifact import or runtime compatibility negotiation. Feel.Runtime.abi/0 is only a cache-generation stamp, included in the module-name hash.

Failures are Feel.Error values of kind :backend with codes :compile_failed, :load_failed or :module_in_use. Exhausting the lifetime name quota raises a Feel.Error of kind :limit with code :too_many_generated_modules; the backend returns it as a preparation error.

Summary

Functions

Compiles the forms to a binary and loads it as module.

Returns the module name for an IR: a name under Feel.Generated, derived from a hash of the IR. Creating the atom is deliberate; callers should expect one atom per distinct IR. Raises Feel.Error with code :too_many_generated_modules before interning a new name if the lifetime quota or atom-table headroom is exhausted. Setting :feel, :max_generated_modules to zero permits only existing names.

Unloads a generated module. Returns {:error, :in_use} while a process is still running its code.

Functions

load(forms, module)

@spec load(Feel.Backend.Beam.Emitter.forms(), module()) ::
  {:ok, module()} | {:error, Feel.Error.t()}

Compiles the forms to a binary and loads it as module.

Does nothing and returns {:ok, module} if a module with that name, compiled from the same IR, is already loaded.

The errors have the kind :backend:

  • :compile_failed - the forms are not a valid module.
  • :load_failed - the VM refused the code.
  • :module_in_use - the name is taken by code that was unloaded while a process was still running it; try again when it has finished.

module_name(ir)

@spec module_name(Feel.Backend.ir()) :: module()

Returns the module name for an IR: a name under Feel.Generated, derived from a hash of the IR. Creating the atom is deliberate; callers should expect one atom per distinct IR. Raises Feel.Error with code :too_many_generated_modules before interning a new name if the lifetime quota or atom-table headroom is exhausted. Setting :feel, :max_generated_modules to zero permits only existing names.

The same IR on the same cache stamp always gives the same name, and the name has 128 bits of a SHA-256 hash in it, so different IRs do not share one in practice.

iex> ir = {:const, %{span: {0, 1}, type: :number}, Decimal.new(1)}
iex> name = Feel.Backend.Beam.Loader.module_name(ir)
iex> name == Feel.Backend.Beam.Loader.module_name(ir)
true
iex> String.starts_with?(Atom.to_string(name), "Elixir.Feel.Generated.E")
true

unload(module)

@spec unload(module()) :: :ok | {:error, :in_use}

Unloads a generated module. Returns {:error, :in_use} while a process is still running its code.

It first soft-purges any old code, then deletes the current module and soft-purges that code too. No running process is killed. If either purge finds active code, it returns {:error, :in_use}; depending on the stage, the module can still be available to new calls or await only its final purge. Retry after those calls end. Unloading a module with no loaded or active old code is :ok.