lib/forthvm.ex

defmodule ForthVM do
  @moduledoc """
  A toy Forth-like virtual machine.

  I have written it to experiment implementing a stack-based preemtive multitasking
  interpreter (and to play) with Elixir.
  """

  @doc """
  Starts a new VM supervisor, initializin `num_cores` cores.

  ## Examples

  ForthVM.start(num_cores: 2)
  {:ok, #PID<0.375.0>}

  """
  def start(num_cores: num_cores) do
    children = [
      {ForthVM.Supervisor, num_cores: num_cores}
    ]

    opts = [strategy: :one_for_one, name: ForthVM]

    Supervisor.start_link(children, opts)
  end

  @doc """
  Returns a map with cores' id as keys and cores' pid as values.

  ## Examples

  ForthVM.Supervisor.cores()
  %{"core_1" => #PID<0.407.0>, "core_2" => #PID<0.408.0>}
  """
  defdelegate cores(), to: ForthVM.Supervisor

  @doc """
  Returns the PID for the Core with the given `core_id` string.

  ## Examples

  ForthVM.core_pid("core_2")
  #PID<0.408.0>
  """
  defdelegate core_pid(core_id), to: ForthVM.Supervisor

  @doc """
  Executes Forth code in `source` string using `process_id` Process
  managed by the `core_id` Core.
  Optionally, a custom Forth `dictionary` can be passed.

  Returns the updated Core state.

  ## Examples

  ForthVM.execute("core_2", "p_1", "40 2 +")
  %ForthVM.Core{
    id: 2,
    io: :stdio,
    processes: [
      %ForthVM.Process{
        context: {[], '*', [],
        %{
          "dup" => {:word, &ForthVM.Words.Stack.dup/5,
            %{doc: "duplicate element from top of stack", stack: "( x -- x x )"}},
          ...
        },
        %{
          core_id: 2,
          debug: false,
          io: %{
            device: :stdio,
            devices: %{"core_io" => :stdio, "stdio" => :stdio}
          },
          messages: [],
          process_id: "p_1",
          reductions: 997,
          sleep: 0
        }},
        core_id: nil,
        exit_value: 42,
        id: "p_1",
        status: :exit
      }
    ]
  }
  """
  defdelegate execute(core_id, process_id, source, dictionary \\ nil), to: ForthVM.Supervisor

  @doc """
  Loads Forth code from the `source` string into `process_id` Process
  managed by `core_id` Core, replacing all code currrently stored into the process.
  The loaded code is executed right away.

  If the `process_id` does not exist, a message will be logged, but no error raised.

  Returns the updated Core state.
  """
  defdelegate load(core_id, process_id, source), to: ForthVM.Supervisor

  @doc """
  Spawns a new process with given `process_id` that will be managed by the `core_id` Core,
  If `process_id` is `nil`, an new id will be automatically generated using `System.unique_integer()`.

  Returns the newly spawned Process' state.

  ## Examples

  ForthVM.spawn("core_2", "p_new")
  %ForthVM.Process{
    context: {[], [], [],
    %{
      "<<" => {:word, &ForthVM.Words.Logic.b_shift_left/5,
        %{doc: "bitwise shift left", stack: "( x y -- v )"}},
      ...
    },
    %{
      core_id: 2,
      debug: false,
      io: %{device: :stdio, devices: %{"core_io" => :stdio, "stdio" => :stdio}},
      messages: [],
      process_id: "p_new",
      reductions: 0,
      sleep: 0
    }},
    core_id: nil,
    exit_value: nil,
    id: "p_new",
    status: nil
  }
  """
  defdelegate spawn(core_id, process_id, dictionary \\ nil), to: ForthVM.Supervisor

  @doc """
  Sends a message to `process_id` Process managed by `core_id` Core:
  `word_name` is the name of the dictionary's word that will handle the message,
  `message_data` is a list containing the data to be placed on top of the data stack.

  The message will place `{word_name, message_data}` into the Process' messages FIFO queue.

  Messages are handled when the Process has no more tokens to process:
  - `word_name` is placed into the list of tokens to execute
  - `message_data` list is joined with the data stack
  - the message is removed from the `messages` queue

  This is a cast call, so nothing is returned.

  ## Examples

  ForthVM.send_message("core_2", "p_new", ".", ["hello world"])
  :ok
  hello world
  """
  defdelegate send_message(core_id, process_id, word_name, message_data), to: ForthVM.Supervisor
end