Packages

an elixir interface to the great luerl

Current section

2 Versions

Jump to

Compare versions

15 files changed
+469 additions
-103 deletions
  @@ -1,6 +1,6 @@
1 1 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/iggi42/luex">>}]}.
2 2 {<<"name">>,<<"luex">>}.
3 - {<<"version">>,<<"0.0.1">>}.
3 + {<<"version">>,<<"0.0.2">>}.
4 4 {<<"description">>,<<"an elixir interface to the great luerl">>}.
5 5 {<<"elixir">>,<<"~> 1.17">>}.
6 6 {<<"app">>,<<"luex">>}.
  @@ -8,9 +8,12 @@
8 8 {<<"files">>,
9 9 [<<"lib">>,<<"lib/luex.ex">>,<<"lib/luex">>,<<"lib/luex/functions.ex">>,
10 10 <<"lib/luex/records.ex">>,<<"lib/luex/lua_error.ex">>,
11 - <<"lib/luex/table.ex">>,<<"lib/luex/utils.ex">>,
12 - <<"lib/luex/ext_module.ex">>,<<"lib/luex/userdata.ex">>,<<"mix.exs">>,
13 - <<"README.md">>,<<"LICENSE">>]}.
11 + <<"lib/luex/table.ex">>,<<"lib/luex/call_result.ex">>,<<"lib/luex/async">>,
12 + <<"lib/luex/async/server.ex">>,<<"lib/luex/utils.ex">>,
13 + <<"lib/luex/ext_module.ex">>,<<"lib/luex/table">>,
14 + <<"lib/luex/table/array.ex">>,<<"lib/luex/table/not_an_array.ex">>,
15 + <<"lib/luex/table/recursive_exception.ex">>,<<"lib/luex/async.ex">>,
16 + <<"lib/luex/userdata.ex">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>]}.
14 17 {<<"requirements">>,
15 18 [[{<<"name">>,<<"luerl">>},
16 19 {<<"app">>,<<"luerl">>},
  @@ -9,28 +9,16 @@ defmodule Luex do
9 9 require Luex.LuaError
10 10 require Luex.Records
11 11
12 + alias Luex.CallResult
13 +
14 + @typedoc """
15 + represents a lua call, that returned a value and potentially changed the vm state
16 + """
17 + @type lua_call(result_type) :: %Luex.CallResult{vm: Luex.vm(), return: result_type} # {result_type, vm()}
18 +
12 19 @type vm() :: Luex.Records.luerl()
13 20 defguard is_vm(val) when Luex.Records.is_luerl(val)
14 21
15 - @typedoc """
16 - A keypath describes a list of keys, to navigate nested tables.
17 -
18 - For example `package.path` is a keypath with the elixir representation of `["package", "path"]`.
19 - """
20 - @type keypath() :: [String.t()]
21 -
22 - @typedoc """
23 - This type can representation any lua type.
24 - """
25 - @type lua_value() ::
26 - lua_nil()
27 - | lua_bool()
28 - | lua_string()
29 - | lua_number()
30 - | lua_table()
31 - | lua_userdata()
32 - | lua_fun()
33 -
34 22 @typedoc """
35 23 literal type representation of the lua `nil` value via atom `:nil`
36 24 """
  @@ -59,7 +47,7 @@ defmodule Luex do
59 47 references a table in a lua virtual machine.
60 48 See `Luex.Table` for more information on how to handle this value.
61 49 """
62 - @type lua_table() :: Luex.Table.t()
50 + @type lua_table() :: Luex.Records.tref()
63 51 defguard is_lua_table(v) when Luex.Records.is_tref(v)
64 52
65 53 @typedoc """
  @@ -78,9 +66,22 @@ defmodule Luex do
78 66 when Luex.Records.is_erl_mfa(v) or Luex.Records.is_erl_func(v) or
79 67 Luex.Records.is_lua_func(v)
80 68
69 + # TODO write docs
81 70 @type lua_chunk() :: Luex.Records.erl_func() | Luex.Records.funref()
82 71 defguard is_lua_chunk(v) when Luex.Records.is_erl_func(v) or Luex.Records.is_funref(v)
83 72
73 + @typedoc """
74 + This type can representation any lua type.
75 + """
76 + @type lua_value() ::
77 + lua_nil()
78 + | lua_bool()
79 + | lua_string()
80 + | lua_number()
81 + | lua_table()
82 + | lua_userdata()
83 + | lua_fun()
84 +
84 85 @doc """
85 86 `is_lua_value` checks looks like any representation of a value in a lua vm
86 87 """
  @@ -89,6 +90,13 @@ defmodule Luex do
89 90 is_lua_string(value) or is_lua_number(value) or is_lua_table(value) or
90 91 is_lua_userdata(value) or is_lua_fun(value) or is_lua_chunk(value)
91 92
93 + @typedoc """
94 + A keypath describes a list of keys, to navigate nested tables.
95 +
96 + For example `package.path` is a keypath with the elixir representation of `["package", "path"]`.
97 + """
98 + @type keypath() :: [lua_value()]
99 +
92 100 @typedoc """
93 101 input type for encode/2
94 102 """
  @@ -102,6 +110,9 @@ defmodule Luex do
102 110 | {:userdata, any()}
103 111
104 112 @doc """
113 + Import an elixir value into a luerl vm.
114 + This is function recursively loads lists, keyword lists and maps.
115 +
105 116 This is how elixir values are encoded into lua values, by luerl.
106 117 ```mermaid
107 118 graph LR;
  @@ -127,37 +138,47 @@ defmodule Luex do
127 138 ```
128 139 Tuples are only allowed as keyword lists items and to indicate userdata.
129 140 """
130 - @deprecated "luex will move away from luerl style encoding of values"
141 + # maybe don't deprecate, but rename with emphasis on recursive loading, and only for _input_ into lua, not output (bc rec tables in lua)
142 + # @deprecated "luex will move away from luerl style encoding of values"
131 143 # TODO move the mermaid diagram to somewhere useful ()
132 - @spec encode(vm(), encoding_input()) :: {lua_value(), vm()}
133 - defdelegate encode(vm, encode_me), to: Luerl
144 + @spec encode(vm(), encoding_input()) :: lua_call(lua_value())
145 + def encode(vm, encoding_input) do
146 + vm
147 + |> Luerl.encode(encoding_input)
148 + |> CallResult.from_luerl()
149 + end
134 150
135 151 @type input_value() ::
136 152 nil
137 153 | boolean()
138 154 | number()
139 155 | String.t()
140 - | Luex.Table.input()
156 + | Luex.Table.data()
141 157 | Luex.Functions.input()
142 158 | {:userdata, any()}
143 159
144 160 @doc """
145 161 an attempt at doing Luerl.encode/2 better.
146 162
147 - not sure if having such a function at all is a good idea
148 163 """
149 - @spec load_value(vm(), input_value()) :: {lua_value(), vm()}
164 + @spec load_value(vm(), input_value()) :: lua_call(lua_value())
150 165 # literals
151 - def load_value(vm, nil) when is_vm(vm), do: {nil, vm}
152 - def load_value(vm, true) when is_vm(vm), do: {true, vm}
153 - def load_value(vm, false) when is_vm(vm), do: {false, vm}
154 - def load_value(vm, n) when is_vm(vm) and is_number(n), do: {n, vm}
155 - def load_value(vm, s) when is_vm(vm) and is_binary(s), do: {s, vm}
166 + def load_value(vm, nil) when is_vm(vm), do: %CallResult{return: nil, vm: vm}
167 + def load_value(vm, true) when is_vm(vm), do: %CallResult{return: true, vm: vm}
168 + def load_value(vm, false) when is_vm(vm), do: %CallResult{return: false, vm: vm}
169 + def load_value(vm, n) when is_vm(vm) and is_number(n), do: %CallResult{return: n, vm: vm}
170 + def load_value(vm, s) when is_vm(vm) and is_binary(s), do: %CallResult{return: s, vm: vm}
156 171 # referenced values
157 172 def load_value(vm, m) when is_vm(vm) and is_map(m), do: Luex.Table.new(vm, m)
158 173 def load_value(vm, {:userdata, payload}) when is_vm(vm), do: Luex.Userdata.new(vm, payload)
159 174 def load_value(vm, f) when is_vm(vm) and is_function(f, 2), do: Luex.Functions.new(vm, f)
160 175
176 + @doc """
177 +
178 + """
179 + @spec set_value(lua_call(lua_value()), keypath()) :: vm()
180 + def set_value(%Luex.CallResult{vm: vm, return: r }, keypath), do: set_value(vm, keypath, r)
181 +
161 182 @spec set_value(vm(), keypath(), lua_value()) :: vm()
162 183 def set_value(vm, keypath, value) when is_vm(vm) and is_list(keypath) and is_lua_value(value) do
163 184 Luex.LuaError.wrap do
  @@ -171,13 +192,15 @@ defmodule Luex do
171 192 |> Luerl.set_table1(keypath, value)
172 193 end
173 194
174 - @spec get_value(vm(), keypath()) :: {lua_value(), vm()}
195 + @spec get_value(vm() | lua_call(any()), keypath()) :: lua_call(lua_value())
196 + def get_value(%Luex.CallResult{vm: vm}, keypath), do: get_value(vm, keypath)
175 197 def get_value(vm, keypath) when is_vm(vm) and is_list(keypath) do
176 198 Luex.LuaError.wrap do
177 - get_value1(vm, keypath)
199 + get_value1(vm, keypath) |> CallResult.from_luerl()
178 200 end
179 201 end
180 202
203 + # offer private get_value version without wrapping to prevent unnecessary multiple wrapping
181 204 defp get_value1(vm, keypath), do: Luerl.get_table1(vm, keypath)
182 205
183 206 @doc """
  @@ -186,22 +209,45 @@ defmodule Luex do
186 209 @spec init() :: vm()
187 210 defdelegate init, to: Luerl
188 211
212 + @typedoc """
213 + options
214 + - `:ext_searcher`: list of modules that implement Luex.ExtModule, used to extend the lua require function.
215 + """
216 + @type vm_config() :: {:lua_ext_searcher, [module()]}
217 +
218 + @spec configure(vm(), [vm_config()]) :: vm()
219 + def configure(vm, []), do: vm
220 +
221 + def configure(vm, [{:ext_searcher, mods} | args]) do
222 + whitelist = Enum.map(mods, fn m -> {m.target(), m} end) |> Map.new()
223 +
224 + raw_searcher = fn [query], vm_s when is_lua_string(query) ->
225 + case Map.get(whitelist, query, :not_found) do
226 + :not_found ->
227 + {["no luex module registered for: \"#{query}\""], vm_s}
228 +
229 + ext_module when is_atom(ext_module) ->
230 + raw_loader = Luex.ExtModule.build_loader(ext_module)
231 + %CallResult{return: f, vm: vm_s} = Luex.Functions.new(vm_s, raw_loader)
232 + {[f], vm_s}
233 + end
234 + end
235 +
236 + %CallResult{return: searchers, vm: vm} = Luex.get_value(vm, ["package", "searchers"])
237 + %CallResult{return: epath_searcher, vm: vm} = Luex.Functions.new(vm, raw_searcher)
238 +
239 + vm
240 + |> Luex.Table.Array.append(searchers, epath_searcher)
241 + |> configure(args)
242 + end
243 +
189 244 @doc """
190 245 run a string as lua code in the given vm.
191 -
192 - # Example
193 -
194 -
195 - ```elixir
196 - iex> vm0 = Luex.init()
197 - iex> {[5], _vm1} = Luex.do_inline(vm0, "return 3+2")
198 - ```
199 -
200 246 """
201 - @spec do_inline(vm(), String.t()) :: {[lua_value()], vm()}
247 + @spec do_inline(vm(), String.t()) :: lua_call([lua_value()])
202 248 def do_inline(vm, program) do
203 249 Luex.LuaError.wrap do
204 - Luerl.do(vm, program)
250 + Luerl.do(vm, program) |> CallResult.from_luerl()
205 251 end
206 252 end
207 253
  @@ -212,21 +258,22 @@ defmodule Luex do
212 258
213 259 ```elixir
214 260 iex> vm0 = Luex.init()
215 - iex> {[5], _vm1} = Luex.do_file(vm0, "./test/return_5.lua")
261 + iex> %Luex.CallResult{return: [5]} = Luex.do_file(vm0, "./test/return_5.lua")
216 262 ```
217 263
218 264 """
219 - @spec do_file(vm(), Path.t()) :: {[lua_value()], vm()}
265 + @spec do_file(vm(), Path.t()) :: lua_call([lua_value()])
220 266 def do_file(vm, path) do
221 267 Luex.LuaError.wrap do
222 - Luerl.dofile(vm, to_charlist(path))
268 + Luerl.dofile(vm, to_charlist(path)) |> CallResult.from_luerl()
223 269 end
224 270 end
225 271
226 - @spec do_chunk(vm(), lua_chunk(), [lua_value()]) :: {[lua_value()], vm()}
272 + # TODO write docs
273 + @spec do_chunk(vm(), lua_chunk(), [lua_value()]) :: lua_call([lua_value()])
227 274 def do_chunk(vm, chunk, args \\ []) do
228 275 Luex.LuaError.wrap do
229 - Luerl.call(vm, chunk, args)
276 + Luerl.call(vm, chunk, args) |> CallResult.from_luerl()
230 277 end
231 278 end
  @@ -0,0 +1,18 @@
1 + defmodule Luex.Async do
2 + @typedoc """
3 + # config options
4 + - `:boot_args` : map function of lua vm to configure said vm on server start
5 + """
6 + @type boot_args() :: [config: (Luex.vm() -> Luex.vm())]
7 +
8 + @type t() :: pid()
9 +
10 + @spec start_link(boot_args(), GenServer.options()) :: GenServer.on_start()
11 + def start_link(args, options \\ []) do
12 + GenServer.start_link(__MODULE__.Server, args, options)
13 + end
14 +
15 + @spec do_inline(t(), String.t(), timeout()) :: {:ok, [Luex.lua_value()]}
16 + def do_inline(vm, lua, timeout \\ 5000), do: GenServer.call(vm, {:do_inline, lua}, timeout)
17 +
18 + end
  @@ -0,0 +1,28 @@
1 + defmodule Luex.Async.Server do
2 + @moduledoc false
3 + use GenServer
4 +
5 + defmodule __MODULE__.State do
6 + defstruct [:vm]
7 + end
8 +
9 + alias Luex.CallResult
10 + alias __MODULE__.State, as: S
11 +
12 + # configure
13 + def init(args) do
14 + do_conf = args[:setup] || (& &1)
15 +
16 + Luex.init()
17 + |> then(do_conf)
18 + |> then(&{:ok, %S{vm: &1}})
19 + end
20 +
21 + def handle_call({:do_inline, inline_lua}, _from, state) do
22 + %CallResult{return: results, vm: vm} = Luex.do_inline(state.vm, inline_lua)
23 + response = {:ok, results}
24 + state = %S{ state | vm: vm }
25 + {:reply, response, state}
26 + end
27 +
28 + end
  @@ -0,0 +1,16 @@
1 + defmodule Luex.CallResult do
2 + @moduledoc false
3 + # IDEA STAGE: replace the lua(call) type
4 + # implement enumerable against this
5 + defstruct [:vm, :return]
6 +
7 +
8 + def from_luerl({result, vm}), do: %__MODULE__{vm: vm, return: result}
9 +
10 +
11 + def then_vm(%__MODULE__{vm: vm}, fun), do: fun.(vm)
12 +
13 + # def then_result(%__MODULE__{result: r}, fun), do: fun.(r)
14 + # is a bad idea, because users might throw away a vm state and use an old instead
15 +
16 + end
Loading more files…