lib/forthvm/words/interpreter.ex

defmodule ForthVM.Words.Interpreter do
  @moduledoc """
  Interpreter words
  """

  import ForthVM.Utils

  alias ForthVM.Process
  alias ForthVM.Dictionary
  alias ForthVM.Tokenizer

  # ---------------------------------------------
  # Help
  # ---------------------------------------------

  # documentation for words directly implemented into ForthVM.Process
  @core_words %{
    "if/else/than" =>
      {:word, :core,
       %{
         stack: "( x -- )",
         doc:
           "if x is truthly execute words before than, if falsly and else is specified, execute code before else"
       }}
  }

  @doc """
  dictionary: ( -- ) ( -- ) print list of words in dictionary
  """
  def dictionary(tokens, data_stack, return_stack, dictionary, %{io: %{device: device}} = meta) do
    full_dictionary = Map.merge(@core_words, dictionary)

    IO.write(device, "Dictionary ([w] = word, [v] = variable, [c] = constant):")

    width =
      Enum.max_by(Map.keys(full_dictionary), fn name ->
        String.length(name)
      end)
      |> String.length()

    Map.keys(full_dictionary)
    |> Enum.sort()
    |> Enum.each(fn name ->
      Dictionary.print_word_doc(full_dictionary, device, name, width)
    end)

    Process.next(tokens, data_stack, return_stack, dictionary, meta)
  end

  @doc """
  help: ( -- ) ( -- ) print description of dictionary's word/var/const specified as the next token
  """
  def help(
        [word_name | tokens],
        data_stack,
        return_stack,
        dictionary,
        %{io: %{device: device}} = meta
      ) do
    Dictionary.print_word_doc(dictionary, device, word_name)
    Process.next(tokens, data_stack, return_stack, dictionary, meta)
  end

  # ---------------------------------------------
  # Process exit conditions
  # ---------------------------------------------

  @doc """
  exit: ( -- ) ( -- ) explicit VM termination
  """
  def exit(_tokens, _data_stack, _return_stack, _dictionary, _meta) do
    exit(:normal)
  end

  @doc """
  end: ( -- ) ( R: -- ) explicit process termination
  """
  def _end(_tokens, data_stack, return_stack, dictionary, meta) do
    Process.next([], data_stack, return_stack, dictionary, meta)
  end

  @doc """
  abort: ( i * x -- ) ( R: j * x -- ) empty the data stack and perform the function of QUIT, which includes emptying the return stack, without displaying a message.
  """
  def abort(_tokens, _data_stack, _return_stack, dictionary, meta) do
    Process.next([], [], [], dictionary, meta)
  end

  @doc """
  abort?: ( flag i * x -- ) ( R: j * x -- ) if flag is truthly empty the data stack and perform the function of QUIT, which includes emptying the return stack, displaying a message.
  """
  def abort_msg([message | tokens], [flag | data_stack], return_stack, dictionary, meta) do
    if is_truthly(flag) do
      IO.puts(meta.io.device, message)
      abort(tokens, data_stack, return_stack, dictionary, meta)
    else
      Process.next(tokens, data_stack, return_stack, dictionary, meta)
    end
  end

  # ---------------------------------------------
  # Sleep
  # ---------------------------------------------

  @doc """
  sleep: ( x -- ) sleep for given milliseconds
  """
  def sleep(tokens, [ms | data_stack], return_stack, dictionary, meta) do
    till = System.monotonic_time() + System.convert_time_unit(ms, :millisecond, :native)
    Process.next(tokens, data_stack, return_stack, dictionary, %{meta | sleep: till})
  end

  # ---------------------------------------------
  # Comments
  # ---------------------------------------------

  @doc """
  "(": ( -- ) discard all tokens till ")" is fountd
  """
  def comment(tokens, data_stack, return_stack, dictionary, meta) do
    {_comment_tokens, [")" | tokens]} = Enum.split_while(tokens, fn s -> s != ")" end)

    Process.next(tokens, data_stack, return_stack, dictionary, meta)
  end

  # ---------------------------------------------
  # Word definition
  # ---------------------------------------------

  @doc """
  ":": ( -- ) convert all tokens till ";" is found into a new word
  """
  def create([word_name | tokens], data_stack, return_stack, dictionary, meta) do
    # TODO: implement `is-interpret` and `immediate`
    {word_tokens, [";" | tokens]} = Enum.split_while(tokens, fn s -> s != ";" end)

    Process.next(
      tokens,
      data_stack,
      return_stack,
      Dictionary.add(dictionary, word_name, word_tokens),
      meta
    )
  end

  @doc """
  variable: ( -- ) create a new variable with name from next token
  """
  def variable([word_name | tokens], data_stack, return_stack, dictionary, meta) do
    dictionary =
      case Map.has_key?(dictionary, word_name) do
        true -> dictionary
        false -> Dictionary.add_var(dictionary, word_name, nil)
      end

    Process.next(tokens, data_stack, return_stack, dictionary, meta)
  end

  @doc """
  "!": ( x name -- ) store value in variable
  """
  def set_variable(tokens, [word_name, x | data_stack], return_stack, dictionary, meta) do
    case Map.has_key?(dictionary, word_name) do
      false ->
        raise("can not set unknown variable '#{word_name}' with value '#{inspect(x)}'")

      true ->
        Process.next(
          tokens,
          data_stack,
          return_stack,
          Dictionary.set_var(dictionary, word_name, x),
          meta
        )
    end
  end

  @doc """
  "+!": ( x name -- ) increment variable by given value
  """
  def inc_variable(tokens, [word_name, x | data_stack], return_stack, dictionary, meta) do
    case Map.has_key?(dictionary, word_name) do
      false ->
        raise("can not increment unknown variable '#{word_name}' by '#{inspect(x)}'")

      # FIXME: should handle :undefined ?
      true ->
        Process.next(
          tokens,
          data_stack,
          return_stack,
          Dictionary.set_var(
            dictionary,
            word_name,
            Dictionary.get_var(dictionary, word_name) + x
          ),
          meta
        )
    end
  end

  @doc """
  "@": ( name -- ) get value in variable
  """
  def get_variable(tokens, [word_name | data_stack], return_stack, dictionary, meta) do
    case Map.has_key?(dictionary, word_name) do
      false ->
        raise("can not fetch unknown variable '#{word_name}'")

      true ->
        Process.next(
          tokens,
          [Dictionary.get_var(dictionary, word_name) | data_stack],
          return_stack,
          dictionary,
          meta
        )
    end
  end

  @doc """
  constant: ( x -- ) create a new costant with name from next token and value from data stack
  """
  def constant([word_name | tokens], [x | data_stack], return_stack, dictionary, meta) do
    case Map.has_key?(dictionary, word_name) do
      true ->
        raise(
          "can not set already defined constant '#{word_name}' with new value '#{inspect(x)}'"
        )

      false ->
        Process.next(
          tokens,
          data_stack,
          return_stack,
          Dictionary.add_const(dictionary, word_name, x),
          meta
        )
    end
  end

  @doc """
  include: ( -- ) include program file from filename specified in next token.
  """
  def include([filename | tokens], data_stack, return_stack, dictionary, meta) do
    case File.read(filename) do
      {:ok, source} ->
        Process.next(
          Tokenizer.parse(source) ++ tokens,
          data_stack,
          return_stack,
          dictionary,
          meta
        )

      {:error, file_error} ->
        raise("can not include '#{filename}' because '#{inspect(file_error)}'")
    end
  end

  # ---------------------------------------------
  # Debug
  # ---------------------------------------------

  @doc """
  debug-enable: ( -- ) set process debug flag to true.
  """
  def debug_enable(tokens, data_stack, return_stack, dictionary, meta) do
    Process.next(tokens, data_stack, return_stack, dictionary, %{meta | debug: true})
  end

  @doc """
  debug-disable: ( -- ) set process debug flag to false.
  """
  def debug_disable(tokens, data_stack, return_stack, dictionary, meta) do
    Process.next(tokens, data_stack, return_stack, dictionary, %{meta | debug: false})
  end

  @doc """
  inspect: ( -- ) prints process contex: tokens, data stack, return stack, dictionary, meta.
  """
  def inspec(tokens, data_stack, return_stack, dictionary, meta) do
    io = meta.io.device

    IO.puts(io, "<------------------------------ INSPECT ------------------------------------")
    IO.puts(io, "Remaining instructions:")
    IO.inspect(io, tokens, limit: :infinity)
    IO.puts(io, "Data stack:")
    IO.inspect(io, data_stack, limit: :infinity)
    IO.puts(io, "Return stack:")
    IO.inspect(io, return_stack, limit: :infinity)
    IO.puts(io, "Dictionary:")
    IO.inspect(io, dictionary, limit: :infinity)
    IO.puts(io, "Meta:")
    IO.inspect(io, meta, limit: :infinity)
    IO.puts(io, "<---------------------------------------------------------------------------")

    Process.next(tokens, data_stack, return_stack, dictionary, meta)
  end

  @doc """
  debug-dump-word: ( -- ) prints the definition of the word specified in the next token.
  """
  def debug_dump_word([word_name | tokens], data_stack, return_stack, dictionary, meta) do
    io = meta.io.device

    {type, code, doc} = dump_word(ForthVM.Dictionary.get(dictionary, word_name))

    padding = 16
    IO.puts(io, "<--------------------------------- WORD ------------------------------------")
    IO.inspect(io, word_name, label: String.pad_trailing("< name", padding))
    IO.inspect(io, type, label: String.pad_trailing("< type", padding))

    if is_map(doc) do
      if Map.get(doc, :stack) do
        IO.inspect(io, doc.stack, label: String.pad_trailing("< stack", padding))
      end

      if Map.get(doc, :doc) do
        IO.inspect(io, doc.doc, label: String.pad_trailing("< doc", padding))
      end
    end

    IO.inspect(io, code, label: String.pad_trailing("< def", padding))
    IO.puts(io, "<---------------------------------------------------------------------------")

    Process.next(tokens, data_stack, return_stack, dictionary, meta)
  end

  # ---------------------------------------------
  # PRIVATE
  # ---------------------------------------------

  defp dump_word({:word, function, meta}) when is_function(function) do
    # functions
    {"function", function, Map.get(meta, :doc)}
  end

  defp dump_word({:word, word_tokens, meta}) when is_list(word_tokens) do
    # tokens
    {"word", word_tokens, Map.get(meta, :doc)}
  end

  defp dump_word({:var, value}) do
    # variable
    {"var", value, nil}
  end

  defp dump_word({:const, value}) do
    # constant
    {"const", value, nil}
  end

  defp dump_word({:unknown_word, value}) do
    # unknown
    {"unknown", value, nil}
  end

  defp dump_word(invalid) do
    # invalid: should never happen
    {"unknown", inspect(invalid, limit: :infinity), nil}
  end
end