SafeScript - designed for embedded scripting in Elixir

As a challenge to myself I’ve been trying to think of ways to do scripting in elixir, preferably with elixir syntax. About the only thing I and others here came up with was interpreting the AST directly, so that is what I did in SafeScript (Not released, unsure if I’d ever consider it stable enough to release), but here are the test cases for Elixir:


  def lang(), do: Elixir


  test "Integer operators" do
    assert {:ok, {_env, 4}} = SafeScript.eval_expressions("2 + 2", lang: lang())
    assert {:ok, {_env, 0}} = SafeScript.eval_expressions("2 - 2", lang: lang())
    assert {:ok, {_env, 4}} = SafeScript.eval_expressions("2 * 2", lang: lang())
    assert {:ok, {_env, 1}} = SafeScript.eval_expressions("2 / 2", lang: lang())
  end


  test "Float operators" do
    assert {:ok, {_env, 4.0}} = SafeScript.eval_expressions("2.0 + 2.0", lang: lang())
    assert {:ok, {_env, 0.0}} = SafeScript.eval_expressions("2.0 - 2.0", lang: lang())
    assert {:ok, {_env, 4.0}} = SafeScript.eval_expressions("2.0 * 2.0", lang: lang())
    assert {:ok, {_env, 1.0}} = SafeScript.eval_expressions("2.0 / 2.0", lang: lang())
  end


  test "Custom functions" do
    assert {:ok, {_env, {:no_call, :a_func, [21]}}} = SafeScript.eval_expressions("a_func(21)", lang: lang())
    externals =
      fn
        (_env, :null_func, []) -> 42 # Return a value straight, or a tuple of:  {env, result}
        (_env, :a_func, [arg]) -> arg > 0
        (env, _, _) -> env # Returning just the `env` is saying that 'there is no call here' and will be alerted as such
      end
    assert {:ok, {_env, 42}} = SafeScript.eval_expressions("null_func()", externals: externals, lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("a_func(21)", externals: externals, lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("a_func(null_func())", externals: externals, lang: lang())
    assert {:ok, {_env, {:no_call, :does_not_exist, []}}} = SafeScript.eval_expressions("does_not_exist()", externals: externals, lang: lang())
  end


  test "Custom operator - <|>" do
    assert {:ok, {_env, {:no_call, :<|>, [2, 2]}}} = SafeScript.eval_expressions("2 <|> 2", lang: lang())
    externals = fn(_env, :<|>, [l, r]) -> l + r end
    assert {:ok, {_env, 4}} = SafeScript.eval_expressions("2 <|> 2", externals: externals, lang: lang())
  end


  test "Module call" do
    assert {:ok, {_env, {:no_call, {[:Testering], :a_func}, [21]}}} = SafeScript.eval_expressions("Testering.a_func(21)", lang: lang())
    assert {:ok, {_env, {:no_call, {[:Testering, :Bloop], :a_func}, [21]}}} = SafeScript.eval_expressions("Testering.Bloop.a_func(21)", lang: lang())
    assert {:ok, {_env, {:no_call, {{:no_call, :testering, nil}, :a_func}, [21]}}} = SafeScript.eval_expressions("testering.a_func(21)", lang: lang())
    externals =
      fn
        (_env, {[:Testering], :a_func}, [arg]) -> arg > 0
        (_env, {[:Testering, :Bloop], :a_func}, [arg]) -> arg > 0
        (_env, :testering, [x]) when is_integer(x) -> x - 1
      end
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("Testering.a_func(21)", externals: externals, lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("Testering.Bloop.a_func(21)", externals: externals, lang: lang())
    assert {:ok, {_env, 20}} = SafeScript.eval_expressions("testering(21)", externals: externals, lang: lang())
  end


  test "String call" do
    assert {:ok, {_env, {:no_call, "blah", [21]}}} = SafeScript.eval_expressions("\"blah\".(21)", lang: lang())
    externals =
      fn
        (_env, "blah", [arg]) -> arg * 2
      end
    assert {:ok, {_env, 42}} = SafeScript.eval_expressions("\"blah\".(21)", externals: externals, lang: lang())
  end


  test "Int call" do
    assert {:ok, {_env, {:no_call, 42, [21]}}} = SafeScript.eval_expressions("42.(21)", lang: lang())
    externals =
      fn
        (_env, 42, [arg]) -> arg * 2
      end
    assert {:ok, {_env, 42}} = SafeScript.eval_expressions("42.(21)", externals: externals, lang: lang())
  end


  test "External Binding Lookup" do
    assert {:ok, {_env, {:no_call, :life, nil}}} = SafeScript.eval_expressions("life", lang: lang())
    externals =
      fn
        (_env, :life, nil) -> 42
        (env, _, _) -> env
      end
    assert {:ok, {_env, 42}} = SafeScript.eval_expressions("life", externals: externals, lang: lang())
  end


  test "External Binding Lookup - call" do
    assert {:ok, {_env, {:no_call, :life, nil}}} = SafeScript.eval_expressions("life", lang: lang())
    externals =
      fn
        (_env, :life, nil) -> 42
        (_env, :testering, nil) -> fn x -> x - 2 end
      end
    assert {:ok, {_env, 42}} = SafeScript.eval_expressions("life", externals: externals, lang: lang())
    assert {:ok, {_env, 19}} = SafeScript.eval_expressions("testering.(21)", externals: externals, lang: lang())
end

And why stop at Elixir, so I made a heavily altered Forth interpreter too, here are it’s tests:


  def lang(), do: ExForth


  test "Parser test - integer" do
    assert {:ok, {:ex_forth, [0]}} = SafeScript.compile_expressions("0", lang: lang())
    assert {:ok, {:ex_forth, [1]}} = SafeScript.compile_expressions("1", lang: lang())
    assert {:ok, {:ex_forth, [-1]}} = SafeScript.compile_expressions("-1", lang: lang())
    assert {:ok, {:ex_forth, [2]}} = SafeScript.compile_expressions("2", lang: lang())
    assert {:ok, {:ex_forth, [-2]}} = SafeScript.compile_expressions("-2", lang: lang())
  end


  test "Parser test - integer - decimal" do
    assert {:ok, {:ex_forth, [0]}} = SafeScript.compile_expressions("0d0", lang: lang())
    assert {:ok, {:ex_forth, [1]}} = SafeScript.compile_expressions("0d1", lang: lang())
    assert {:ok, {:ex_forth, [-1]}} = SafeScript.compile_expressions("-0d1", lang: lang())
    assert {:ok, {:ex_forth, [2]}} = SafeScript.compile_expressions("0d2", lang: lang())
    assert {:ok, {:ex_forth, [-2]}} = SafeScript.compile_expressions("-0d2", lang: lang())
  end


  test "Parser test - integer - hexidecimal" do
    assert {:ok, {:ex_forth, [0]}} = SafeScript.compile_expressions("0x0", lang: lang())
    assert {:ok, {:ex_forth, [1]}} = SafeScript.compile_expressions("0x1", lang: lang())
    assert {:ok, {:ex_forth, [-1]}} = SafeScript.compile_expressions("-0x1", lang: lang())
    assert {:ok, {:ex_forth, [2]}} = SafeScript.compile_expressions("0x2", lang: lang())
    assert {:ok, {:ex_forth, [-2]}} = SafeScript.compile_expressions("-0x2", lang: lang())
    assert {:ok, {:ex_forth, [15]}} = SafeScript.compile_expressions("0xf", lang: lang())
    assert {:ok, {:ex_forth, [-15]}} = SafeScript.compile_expressions("-0xf", lang: lang())
    assert {:ok, {:ex_forth, [255]}} = SafeScript.compile_expressions("0xFf", lang: lang())
    assert {:ok, {:ex_forth, [-255]}} = SafeScript.compile_expressions("-0xFf", lang: lang())
  end


  test "Parser test - integer - octal" do
    assert {:ok, {:ex_forth, [0]}} = SafeScript.compile_expressions("0o0", lang: lang())
    assert {:ok, {:ex_forth, [1]}} = SafeScript.compile_expressions("0o1", lang: lang())
    assert {:ok, {:ex_forth, [-1]}} = SafeScript.compile_expressions("-0o1", lang: lang())
    assert {:ok, {:ex_forth, [2]}} = SafeScript.compile_expressions("0o2", lang: lang())
    assert {:ok, {:ex_forth, [-2]}} = SafeScript.compile_expressions("-0o2", lang: lang())
    assert {:ok, {:ex_forth, [7]}} = SafeScript.compile_expressions("0o7", lang: lang())
    assert {:ok, {:ex_forth, [-7]}} = SafeScript.compile_expressions("-0o7", lang: lang())
    assert {:ok, {:ex_forth, [63]}} = SafeScript.compile_expressions("0o77", lang: lang())
    assert {:ok, {:ex_forth, [-63]}} = SafeScript.compile_expressions("-0o77", lang: lang())
  end


  test "Parser test - integer - binary" do
    assert {:ok, {:ex_forth, [0]}} = SafeScript.compile_expressions("0b0", lang: lang())
    assert {:ok, {:ex_forth, [1]}} = SafeScript.compile_expressions("0b1", lang: lang())
    assert {:ok, {:ex_forth, [-1]}} = SafeScript.compile_expressions("-0b1", lang: lang())
    assert {:ok, {:ex_forth, [2]}} = SafeScript.compile_expressions("0b10", lang: lang())
    assert {:ok, {:ex_forth, [-2]}} = SafeScript.compile_expressions("-0b10", lang: lang())
    assert {:ok, {:ex_forth, [3]}} = SafeScript.compile_expressions("0b11", lang: lang())
    assert {:ok, {:ex_forth, [-3]}} = SafeScript.compile_expressions("-0b11", lang: lang())
    assert {:ok, {:ex_forth, [42]}} = SafeScript.compile_expressions("0b101010", lang: lang())
    assert {:ok, {:ex_forth, [-42]}} = SafeScript.compile_expressions("-0b101010", lang: lang())
  end


  test "Parser test - float" do
    assert {:ok, {:ex_forth, [0.0]}} = SafeScript.compile_expressions("0.0", lang: lang())
    assert {:ok, {:ex_forth, [1.0]}} = SafeScript.compile_expressions("1.0", lang: lang())
    assert {:ok, {:ex_forth, [-1.0]}} = SafeScript.compile_expressions("-1.0", lang: lang())
    assert {:ok, {:ex_forth, [6.28]}} = SafeScript.compile_expressions("6.28", lang: lang())
    assert {:ok, {:ex_forth, [-6.28]}} = SafeScript.compile_expressions("-6.28", lang: lang())
    assert {:ok, {:ex_forth, [628.0]}} = SafeScript.compile_expressions("6.28e2", lang: lang())
    assert {:ok, {:ex_forth, [-628.0]}} = SafeScript.compile_expressions("-6.28e2", lang: lang())
    assert {:ok, {:ex_forth, [0.0628]}} = SafeScript.compile_expressions("6.28e-2", lang: lang())
    assert {:ok, {:ex_forth, [-0.0628]}} = SafeScript.compile_expressions("-6.28e-2", lang: lang())
  end


  test "Parser test - atom" do
    assert {:ok, {:ex_forth, [:ok]}} = SafeScript.compile_expressions(":ok", lang: lang())
    assert {:ok, {:ex_forth, [:ok]}} = SafeScript.compile_expressions(":\"ok\"", lang: lang())
    assert {:ok, {:ex_forth, [:"$ok$"]}} = SafeScript.compile_expressions(":\"$ok$\"", lang: lang())
    assert {:ok, {:ex_forth, [:<|>]}} = SafeScript.compile_expressions(":<|>", lang: lang())
  end


  test "Parser test - atom - nonexisting" do # Atom's that do not exist in the current running system just become words
    assert {:ok, {:ex_forth, [{:word, ":THIS_should_NOT_exist", nil}]}} = SafeScript.compile_expressions(":THIS_should_NOT_exist", lang: lang())
  end


  test "Parser test - external call" do
    assert {:ok, {:ex_forth, [external: {:ok, 2}]}} = SafeScript.compile_expressions("ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions("Module.ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module, :SubModule], :ok}, 2}]}} = SafeScript.compile_expressions("Module.SubModule.ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {:ok, 2}]}} = SafeScript.compile_expressions(":ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions(":Module.ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions("Module.\"ok\"/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions("\"Module\".ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions("\"Module\".\"ok\"/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions(":\"Module\".ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions(":\"Module\".\"ok\"/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {:<|>, 2}]}} = SafeScript.compile_expressions("<|>/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :<|>}, 2}]}} = SafeScript.compile_expressions("Module.<|>/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:<|>], :ok}, 2}]}} = SafeScript.compile_expressions("<|>.ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module, :<|>], :ok}, 2}]}} = SafeScript.compile_expressions("Module.<|>.ok/2", lang: lang())
    # Unknown atoms are left as strings to be passed to the FFI directly
    assert {:ok, {:ex_forth, [external: {"THIS_should_NOT_exist", 2}]}} = SafeScript.compile_expressions("THIS_should_NOT_exist/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{["THIS_should_NOT_exist"], "nor_THIS"}, 2}]}} = SafeScript.compile_expressions("THIS_should_NOT_exist.nor_THIS/2", lang: lang())
  end


  test "Parser test - string" do
    assert {:ok, {:ex_forth, ["Simple String"]}} = SafeScript.compile_expressions("\"Simple String\"", lang: lang())
    assert {:ok, {:ex_forth, ["Tester \n \r \t \s \" \\ done"]}} = SafeScript.compile_expressions("\"Tester \\n \\r \\t \\s \\\" \\\\ done\"", lang: lang())
    assert {:ok, {:ex_forth, ["Test newline \n return \r tab \t space \s"]}} = SafeScript.compile_expressions("\"Test newline \\n return \\r tab \\t space \\s\"", lang: lang())
  end


  test "Parser test - word" do # Everything else just ends up as a word
    assert {:ok, {:ex_forth, [{:word, "everything_else", nil}]}} = SafeScript.compile_expressions("everything_else", lang: lang())
    assert {:ok, {:ex_forth, [{:word, "@{*&()#", nil}]}} = SafeScript.compile_expressions("@{*&()#", lang: lang())
  end


  test "Integers" do
    assert {:ok, {_env, 4}} = SafeScript.eval_expressions("2 2 +", lang: lang())
    assert {:ok, {_env, -5}} = SafeScript.eval_expressions("0 5 -", lang: lang())
    assert {:ok, {_env, 4}} = SafeScript.eval_expressions("2 2 *", lang: lang())
    assert {:ok, {_env, 1}} = SafeScript.eval_expressions("2 2 /", lang: lang())
    assert {:ok, {_env, 21}} = SafeScript.eval_expressions("0 1 2 0b11 0d4 0o5 0x6 + + + + + +", lang: lang())
  end


  test "Floats" do
    assert {:ok, {_env, 4.0}} = SafeScript.eval_expressions("2.0 2.0 +", lang: lang())
    assert {:ok, {_env, -5.0}} = SafeScript.eval_expressions("0.0 5.0 -", lang: lang())
    assert {:ok, {_env, 4.0}} = SafeScript.eval_expressions("2.0 2.0 *", lang: lang())
    assert {:ok, {_env, 1.0}} = SafeScript.eval_expressions("2.0 2.0 /", lang: lang())
  end


  test "Atoms" do
    assert {:ok, {_env, :test}} = SafeScript.eval_expressions(":test", lang: lang())
    assert {:ok, {_env, :Test}} = SafeScript.eval_expressions(":Test", lang: lang())
    assert {:ok, {_env, :<|>}} = SafeScript.eval_expressions(":<|>", lang: lang())
    assert {:ok, {_env, :"32DBAS@{!#&"}} = SafeScript.eval_expressions(":\"32DBAS@{!#&\"", lang: lang())
  end


  test "Strings" do
    assert {:ok, {_env, "Tester"}} = SafeScript.eval_expressions("\"Tester\"", lang: lang())
  end


  test "String Escapes" do
    assert {:ok, {_env, "Tester \n \r \t \s \" \\ done"}} = SafeScript.eval_expressions("\"Tester \\n \\r \\t \\s \\\" \\\\ done\"", lang: lang())
    assert {:ok, {_env, "Test newline \n return \r tab \t space \s"}} = SafeScript.eval_expressions("\"Test newline \\n return \\r tab \\t space \\s\"", lang: lang())
  end


  test "External function" do
    assert {:ok, {_env, [{:no_externals, :add, 2}, 2, 1]}} = SafeScript.eval_expressions("1 2 add/2", lang: lang())
    assert {:ok, {_env, [{:no_externals, {[:Testering], :add}, 2}, 2, 1]}} = SafeScript.eval_expressions("1 2 Testering.add/2", lang: lang())
    assert {:ok, {_env, [{:no_externals, {[:Testering, :Bloop], :add}, 2}, 2, 1]}} = SafeScript.eval_expressions("1 2 Testering.Bloop.add/2", lang: lang())
    externals =
      fn
        (_env, :add, [l, r]) -> l + r
        (_env, {[:Testering], :add}, [l, r]) -> l + r
        (_env, {[:Testering, :Bloop], :add}, [l, r]) -> l + r
      end
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("1 2 add/2", lang: lang(), externals: externals)
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("1 2 Testering.add/2", lang: lang(), externals: externals)
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("1 2 Testering.Bloop.add/2", lang: lang(), externals: externals)
  end


  test "External function - FFI" do
    assert {:ok, {_env, [{:no_externals, :add, 2}, 2, 1]}} = SafeScript.eval_expressions("1 2 add/2", lang: lang())
    assert {:ok, {_env, [{:no_externals, {[:Testering], :add}, 2}, 2, 1]}} = SafeScript.eval_expressions("1 2 Testering.add/2", lang: lang())
    assert {:ok, {_env, [{:no_externals, {[:Testering, :Bloop], :add}, 2}, 2, 1]}} = SafeScript.eval_expressions("1 2 Testering.Bloop.add/2", lang: lang())
    externals =
      fn
        (_env, :add, [l, r]) -> l + r
        (_env, {[:Testering], :add}, [l, r]) -> l + r
        (_env, {[:Testering, :Bloop], :add}, [l, r]) -> l + r
      end
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("1 2 add/2", lang: lang(), externals: externals)
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("1 2 Testering.add/2", lang: lang(), externals: externals)
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("1 2 Testering.Bloop.add/2", lang: lang(), externals: externals)
  end


  test "Comments" do
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("""
      1 ( This is a block comment )
      2 + ( Let's add these together )
    """, lang: lang())
  end


  test "Forth function definition" do
    assert {:ok, {_env, []}} = SafeScript.eval_expressions(": *+ ( a b c -- 'a ) * + ;", lang: lang())
  end


  test "Forth function Forgetting" do
    assert {:ok, {_env, [{:unknown_word, "*+"}, 3, 2, 1]}} = SafeScript.eval_expressions(": *+ ( a b c -- 'a ) * + ; ; *+ 1 2 3 *+", lang: lang())
  end


  test "Forth function accessing prior function name" do
    assert {:ok, {_env, ["( a b c -- 'a ) *+ SEE *+", "( a b c -- 'a ) * +", 7]}} = SafeScript.eval_expressions("""
    : *+ ( a b c -- 'a ) * + ;
    : *+ ( a b c -- 'a ) *+ SEE *+ ;
    1 2 3 *+
    SEE *+
    """, lang: lang())
  end


  test "Forth function definition and call" do
    assert {:ok, {_env, 5}} = SafeScript.eval_expressions(": *+ ( a b c -- 'a ) * + ; 3 2 1 *+", lang: lang())
  end


  test "Forth function definition with unquoting" do
    assert {:ok, {_env, 5}} = SafeScript.eval_expressions("QUOTE + : *+ * `UNQUOTE ; 3 2 1 *+", lang: lang())
    assert {:ok, {_env, "* +"}} = SafeScript.eval_expressions("QUOTE + : *+ * `UNQUOTE ; SEE *+", lang: lang())
  end


  test "Unquoting" do
    assert {:ok, {_env, 2}} = SafeScript.eval_expressions("2 UNQUOTE", lang: lang())
    assert {:ok, {_env, {:word, "+", _}}} = SafeScript.eval_expressions("\"+\" WORD", lang: lang())
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("\"+\" WORD 1 2 ROT UNQUOTE", lang: lang())
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("QUOTE + 1 2 ROT UNQUOTE", lang: lang())
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("QUOTE + : + - ; 2 1 + 2 ROT UNQUOTE", lang: lang())
  end


  test "Quoting" do
    assert {:ok, {_env, {:word, "+", _}}} = SafeScript.eval_expressions("QUOTE +", lang: lang())
    assert {:ok, {_env, {:external, {:add, 2}}}} = SafeScript.eval_expressions("QUOTE add/2", lang: lang())
    assert {:ok, {_env, 2}} = SafeScript.eval_expressions("QUOTE 2", lang: lang())
  end


  test "Forth command - DUP" do
    assert {:ok, {_env, [:no_value, :no_value]}} = SafeScript.eval_expressions("DUP", lang: lang())
    assert {:ok, {_env, [2, 2]}} = SafeScript.eval_expressions("2 DUP", lang: lang())
  end


  test "Forth command - SWAP" do
    assert {:ok, {_env, [:no_value, :no_value]}} = SafeScript.eval_expressions("SWAP", lang: lang())
    assert {:ok, {_env, [2, 1]}} = SafeScript.eval_expressions("2 1 SWAP", lang: lang())
    assert {:ok, {_env, [2, 1, 1, 2]}} = SafeScript.eval_expressions("2 1 2 1 SWAP", lang: lang())
  end


  test "Forth command - OVER" do
    assert {:ok, {_env, [:no_value, :no_value, :no_value]}} = SafeScript.eval_expressions("OVER", lang: lang())
    assert {:ok, {_env, [1, 2, 1]}} = SafeScript.eval_expressions("1 2 OVER", lang: lang())
  end


  test "Forth command - ROT" do
    assert {:ok, {_env, [:no_value, :no_value, :no_value]}} = SafeScript.eval_expressions("ROT", lang: lang())
    assert {:ok, {_env, [1, 3, 2]}} = SafeScript.eval_expressions("1 2 3 ROT ", lang: lang())
  end


  test "Forth command - SEE" do
    assert {:ok, {_env, "( WORD `*+` IS NOT DEFINED )"}} = SafeScript.eval_expressions("SEE *+", lang: lang())
    assert {:ok, {_env, "( NATIVE WORD `SEE` )"}} = SafeScript.eval_expressions("SEE SEE", lang: lang())
    assert {:ok, {_env, "( a b c -- 'a ) * +"}} = SafeScript.eval_expressions(": *+   ( a b c   -- 'a    ) *    + ; SEE *+", lang: lang())
  end


  test "Case sensitivity or insensitivity" do
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions(": add ( a b -- 'a ) + ; 1 2 add", lang: lang())
    assert {:ok, {_env, [{:unknown_word, "add"}, 2, 1]}} = SafeScript.eval_expressions(": AdD ( a b -- 'a ) + ; 1 2 add", lang: lang())
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions(": add ( a b -- 'a ) + ; 1 2 add", lang: lang(), always_uppercase_words: true)
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions(": AdD ( a b -- 'a ) + ; 1 2 add", lang: lang(), always_uppercase_words: true)
    assert {:ok, {_env, "( A B -- 'A ) +"}} = SafeScript.eval_expressions(": add   ( a b   -- 'a    )   + ; SEE add", lang: lang(), always_uppercase_words: true)
  end


  test "List operations" do
    assert {:ok, {_env, []}} = SafeScript.eval_expressions("[]", lang: lang())
    assert {:ok, {_env, [3, 2, 1, 0]}} = SafeScript.eval_expressions("3 2 1 0 [] :: :: :: ::", lang: lang())
    assert {:ok, {_env, [3, 2, 1, 0]}} = SafeScript.eval_expressions("3 2 1 0 [] 4 ::N", lang: lang())
    assert {:ok, {_env, []}} = SafeScript.eval_expressions("0 []N", lang: lang())
    assert {:ok, {_env, [0, 1]}} = SafeScript.eval_expressions("0 1 2 []N", lang: lang())
end

Basically ExForth (as I’m calling it, Elixirized-Forth) supports more types than just integers in its syntax, it also supports integers in multiple formats/bases, floats, strings, atoms, external calls (for compatibility with and library sharing with the Elixir scripting language, you can still define your own external WORDs too of course, for full forth power), and of course normal WORDs for everything that does not fit in to the above. A special note about the external call format, it is basically Atom’s sans : of 1 or more, if more than 1 then they are separated by dots . and have a trailing / then an integer that defines the arity. So something like MyModule.Blah.add/2 will pop the top two items off the stack and call the FFI interface of SafeScript with the module of {{[:MyModule, :Blah}, :add, [the, args]} (it only creates atoms if they already existed, else it passes them as strings) where the and args are the top two items on the stack, and the return value is popped on to the stack.

It also has support for lists via WORDS so you can do things like make an empty list with [] (pushes an empty list on to the stack) and can CONS things with :: (I should probably rename it to CONS, but I like :: for the readability of it) where the top two items on the stack should be a list then an item, used like 2 1 0 [] :: :: :: :: and a helper WORD of ::N that pops the top item on the stack (it is a single stack for all items rather than a stack per ‘type’ like most Forth’s do, since we have so many types in Elixir/Erlang) that should be a positive integer and it pops that many items off the stack to make in to a list like 3 2 1 0 [] 4 ::N works on 4 items after the list item, so the top of the stack is the number of items to CONS on, the next is the list, then it pops an element, CONS it to the list, then repeats until it has used all the items, so the above makes the Elixir list of [0, 1, 2, 3]. Instead of always putting in the empty list you can use the ‘list constructor’ WORD of []N too so 0 []N is the empty list and 0 1 2 []N takes the top value of 2, then pops the next 2 things off to make a list, so it makes the Elixir list of [0, 1], you can basically think of []N as being implemented like : []N [] SWAP ::[] ;. I plan to make more later as well, including a way to map/reduce over lists and so forth as well, and the traditional way to do that in forth is via a stack mutating loop with the DO and LOOP and such words, but this ExForth has an extra feature too, first-class functions via a quoting/unquoting system. I tend to dislike commands that read the next token(s) instead of just reading the stack, so I’m trying to minimize these, so I have a QUOTE and UNQUOTE word to replace most of those abilities. QUOTE <token> takes whatever <token> is an pushes the token to the stack. UNQUOTE just pops a token off the stack (validating it since the user could of course craft their own) and just calls that token next as if calling UNQUOTE was calling that token. So doing QUOTE + 1 2 ROT UNQUOTE will return 3, as it quotes the call to the + WORD, pushes 1 and 2 to the stack, rotates the quote to the top then unquotes it, thus calling it, which calls 1 2 +. UNQUOTE is just a normal word so while in a functional declaration context (yes this is a different setup than Forth, I wanted to do something unique) then a special WORD called \UNQUOTE (without the`, it is just `UNQUOTE but Discourse’s markdown is a bit borked), it will UNQUOTE at function definition time instead of when the function is running, so you can do something like QUOTE + : *+ *UNQUOTE ; SEE +, which returns (the wordSEEreturns a string of the definition of a function)" +", so as you can see the+` WORD was UNQUOTEd inline to the definition. :slight_smile:

Also, a call remembers its definition at the time it was Quoted, so if you do QUOTE + : + - ; 2 1 + 2 ROT UNQUOTE, which quotes the word + to the top of the stack, then defines the word + to actually mean - (whoo!), then calls 2 1 +, which actually does 2-1 to return 1, then push a 2 to the stack above that 1, rotate the quote to the top and unquote it then calls 1 2 + with the original definition of the WORD +, which then returns 3. :slight_smile:

What this all means is that you can do everything from passing a function to a function (first class functions!) by just quoting it to the stack then unquoting it inside the function to call it, but you can even dynamically build up and construct entire functions from scratch. :slight_smile:

I plan more base constructors like for tuples and dictionaries and such too.

And of course users can define their own languages to fit in the same interface as well.

But overall, this was a fun little project. :slight_smile:

6 Likes

Added some basic tuples and maps functions too, still more to make. Current tests for the ExForth part:

  def lang(), do: ExForth


  test "Parser test - integer" do
    assert {:ok, {:ex_forth, [0]}} = SafeScript.compile_expressions("0", lang: lang())
    assert {:ok, {:ex_forth, [1]}} = SafeScript.compile_expressions("1", lang: lang())
    assert {:ok, {:ex_forth, [-1]}} = SafeScript.compile_expressions("-1", lang: lang())
    assert {:ok, {:ex_forth, [2]}} = SafeScript.compile_expressions("2", lang: lang())
    assert {:ok, {:ex_forth, [-2]}} = SafeScript.compile_expressions("-2", lang: lang())
  end


  test "Parser test - integer - decimal" do
    assert {:ok, {:ex_forth, [0]}} = SafeScript.compile_expressions("0d0", lang: lang())
    assert {:ok, {:ex_forth, [1]}} = SafeScript.compile_expressions("0d1", lang: lang())
    assert {:ok, {:ex_forth, [-1]}} = SafeScript.compile_expressions("-0d1", lang: lang())
    assert {:ok, {:ex_forth, [2]}} = SafeScript.compile_expressions("0d2", lang: lang())
    assert {:ok, {:ex_forth, [-2]}} = SafeScript.compile_expressions("-0d2", lang: lang())
  end


  test "Parser test - integer - hexidecimal" do
    assert {:ok, {:ex_forth, [0]}} = SafeScript.compile_expressions("0x0", lang: lang())
    assert {:ok, {:ex_forth, [1]}} = SafeScript.compile_expressions("0x1", lang: lang())
    assert {:ok, {:ex_forth, [-1]}} = SafeScript.compile_expressions("-0x1", lang: lang())
    assert {:ok, {:ex_forth, [2]}} = SafeScript.compile_expressions("0x2", lang: lang())
    assert {:ok, {:ex_forth, [-2]}} = SafeScript.compile_expressions("-0x2", lang: lang())
    assert {:ok, {:ex_forth, [15]}} = SafeScript.compile_expressions("0xf", lang: lang())
    assert {:ok, {:ex_forth, [-15]}} = SafeScript.compile_expressions("-0xf", lang: lang())
    assert {:ok, {:ex_forth, [255]}} = SafeScript.compile_expressions("0xFf", lang: lang())
    assert {:ok, {:ex_forth, [-255]}} = SafeScript.compile_expressions("-0xFf", lang: lang())
  end


  test "Parser test - integer - octal" do
    assert {:ok, {:ex_forth, [0]}} = SafeScript.compile_expressions("0o0", lang: lang())
    assert {:ok, {:ex_forth, [1]}} = SafeScript.compile_expressions("0o1", lang: lang())
    assert {:ok, {:ex_forth, [-1]}} = SafeScript.compile_expressions("-0o1", lang: lang())
    assert {:ok, {:ex_forth, [2]}} = SafeScript.compile_expressions("0o2", lang: lang())
    assert {:ok, {:ex_forth, [-2]}} = SafeScript.compile_expressions("-0o2", lang: lang())
    assert {:ok, {:ex_forth, [7]}} = SafeScript.compile_expressions("0o7", lang: lang())
    assert {:ok, {:ex_forth, [-7]}} = SafeScript.compile_expressions("-0o7", lang: lang())
    assert {:ok, {:ex_forth, [63]}} = SafeScript.compile_expressions("0o77", lang: lang())
    assert {:ok, {:ex_forth, [-63]}} = SafeScript.compile_expressions("-0o77", lang: lang())
  end


  test "Parser test - integer - binary" do
    assert {:ok, {:ex_forth, [0]}} = SafeScript.compile_expressions("0b0", lang: lang())
    assert {:ok, {:ex_forth, [1]}} = SafeScript.compile_expressions("0b1", lang: lang())
    assert {:ok, {:ex_forth, [-1]}} = SafeScript.compile_expressions("-0b1", lang: lang())
    assert {:ok, {:ex_forth, [2]}} = SafeScript.compile_expressions("0b10", lang: lang())
    assert {:ok, {:ex_forth, [-2]}} = SafeScript.compile_expressions("-0b10", lang: lang())
    assert {:ok, {:ex_forth, [3]}} = SafeScript.compile_expressions("0b11", lang: lang())
    assert {:ok, {:ex_forth, [-3]}} = SafeScript.compile_expressions("-0b11", lang: lang())
    assert {:ok, {:ex_forth, [42]}} = SafeScript.compile_expressions("0b101010", lang: lang())
    assert {:ok, {:ex_forth, [-42]}} = SafeScript.compile_expressions("-0b101010", lang: lang())
  end


  test "Parser test - float" do
    assert {:ok, {:ex_forth, [0.0]}} = SafeScript.compile_expressions("0.0", lang: lang())
    assert {:ok, {:ex_forth, [1.0]}} = SafeScript.compile_expressions("1.0", lang: lang())
    assert {:ok, {:ex_forth, [-1.0]}} = SafeScript.compile_expressions("-1.0", lang: lang())
    assert {:ok, {:ex_forth, [6.28]}} = SafeScript.compile_expressions("6.28", lang: lang())
    assert {:ok, {:ex_forth, [-6.28]}} = SafeScript.compile_expressions("-6.28", lang: lang())
    assert {:ok, {:ex_forth, [628.0]}} = SafeScript.compile_expressions("6.28e2", lang: lang())
    assert {:ok, {:ex_forth, [-628.0]}} = SafeScript.compile_expressions("-6.28e2", lang: lang())
    assert {:ok, {:ex_forth, [0.0628]}} = SafeScript.compile_expressions("6.28e-2", lang: lang())
    assert {:ok, {:ex_forth, [-0.0628]}} = SafeScript.compile_expressions("-6.28e-2", lang: lang())
  end


  test "Parser test - atom" do
    assert {:ok, {:ex_forth, [:ok]}} = SafeScript.compile_expressions(":ok", lang: lang())
    assert {:ok, {:ex_forth, [:ok]}} = SafeScript.compile_expressions(":\"ok\"", lang: lang())
    assert {:ok, {:ex_forth, [:"$ok$"]}} = SafeScript.compile_expressions(":\"$ok$\"", lang: lang())
    assert {:ok, {:ex_forth, [:<|>]}} = SafeScript.compile_expressions(":<|>", lang: lang())
  end


  test "Parser test - atom - nonexisting" do # Atom's that do not exist in the current running system just become words
    assert {:ok, {:ex_forth, [{:word, ":THIS_should_NOT_exist", nil}]}} = SafeScript.compile_expressions(":THIS_should_NOT_exist", lang: lang())
  end


  test "Parser test - external call" do
    assert {:ok, {:ex_forth, [external: {:ok, 2}]}} = SafeScript.compile_expressions("ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions("Module.ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module, :SubModule], :ok}, 2}]}} = SafeScript.compile_expressions("Module.SubModule.ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {:ok, 2}]}} = SafeScript.compile_expressions(":ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions(":Module.ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions("Module.\"ok\"/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions("\"Module\".ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions("\"Module\".\"ok\"/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions(":\"Module\".ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :ok}, 2}]}} = SafeScript.compile_expressions(":\"Module\".\"ok\"/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {:<|>, 2}]}} = SafeScript.compile_expressions("<|>/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module], :<|>}, 2}]}} = SafeScript.compile_expressions("Module.<|>/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:<|>], :ok}, 2}]}} = SafeScript.compile_expressions("<|>.ok/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{[:Module, :<|>], :ok}, 2}]}} = SafeScript.compile_expressions("Module.<|>.ok/2", lang: lang())
    # Unknown atoms are left as strings to be passed to the FFI directly
    assert {:ok, {:ex_forth, [external: {"THIS_should_NOT_exist", 2}]}} = SafeScript.compile_expressions("THIS_should_NOT_exist/2", lang: lang())
    assert {:ok, {:ex_forth, [external: {{["THIS_should_NOT_exist"], "nor_THIS"}, 2}]}} = SafeScript.compile_expressions("THIS_should_NOT_exist.nor_THIS/2", lang: lang())
  end


  test "Parser test - string" do
    assert {:ok, {:ex_forth, ["Simple String"]}} = SafeScript.compile_expressions("\"Simple String\"", lang: lang())
    assert {:ok, {:ex_forth, ["Tester \n \r \t \s \" \\ done"]}} = SafeScript.compile_expressions("\"Tester \\n \\r \\t \\s \\\" \\\\ done\"", lang: lang())
    assert {:ok, {:ex_forth, ["Test newline \n return \r tab \t space \s"]}} = SafeScript.compile_expressions("\"Test newline \\n return \\r tab \\t space \\s\"", lang: lang())
  end


  test "Parser test - word" do # Everything else just ends up as a word
    assert {:ok, {:ex_forth, [{:word, "everything_else", nil}]}} = SafeScript.compile_expressions("everything_else", lang: lang())
    assert {:ok, {:ex_forth, [{:word, "@{*&()#", nil}]}} = SafeScript.compile_expressions("@{*&()#", lang: lang())
  end


  test "Integers" do
    assert {:ok, {_env, 4}} = SafeScript.eval_expressions("2 2 +", lang: lang())
    assert {:ok, {_env, -5}} = SafeScript.eval_expressions("0 5 -", lang: lang())
    assert {:ok, {_env, 4}} = SafeScript.eval_expressions("2 2 *", lang: lang())
    assert {:ok, {_env, 1}} = SafeScript.eval_expressions("2 2 /", lang: lang())
    assert {:ok, {_env, 21}} = SafeScript.eval_expressions("0 1 2 0b11 0d4 0o5 0x6 + + + + + +", lang: lang())
  end


  test "Floats" do
    assert {:ok, {_env, 4.0}} = SafeScript.eval_expressions("2.0 2.0 +", lang: lang())
    assert {:ok, {_env, -5.0}} = SafeScript.eval_expressions("0.0 5.0 -", lang: lang())
    assert {:ok, {_env, 4.0}} = SafeScript.eval_expressions("2.0 2.0 *", lang: lang())
    assert {:ok, {_env, 1.0}} = SafeScript.eval_expressions("2.0 2.0 /", lang: lang())
  end


  test "Atoms" do
    assert {:ok, {_env, :test}} = SafeScript.eval_expressions(":test", lang: lang())
    assert {:ok, {_env, :Test}} = SafeScript.eval_expressions(":Test", lang: lang())
    assert {:ok, {_env, :<|>}} = SafeScript.eval_expressions(":<|>", lang: lang())
    assert {:ok, {_env, :"32DBAS@{!#&"}} = SafeScript.eval_expressions(":\"32DBAS@{!#&\"", lang: lang())
  end


  test "Strings" do
    assert {:ok, {_env, "Tester"}} = SafeScript.eval_expressions("\"Tester\"", lang: lang())
  end


  test "String Escapes" do
    assert {:ok, {_env, "Tester \n \r \t \s \" \\ done"}} = SafeScript.eval_expressions("\"Tester \\n \\r \\t \\s \\\" \\\\ done\"", lang: lang())
    assert {:ok, {_env, "Test newline \n return \r tab \t space \s"}} = SafeScript.eval_expressions("\"Test newline \\n return \\r tab \\t space \\s\"", lang: lang())
  end


  test "External function" do
    assert {:ok, {_env, [{:no_externals, :add, 2}, 2, 1]}} = SafeScript.eval_expressions("1 2 add/2", lang: lang())
    assert {:ok, {_env, [{:no_externals, {[:Testering], :add}, 2}, 2, 1]}} = SafeScript.eval_expressions("1 2 Testering.add/2", lang: lang())
    assert {:ok, {_env, [{:no_externals, {[:Testering, :Bloop], :add}, 2}, 2, 1]}} = SafeScript.eval_expressions("1 2 Testering.Bloop.add/2", lang: lang())
    externals =
      fn
        (_env, :add, [l, r]) -> l + r
        (_env, {[:Testering], :add}, [l, r]) -> l + r
        (_env, {[:Testering, :Bloop], :add}, [l, r]) -> l + r
      end
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("1 2 add/2", lang: lang(), externals: externals)
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("1 2 Testering.add/2", lang: lang(), externals: externals)
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("1 2 Testering.Bloop.add/2", lang: lang(), externals: externals)
  end


  test "External function - FFI" do
    assert {:ok, {_env, [{:no_externals, :add, 2}, 2, 1]}} = SafeScript.eval_expressions("1 2 add/2", lang: lang())
    assert {:ok, {_env, [{:no_externals, {[:Testering], :add}, 2}, 2, 1]}} = SafeScript.eval_expressions("1 2 Testering.add/2", lang: lang())
    assert {:ok, {_env, [{:no_externals, {[:Testering, :Bloop], :add}, 2}, 2, 1]}} = SafeScript.eval_expressions("1 2 Testering.Bloop.add/2", lang: lang())
    externals =
      fn
        (_env, :add, [l, r]) -> l + r
        (_env, {[:Testering], :add}, [l, r]) -> l + r
        (_env, {[:Testering, :Bloop], :add}, [l, r]) -> l + r
      end
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("1 2 add/2", lang: lang(), externals: externals)
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("1 2 Testering.add/2", lang: lang(), externals: externals)
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("1 2 Testering.Bloop.add/2", lang: lang(), externals: externals)
  end


  test "Comments" do
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("""
      1 ( This is a block comment )
      2 + ( Let's add these together )
    """, lang: lang())
  end


  test "Forth function definition" do
    assert {:ok, {_env, []}} = SafeScript.eval_expressions(": *+ ( a b c -- 'a ) * + ;", lang: lang())
  end


  test "Forth function Forgetting" do
    assert {:ok, {_env, [{:unknown_word, "*+"}, 3, 2, 1]}} = SafeScript.eval_expressions(": *+ ( a b c -- 'a ) * + ; ; *+ 1 2 3 *+", lang: lang())
  end


  test "Forth function accessing prior function name" do
    assert {:ok, {_env, ["( a b c -- 'a ) *+ SEE *+", "( a b c -- 'a ) * +", 7]}} = SafeScript.eval_expressions("""
    : *+ ( a b c -- 'a ) * + ;
    : *+ ( a b c -- 'a ) *+ SEE *+ ;
    1 2 3 *+
    SEE *+
    """, lang: lang())
  end


  test "Forth function definition and call" do
    assert {:ok, {_env, 5}} = SafeScript.eval_expressions(": *+ ( a b c -- 'a ) * + ; 3 2 1 *+", lang: lang())
  end


  test "Forth function definition with unquoting" do
    assert {:ok, {_env, 5}} = SafeScript.eval_expressions("QUOTE + : *+ * `UNQUOTE ; 3 2 1 *+", lang: lang())
    assert {:ok, {_env, "* +"}} = SafeScript.eval_expressions("QUOTE + : *+ * `UNQUOTE ; SEE *+", lang: lang())
  end


  test "Unquoting" do
    assert {:ok, {_env, 2}} = SafeScript.eval_expressions("2 UNQUOTE", lang: lang())
    assert {:ok, {_env, {:word, "+", _}}} = SafeScript.eval_expressions("\"+\" WORD", lang: lang())
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("\"+\" WORD 1 2 ROT UNQUOTE", lang: lang())
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("QUOTE + 1 2 ROT UNQUOTE", lang: lang())
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("QUOTE + : + - ; 2 1 + 2 ROT UNQUOTE", lang: lang())
  end


  test "Quoting" do
    assert {:ok, {_env, {:word, "+", _}}} = SafeScript.eval_expressions("QUOTE +", lang: lang())
    assert {:ok, {_env, {:external, {:add, 2}}}} = SafeScript.eval_expressions("QUOTE add/2", lang: lang())
    assert {:ok, {_env, 2}} = SafeScript.eval_expressions("QUOTE 2", lang: lang())
  end


  test "Forth command - DUP" do
    assert {:ok, {_env, [nil, nil]}} = SafeScript.eval_expressions("DUP", lang: lang())
    assert {:ok, {_env, [2, 2]}} = SafeScript.eval_expressions("2 DUP", lang: lang())
  end


  test "Forth command - SWAP" do
    assert {:ok, {_env, [nil, nil]}} = SafeScript.eval_expressions("SWAP", lang: lang())
    assert {:ok, {_env, [2, 1]}} = SafeScript.eval_expressions("2 1 SWAP", lang: lang())
    assert {:ok, {_env, [2, 1, 1, 2]}} = SafeScript.eval_expressions("2 1 2 1 SWAP", lang: lang())
  end


  test "Forth command - OVER" do
    assert {:ok, {_env, [nil, nil, nil]}} = SafeScript.eval_expressions("OVER", lang: lang())
    assert {:ok, {_env, [1, 2, 1]}} = SafeScript.eval_expressions("1 2 OVER", lang: lang())
  end


  test "Forth command - ROT" do
    assert {:ok, {_env, [nil, nil, nil]}} = SafeScript.eval_expressions("ROT", lang: lang())
    assert {:ok, {_env, [1, 3, 2]}} = SafeScript.eval_expressions("1 2 3 ROT ", lang: lang())
  end


  test "Forth command - SEE" do
    assert {:ok, {_env, "( WORD `*+` IS NOT DEFINED )"}} = SafeScript.eval_expressions("SEE *+", lang: lang())
    assert {:ok, {_env, "( NATIVE WORD `SEE` )"}} = SafeScript.eval_expressions("SEE SEE", lang: lang())
    assert {:ok, {_env, "( a b c -- 'a ) * +"}} = SafeScript.eval_expressions(": *+   ( a b c   -- 'a    ) *    + ; SEE *+", lang: lang())
  end


  test "Forth command - <tests>" do
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("TRUE", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("FALSE", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("2 10 <", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("10 2 <", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("10 2 >", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("2 10 >", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("2 10 <=", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("10 2 <=", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("2 2 <=", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("10 2 >=", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("2 10 >=", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("2 2 >=", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("TRUE !", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("FALSE !", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("FALSE FALSE AND", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("FALSE TRUE AND", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("TRUE FALSE AND", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("TRUE TRUE AND", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("FALSE FALSE OR", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("FALSE TRUE OR", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("TRUE FALSE OR", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("TRUE TRUE OR", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("FALSE 42 &&", lang: lang())
    assert {:ok, {_env, 21}} = SafeScript.eval_expressions("TRUE 21 &&", lang: lang())
    assert {:ok, {_env, 21}} = SafeScript.eval_expressions("FALSE 21 ||", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("TRUE 21 ||", lang: lang())
  end


  test "Forth command - IF" do
    # assert {:ok, {_env, [3, 1, 0]}} = SafeScript.eval_expressions("0 TRUE IF 1 THEN 3", lang: lang())
    # assert {:ok, {_env, [3, 0]}} = SafeScript.eval_expressions("0 FALSE IF 1 THEN 3", lang: lang())
    # assert {:ok, {_env, [3, 1, 0]}} = SafeScript.eval_expressions("0 TRUE IF 1 ELSE 2 THEN 3", lang: lang())
    # assert {:ok, {_env, [3, 2, 0]}} = SafeScript.eval_expressions("0 FALSE IF 1 ELSE 2 THEN 3", lang: lang())
    assert {:ok, {_env, 2}} = SafeScript.eval_expressions("TRUE IF FALSE IF 1 ELSE 2 THEN THEN", lang: lang())
    assert {:ok, {_env, 1}} = SafeScript.eval_expressions("TRUE IF TRUE IF 1 ELSE 2 THEN THEN", lang: lang())
    assert {:ok, {_env, []}} = SafeScript.eval_expressions("FALSE IF TRUE IF 1 ELSE 2 THEN THEN", lang: lang())
    assert {:ok, {_env, 2}} = SafeScript.eval_expressions("TRUE IF FALSE IF 1 ELSE 2 THEN ELSE FALSE IF 3 ELSE 4 THEN THEN", lang: lang())
    assert {:ok, {_env, 1}} = SafeScript.eval_expressions("TRUE IF TRUE IF 1 ELSE 2 THEN ELSE FALSE IF 3 ELSE 4 THEN THEN", lang: lang())
    assert {:ok, {_env, 4}} = SafeScript.eval_expressions("FALSE IF TRUE IF 1 ELSE 2 THEN ELSE FALSE IF 3 ELSE 4 THEN THEN", lang: lang())
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions("FALSE IF TRUE IF 1 ELSE 2 THEN ELSE TRUE IF 3 ELSE 4 THEN THEN", lang: lang())
  end


  test "Type tests" do
    assert {:ok, {_env, true}} = SafeScript.eval_expressions(":test IS_ATOM", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("21 IS_ATOM", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("\"test\" IS_BINARY", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("21 IS_BINARY", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("\"test\" IS_BITSTRING", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("21 IS_BITSTRING", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("TRUE IS_BOOLEAN", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("21 IS_BOOLEAN", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("6.28 IS_FLOAT", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("21 IS_FLOAT", lang: lang())
    # assert {:ok, {_env, true}} = SafeScript.eval_expressions("IS_FUNCTION", lang: lang())
    # assert {:ok, {_env, false}} = SafeScript.eval_expressions("IS_FUNCTION", lang: lang())
    # assert {:ok, {_env, true}} = SafeScript.eval_expressions("IS_FUNCTION", lang: lang())
    # assert {:ok, {_env, false}} = SafeScript.eval_expressions("IS_FUNCTION", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("21 IS_INTEGER", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions(":test IS_INTEGER", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("[] IS_LIST", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("21 IS_LIST", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("%{} IS_MAP", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("21 IS_MAP", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("21 IS_NUMBER", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions(":test IS_NUMBER", lang: lang())
    # assert {:ok, {_env, true}} = SafeScript.eval_expressions("IS_PID", lang: lang())
    # assert {:ok, {_env, false}} = SafeScript.eval_expressions("IS_PID", lang: lang())
    # assert {:ok, {_env, true}} = SafeScript.eval_expressions("IS_PORT", lang: lang())
    # assert {:ok, {_env, false}} = SafeScript.eval_expressions("IS_PORT", lang: lang())
    # assert {:ok, {_env, true}} = SafeScript.eval_expressions("IS_REFERENCE", lang: lang())
    # assert {:ok, {_env, false}} = SafeScript.eval_expressions("IS_REFERENCE", lang: lang())
    assert {:ok, {_env, true}} = SafeScript.eval_expressions("{} IS_TUPLE", lang: lang())
    assert {:ok, {_env, false}} = SafeScript.eval_expressions("21 IS_TUPLE", lang: lang())
  end


  test "Case sensitivity or insensitivity" do
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions(": add ( a b -- 'a ) + ; 1 2 add", lang: lang())
    assert {:ok, {_env, [{:unknown_word, "add"}, 2, 1]}} = SafeScript.eval_expressions(": AdD ( a b -- 'a ) + ; 1 2 add", lang: lang())
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions(": add ( a b -- 'a ) + ; 1 2 add", lang: lang(), always_uppercase_words: true)
    assert {:ok, {_env, 3}} = SafeScript.eval_expressions(": AdD ( a b -- 'a ) + ; 1 2 add", lang: lang(), always_uppercase_words: true)
    assert {:ok, {_env, "( A B -- 'A ) +"}} = SafeScript.eval_expressions(": add   ( a b   -- 'a    )   + ; SEE add", lang: lang(), always_uppercase_words: true)
  end


  test "List operations" do
    assert {:ok, {_env, []}} = SafeScript.eval_expressions("[]", lang: lang())
    assert {:ok, {_env, [3, 2, 1, 0]}} = SafeScript.eval_expressions("3 2 1 0 [] :: :: :: ::", lang: lang())
    assert {:ok, {_env, [3, 2, 1, 0]}} = SafeScript.eval_expressions("3 2 1 0 [] 4 ::N", lang: lang())
    assert {:ok, {_env, []}} = SafeScript.eval_expressions("0 []N", lang: lang())
    assert {:ok, {_env, [0, 1]}} = SafeScript.eval_expressions("0 1 2 []N", lang: lang())
  end


  test "Map operations" do
    assert {:ok, {_env, %{}}} = SafeScript.eval_expressions("%{}", lang: lang())
    assert {:ok, {_env, %{key: :value}}} = SafeScript.eval_expressions(":key :value %{} =>", lang: lang())
    assert {:ok, {_env, %{1 => 2, 3 => 4, 5 => 6, 7 => 8}}} = SafeScript.eval_expressions("1 2 3 4 %{} => => 5 6 ROT => 7 8 ROT =>", lang: lang())
    assert {:ok, {_env, 4}} = SafeScript.eval_expressions("1 2 3 4 %{} => => 5 6 ROT => 3 GET_KEY", lang: lang())
  end


  test "Tuple operations" do
    assert {:ok, {_env, {}}} = SafeScript.eval_expressions("{}", lang: lang())
    assert {:ok, {_env, {}}} = SafeScript.eval_expressions("0 {}N", lang: lang())
    assert {:ok, {_env, {0, 1}}} = SafeScript.eval_expressions("0 1 2 {}N", lang: lang())
    assert {:ok, {_env, 0}} = SafeScript.eval_expressions("0 1 2 {}N 0 ELEM", lang: lang())
    assert {:ok, {_env, 1}} = SafeScript.eval_expressions("0 1 2 {}N 1 ELEM", lang: lang())
    assert {:ok, {_env, [0, 1]}} = SafeScript.eval_expressions("0 1 2 {}N ELEMS->LIST", lang: lang())
    assert {:ok, {_env, {0, 1}}} = SafeScript.eval_expressions("0 1 2 []N LIST->ELEMS", lang: lang())
    assert {:ok, {_env, 2}} = SafeScript.eval_expressions("0 1 2 {}N #ELEMS", lang: lang())
  end
2 Likes

This is really great! Keep going…

1 Like

This is NICE :smirk:

It works similarly to an interpreter right?

Why not? this is so coool…

It is an interpreter in full (not necessarily an ‘efficient’ one by any stretch as I’m more used to generating machine code, but that can always be optimized later if I really care). The elixir one just uses the elixir compiler to make the AST (safely, no atom creation unless you override it), then stores that and you pass that to the interpreter to ‘execute’ it. The ExForth one is similar but it tokanizes the stream on the ‘load’ step and ‘executes’ the stream on the interpreter call. It is a really really simple interpreter, I have far far more complex ones that i’ve written elsewhere, but I’m trying to keep these limited and ‘safe’, so almost everything devolves into an external call that you can handle, which keeps it really simple. ^.^;

It’s really just a playground for simple scripting, if others started using it and contributing PR’s then sure, it would get published, but until then I’ll just keep using it myself for my own little playground things and until it is published I routinely make API-breaking changes (once published then I follow semver though).

1 Like