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 word
SEEreturns a string of the definition of a function)
" +", so as you can see the
+` WORD was UNQUOTEd inline to the definition.
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
.
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.
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.