Current section

Files

Jump to
lua lib lua vm stdlib.ex
Raw

lib/lua/vm/stdlib.ex

defmodule Lua.VM.Stdlib do
@moduledoc """
Standard library functions for the Lua VM.
Provides essential built-in functions that are available in the global
scope when the standard library is installed.
"""
alias Lua.VM.ArgumentError
alias Lua.VM.AssertionError
alias Lua.VM.Executor
alias Lua.VM.Limits
alias Lua.VM.Numeric
alias Lua.VM.ProtectedCall
alias Lua.VM.RuntimeError
alias Lua.VM.State
alias Lua.VM.Table
alias Lua.VM.TypeError
alias Lua.VM.Value
@doc """
Installs the standard library into the given VM state.
"""
@spec install(State.t()) :: State.t()
def install(%State{} = state) do
state
|> State.register_function("type", &lua_type/2)
|> State.register_function("tostring", &lua_tostring/2)
|> State.register_function("tonumber", &lua_tonumber/2)
|> State.register_function("print", &lua_print/2)
|> State.register_function("error", &lua_error/2)
|> State.register_function("assert", &lua_assert/2)
|> State.register_function("pcall", &lua_pcall/2)
|> State.register_function("xpcall", &lua_xpcall/2)
|> State.register_function("rawget", &lua_rawget/2)
|> State.register_function("rawset", &lua_rawset/2)
|> State.register_function("rawlen", &lua_rawlen/2)
|> State.register_function("rawequal", &lua_rawequal/2)
|> State.register_function("next", &lua_next/2)
|> State.register_function("pairs", &lua_pairs/2)
|> State.register_function("ipairs", &lua_ipairs/2)
|> State.register_function("setmetatable", &lua_setmetatable/2)
|> State.register_function("getmetatable", &lua_getmetatable/2)
|> State.register_function("select", &lua_select/2)
|> State.register_function("load", &lua_load/2)
|> State.register_function("require", &lua_require/2)
|> State.register_function("collectgarbage", &lua_collectgarbage/2)
|> State.register_function("dofile", &lua_dofile/2)
|> State.set_global("_VERSION", "Lua 5.3")
|> install_package_table()
|> install_library(Lua.VM.Stdlib.String)
|> install_library(Lua.VM.Stdlib.Math)
|> install_library(Lua.VM.Stdlib.Table)
|> install_library(Lua.VM.Stdlib.Utf8)
|> install_library(Lua.VM.Stdlib.Os)
|> install_library(Lua.VM.Stdlib.Debug)
|> preload_stdlib_modules()
|> install_unpack_alias()
|> install_global_g()
end
# Install a stdlib library module and register it in package.loaded
defp install_library(state, module) do
state = module.install(state)
name = module.lib_name()
case State.get_global(state, name) do
{:tref, _} = tref -> cache_module_result(state, name, tref)
_ -> state
end
end
# Install `_G` and `_ENV` globals.
#
# As of Plan A16 (Lua 5.3 `_ENV` semantics), the `_G` table itself is the
# storage for globals — its `data` map is what `State.set_global/3` and
# `State.get_global/2` read and write. The `_G` table is allocated by
# `State.new/0` and its tref is stored in `state.g_ref`.
#
# Here we just expose `_G` to Lua code under the names `_G` and `_ENV`.
# Top-level chunks see `_ENV` as a register holding `_G`; user code can
# reassign `_ENV` to redirect global access without affecting `_G` itself.
defp install_global_g(state) do
g_ref = State.g_ref(state)
# Expose _G to Lua under the name "_G"
state = State.set_global(state, "_G", g_ref)
# _ENV is also exposed at boot for backwards compatibility with code that
# references `_ENV` as a global. The compiler binds `_ENV` as a chunk-level
# local (register 0) at execute time, so this global is mostly for
# introspection; user-level `_ENV` reassignment goes to that local, not
# this global.
State.set_global(state, "_ENV", g_ref)
end
# Pre-load any stdlib table globals into package.loaded so that
# require("string"), require("math"), etc. resolve to the existing global
# tables without triggering a filesystem search. This mirrors Lua 5.3's
# behaviour where package.loaded is pre-populated by the runtime.
#
# install_library/2 already caches the four installed modules (string, math,
# table, debug), so this pass is a safety net for any future stdlib tables
# that may be added as globals before this call.
defp preload_stdlib_modules(state) do
modules = ["string", "math", "table", "utf8", "debug"]
Enum.reduce(modules, state, fn name, acc ->
case State.get_global(acc, name) do
{:tref, _} = tref -> cache_module_result(acc, name, tref)
_ -> acc
end
end)
end
# type(v) — returns the type of v as a string
defp lua_type([value | _], state), do: {[Value.type_name(value)], state}
defp lua_type([], _state), do: raise(ArgumentError.value_expected("type", 1))
# tostring(v) — converts a value to its string representation
defp lua_tostring([value | _], state) do
{str, state} = value_to_string_with_mt(value, state)
{[str], state}
end
defp lua_tostring([], state), do: {["nil"], state}
# tonumber(v [, base]) — converts a string to a number
defp lua_tonumber([value | rest], state) do
base = List.first(rest)
result =
case {value, base} do
{v, nil} when is_number(v) ->
v
{v, nil} when is_binary(v) ->
Value.parse_number(v)
{v, b} when is_binary(v) and is_integer(b) and b >= 2 and b <= 36 ->
parse_in_base(v, b)
_ ->
nil
end
{[result], state}
end
defp lua_tonumber([], state), do: {[nil], state}
# tonumber(s, base) per Lua 5.3 §6.1: surrounding whitespace is ignored, an
# optional sign is allowed, digits a-z/A-Z map to 10-35 (case-insensitive),
# and the result is always an integer (wrapping into the signed 64-bit range
# like an integer literal). The whole trimmed string must be valid digits.
defp parse_in_base(str, base) do
{sign, body} =
case String.trim(str) do
"-" <> rest -> {-1, rest}
"+" <> rest -> {1, rest}
other -> {1, other}
end
case Integer.parse(body, base) do
{n, ""} when body != "" -> Numeric.to_signed_int64(sign * n)
_ -> nil
end
end
# print(...) — prints values separated by tabs, followed by a newline
defp lua_print(args, state) do
{strings, state} =
Enum.reduce(args, {[], state}, fn val, {acc, st} ->
{str, st} = value_to_string_with_mt(val, st)
{[str | acc], st}
end)
output = strings |> Enum.reverse() |> Enum.join("\t")
IO.puts(output)
{[], state}
end
# error(message [, level]) — raises a Lua runtime error.
# The executor stashes the calling source position in the process dict
# before invoking native callbacks (see `Lua.VM.Executor.execute/5`), so
# this raise picks up `at <source>:<line>:` automatically. If the function
# is called outside a Lua execution (or from a context where no line was
# set), `current_position/0` returns `{nil, nil}` and the message just
# omits the location.
#
# Per Lua 5.3 §6.1 the raised value passes through pcall/xpcall verbatim,
# and string messages gain a `source:line:` prefix unless `level == 0`.
# The prefixed view lives in `:lua_value` (Lua-facing only); `:value`
# stays raw so host rendering is untouched. Levels >= 2 (attribute to the
# caller's caller) need per-frame lines the call stack does not carry and
# are treated as level 1 for now.
defp lua_error([message | rest], state) do
{line, source} = Executor.current_position()
level = error_level(rest)
raise RuntimeError,
value: message,
lua_value: position_prefixed(message, level, line, source),
line: line,
source: source,
state: state
end
defp lua_error([], state) do
{line, source} = Executor.current_position()
raise RuntimeError, value: nil, line: line, source: source, state: state
end
defp error_level([level | _]) when is_number(level), do: level
defp error_level(_), do: 1
# §6.1 position prefix: only string messages, only when `level ~= 0`, and
# only when the executor recorded a usable position. A `{nil, _}` position
# (native call inside a compiled closure — the dispatcher does not plumb
# per-call lines yet) omits the prefix rather than emit a wrong line.
defp position_prefixed(message, level, line, source)
when is_binary(message) and level != 0 and is_integer(line) and is_binary(source) do
"#{source}:#{line}: #{message}"
end
defp position_prefixed(message, _level, _line, _source), do: message
# assert(v [, message]) — raises if v is falsy
defp lua_assert([value | rest], state) do
if value == nil or value == false do
message =
case rest do
[msg | _] -> msg
[] -> "assertion failed!"
end
{line, source} = Executor.current_position()
raise AssertionError, value: message, line: line, source: source, state: state
else
{[value | rest], state}
end
end
defp lua_assert([], state) do
{line, source} = Executor.current_position()
raise AssertionError, value: "assertion failed!", line: line, source: source, state: state
end
# pcall(f [, arg1, ...]) — calls function in protected mode
# Returns true, result(s) on success or false, error_message on error.
# The trapped error unwinds control state only: heap effects the callee
# made before raising are kept via the exception's `:state` snapshot
# (Lua 5.3 §2.3) — see `State.unwind_to/2`.
defp lua_pcall([func | args], state) do
{results, state} = Executor.call_function(func, args, state)
{[true | results], state}
rescue
e in [RuntimeError, AssertionError, TypeError, ArgumentError] ->
{[false, ProtectedCall.error_value(e)], State.unwind_to(state, e.state)}
e ->
# Catch any other error
{[false, Exception.message(e)], State.unwind_to(state, raised_state(e))}
end
defp lua_pcall([], state), do: {[false, "bad argument #1 to 'pcall' (value expected)"], state}
# xpcall(f, msgh [, arg1, ...]) — calls function with message handler
# Returns true, result(s) on success or false, handler_result on error.
# Like pcall, heap effects made before the error are kept; the message
# handler runs against that recovered state so it observes them
# (reference Lua invokes the handler before the stack unwinds, with the
# heap intact).
defp lua_xpcall([func, handler | args], state) do
{results, state} = Executor.call_function(func, args, state)
{[true | results], state}
rescue
e in [RuntimeError, AssertionError, TypeError, ArgumentError] ->
run_xpcall_handler(handler, ProtectedCall.error_value(e), State.unwind_to(state, e.state))
e ->
# Catch any other error
run_xpcall_handler(handler, Exception.message(e), State.unwind_to(state, raised_state(e)))
end
defp lua_xpcall(_, state), do: {[false, "bad argument to 'xpcall'"], state}
# If the handler itself fails, return the original error — keeping any
# heap effects the handler made before its own error.
defp run_xpcall_handler(handler, error_msg, state) do
{handler_results, state} = Executor.call_function(handler, [error_msg], state)
{[false | handler_results], state}
rescue
e ->
{[false, error_msg], State.unwind_to(state, raised_state(e))}
end
# Raise-time state ferried out on VM exceptions; `nil` for anything else
# (plain Elixir exceptions carry no Lua state).
defp raised_state(%{state: %State{} = state}), do: state
defp raised_state(_), do: nil
# rawget(table, key) — get without metamethods
defp lua_rawget([{:tref, id}, key | _], state) do
table = Map.fetch!(state.tables, id)
{[Table.get(table, key)], state}
end
# rawset(table, key, value) — set without metamethods
defp lua_rawset([{:tref, _} = tref, key, value | _], state) do
state = State.update_table(state, tref, fn table -> Table.put(table, key, value) end)
{[tref], state}
end
# rawlen(v) — length without metamethods.
#
# Lua 5.3 reference: rawlen requires a table or string argument and
# raises "table or string expected" otherwise. The previous
# implementation silently returned 0 for non-table, non-string
# values, which broke `pcall(rawlen, ...)` patterns in events.lua.
defp lua_rawlen([{:tref, id} | _], state) do
table = Map.fetch!(state.tables, id)
{[Table.length(table)], state}
end
defp lua_rawlen([v | _], state) when is_binary(v) do
{[byte_size(v)], state}
end
defp lua_rawlen([], _state) do
raise ArgumentError.value_expected("rawlen", 1)
end
defp lua_rawlen([v | _], _state) do
raise ArgumentError,
function_name: "rawlen",
arg_num: 1,
expected: "table or string",
got: Value.type_name(v)
end
# rawequal(a, b) — equality without metamethods
defp lua_rawequal([a, b | _], state) do
{[a === b], state}
end
defp lua_rawequal(_, state), do: {[false], state}
# next(table [, key]) — returns next key-value pair after key.
# Per Lua 5.3 §6.1, when `key` is non-nil and was never a key in the
# table, raises "invalid key to 'next'". Dead keys (cleared during
# iteration) are treated as still-iterable so `for k,v in pairs(t)`
# loops that nil out their keys mid-traversal complete cleanly.
defp lua_next([{:tref, id} | rest], state) do
key = List.first(rest)
table = Map.fetch!(state.tables, id)
# Eagerly flush any deferred-append `order_tail` and build the O(1)
# iteration memo so subsequent instruction_count are index lookups rather than
# linear scans. The first call to `lua_next` for a given iteration
# pays the cost once; the rest see a clean `order` and a live memo.
{table, state} =
if table.order_tail == [] and table.order_index != nil do
{table, state}
else
flushed = Table.flush_order(table)
{flushed, %{state | tables: Map.put(state.tables, id, flushed)}}
end
case Table.next_entry(table, key) do
:invalid_key ->
raise ArgumentError,
function_name: "next",
arg_num: 2,
details: "invalid key to 'next'",
state: state
nil ->
{[nil, nil], state}
{k, v} ->
{[k, v], state}
end
end
defp lua_next(_, state), do: {[nil, nil], state}
# pairs(table) — returns next, table, nil
defp lua_pairs([{:tref, _} = tref | _], state) do
next_func = State.get_global(state, "next")
{[next_func, tref, nil], state}
end
defp lua_pairs([], _state), do: raise(ArgumentError.value_expected("pairs", 1))
defp lua_pairs([v | _], _state), do: raise(ArgumentError.type_error("pairs", 1, "table", Value.type_name(v)))
# ipairs(table) — returns iterator function, table, 0
defp lua_ipairs([{:tref, _} = tref | _], state) do
# Per Lua 5.3 §6.1, ipairs reads through the indexing operator, so
# __index is honoured. Iteration stops at the first nil.
iterator =
{:native_func,
fn [table_ref, index], state ->
i = index + 1
case Executor.table_index(table_ref, i, state) do
{nil, state} -> {[nil], state}
{v, state} -> {[i, v], state}
end
end}
{[iterator, tref, 0], state}
end
defp lua_ipairs([], _state), do: raise(ArgumentError.value_expected("ipairs", 1))
defp lua_ipairs([v | _], _state), do: raise(ArgumentError.type_error("ipairs", 1, "table", Value.type_name(v)))
# select(index, ...) — returns arguments starting from index
# select('#', ...) — returns count of arguments
defp lua_select(["#" | args], state) do
{[length(args)], state}
end
defp lua_select([index | args], state) when is_integer(index) do
cond do
# Positive index: return from that position onward
index > 0 ->
selected = Enum.drop(args, index - 1)
{selected, state}
# Negative index: count from end (take last n elements)
index < 0 ->
selected = Enum.take(args, index)
{selected, state}
# Index 0 is invalid
true ->
raise RuntimeError, value: "bad argument #1 to 'select' (index out of range)"
end
end
defp lua_select([non_valid | _], _state) do
raise ArgumentError,
function_name: "select",
arg_num: 1,
expected: "number or '#'",
got: Value.type_name(non_valid)
end
defp lua_select([], _state) do
raise ArgumentError.value_expected("select", 1)
end
# load(chunk, chunkname, mode, env) — loads a Lua chunk
# `chunk` may be either a string or a reader function. A reader function
# is called repeatedly; each call must return a string piece, and the
# chunk ends when it returns nil, an empty string, or no value.
# Returns the compiled function, or (nil, error message) on failure.
defp lua_load([chunk | rest], state) when is_binary(chunk) do
compile_loaded_chunk(chunk, load_env_arg(rest, state), state)
end
defp lua_load([{:lua_closure, _, _} = reader | rest], state) do
load_from_reader(reader, load_env_arg(rest, state), state)
end
defp lua_load([{:compiled_closure, _, _} = reader | rest], state) do
load_from_reader(reader, load_env_arg(rest, state), state)
end
defp lua_load([{:native_func, _} = reader | rest], state) do
load_from_reader(reader, load_env_arg(rest, state), state)
end
defp lua_load([_other | _], state) do
# Lua 5.3 reference: "If chunk is not a string, load also accepts a
# function value." Anything else is rejected.
{[nil, "bad argument #1 to 'load' (string or function expected)"], state}
end
defp lua_load([], _state) do
raise ArgumentError.value_expected("load", 1)
end
# Calls the reader function in a loop, accumulating string pieces until
# it signals end-of-chunk (nil or ""). Bails out with `(nil, error_msg)`
# if the reader ever returns a non-string non-nil value, mirroring the
# behavior of Lua 5.3's reference implementation.
defp load_from_reader(reader, env, state) do
case collect_reader_chunks(reader, state, [], 0) do
{:ok, source, state} ->
compile_loaded_chunk(source, env, state)
{:error, msg, state} ->
{[nil, msg], state}
end
end
# The optional 4th argument to `load(chunk, chunkname, mode, env)` is the
# environment the loaded chunk sees as `_ENV`. When absent or nil it defaults
# to the current global table `_G` (Lua 5.3 §6.1).
defp load_env_arg([_chunkname, _mode, env | _], _state) when env != nil, do: env
defp load_env_arg(_rest, state), do: State.g_ref(state)
defp collect_reader_chunks(reader, state, acc, size) do
{results, state} = Executor.call_function(reader, [], state)
case results do
[] ->
{:ok, IO.iodata_to_binary(Enum.reverse(acc)), state}
[nil | _] ->
{:ok, IO.iodata_to_binary(Enum.reverse(acc)), state}
["" | _] ->
{:ok, IO.iodata_to_binary(Enum.reverse(acc)), state}
[piece | _] when is_binary(piece) ->
# A reader that never signals end-of-input would otherwise accumulate
# chunks until the host runs out of memory. Bound the total source
# size with the same ceiling used for string allocations. Note this
# raises rather than returning load's documented `(nil, message)`
# pair: an oversized reader is a DoS attempt, not a recoverable
# syntax error, so we surface it as a catchable runtime error
# (caught by `pcall`) instead of a quiet load failure.
Limits.check_string_size!(size + byte_size(piece), state.max_string_bytes)
collect_reader_chunks(reader, state, [piece | acc], size + byte_size(piece))
[bad | _] ->
{:error, "reader function must return a string (got #{Value.type_name(bad)})", state}
end
end
defp compile_loaded_chunk(source, env, state) do
case Lua.Parser.parse(source) do
{:ok, ast} ->
# Compiler currently never returns errors, always succeeds — see
# `Lua.Compiler.compile!/2` for the matching note.
{:ok, prototype} = Lua.Compiler.compile(ast)
# A loaded chunk's sole upvalue is `_ENV`. Back it with a real cell
# holding `env` so `load_env` sources it (instead of `_G`), the chunk's
# global writes route through `env`, and `debug.getupvalue`/`setupvalue`
# can read and mutate it.
env_cell = make_ref()
state = %{state | upvalue_cells: Map.put(state.upvalue_cells, env_cell, env)}
# When the bytecode compiler accepts the loaded chunk, surface
# it as a `:compiled_closure` so the dispatcher takes over.
# Otherwise fall back to the standard interpreted closure.
closure =
case prototype.bytecode do
nil -> {:lua_closure, prototype, {env_cell}}
_ -> {:compiled_closure, prototype, {env_cell}}
end
{[closure], state}
{:error, reason} ->
error_msg = format_parse_error(reason)
{[nil, error_msg], state}
end
end
defp format_parse_error(error) when is_binary(error), do: error
# setmetatable(table, metatable) — sets the metatable for a table
defp lua_setmetatable([{:tref, _} = tref, metatable], state) do
# Check for __metatable protection on existing metatable
table = State.get_table(state, tref)
case table.metatable do
{:tref, mt_id} ->
mt = Map.fetch!(state.tables, mt_id)
if Map.has_key?(mt.data, "__metatable") do
raise RuntimeError, value: "cannot change a protected metatable"
end
_ ->
:ok
end
# Validate metatable is nil or a table
case metatable do
nil ->
state = State.update_table(state, tref, fn table -> %{table | metatable: nil} end)
{[tref], state}
{:tref, _} = mt_ref ->
state = State.update_table(state, tref, fn table -> %{table | metatable: mt_ref} end)
{[tref], state}
_ ->
raise ArgumentError,
function_name: "setmetatable",
arg_num: 2,
expected: "nil or table"
end
end
defp lua_setmetatable([non_table | _], _state) do
raise ArgumentError,
function_name: "setmetatable",
arg_num: 1,
expected: "table",
got: Value.type_name(non_table)
end
defp lua_setmetatable([], _state) do
raise ArgumentError.value_expected("setmetatable", 1)
end
# getmetatable(object) — returns the metatable of an object
defp lua_getmetatable([{:tref, _} = tref | _], state) do
table = State.get_table(state, tref)
case table.metatable do
nil ->
{[nil], state}
{:tref, mt_id} = mt_ref ->
mt = Map.fetch!(state.tables, mt_id)
# If metatable has __metatable field, return that instead
case Map.get(mt.data, "__metatable") do
nil -> {[mt_ref], state}
sentinel -> {[sentinel], state}
end
end
end
defp lua_getmetatable([value | _], state) when is_binary(value) do
# For strings, return the string metatable if set
case Map.get(state.metatables, "string") do
nil -> {[nil], state}
mt_ref -> {[mt_ref], state}
end
end
defp lua_getmetatable([_other | _], state) do
{[nil], state}
end
defp lua_getmetatable([], state) do
{[nil], state}
end
# Install package table with loaded and path fields
defp install_package_table(state) do
# Create package.loaded table (initially empty)
{loaded_tref, state} = State.alloc_table(state)
# Create package.preload table (initially empty)
{preload_tref, state} = State.alloc_table(state)
# Create package table with loaded, preload, and path fields
package_data = %{
"loaded" => loaded_tref,
"preload" => preload_tref,
"path" => "?.lua;?/init.lua"
}
{package_tref, state} = State.alloc_table(state, package_data)
state = State.set_global(state, "package", package_tref)
# Cache "package" itself in package.loaded
cache_module_result(state, "package", package_tref)
end
# require(modname) — loads a Lua module
defp lua_require([modname | _], state) when is_binary(modname) do
# Get package table
{:tref, pkg_id} = State.get_global(state, "package")
package = Map.fetch!(state.tables, pkg_id)
# Get package.loaded table
{:tref, loaded_id} = Map.fetch!(package.data, "loaded")
loaded_table = Map.fetch!(state.tables, loaded_id)
# Check if already loaded
case Map.get(loaded_table.data, modname) do
nil ->
# Not loaded. First consult package.preload, then fall back to path.
case lookup_preload(modname, package, state) do
{:ok, loader} ->
load_from_preload(modname, loader, state)
:not_found ->
search_path = Map.get(package.data, "path", "?.lua")
load_module(modname, search_path, state)
end
result ->
# Already loaded, return cached result
{[result], state}
end
end
defp lua_require([non_string | _], _state) do
raise ArgumentError,
function_name: "require",
arg_num: 1,
expected: "string",
got: Value.type_name(non_string)
end
defp lua_require([], _state) do
raise ArgumentError.value_expected("require", 1)
end
# Look up `package.preload[modname]`. Returns `{:ok, loader}` if a callable
# is registered, `:not_found` otherwise. This is the narrow searcher
# equivalent for `package.preload` — we don't yet expose the full
# `package.searchers` table.
defp lookup_preload(modname, package, state) do
case Map.get(package.data, "preload") do
{:tref, preload_id} ->
preload_table = Map.fetch!(state.tables, preload_id)
case Map.get(preload_table.data, modname) do
nil -> :not_found
loader -> {:ok, loader}
end
_ ->
:not_found
end
end
# Call a preload loader (closure or native_func) with the module name
# and cache its result, mirroring `parse_and_execute_module`.
defp load_from_preload(modname, loader, state) do
state = cache_module_result(state, modname, true)
{results, state} = Executor.call_function(loader, [modname], state)
result =
case results do
[value | _] -> value
[] -> true
end
state = cache_module_result(state, modname, result)
{[result], state}
end
# Load a module by searching the path
defp load_module(modname, search_path, state) do
patterns = String.split(search_path, ";", trim: true)
case find_module_file(modname, patterns) do
{:ok, file_path, content} ->
parse_and_execute_module(modname, file_path, content, state)
{:error, :not_found} ->
raise RuntimeError,
value: "module '#{modname}' not found:\n\tno file '#{search_path}'"
end
end
# Parse, compile, and execute a module file.
#
# Caches `true` as a sentinel in `package.loaded` *before* executing the
# module body so that any recursive `require(modname)` call from within
# the body resolves to the sentinel instead of triggering another
# filesystem search and re-execution. This mirrors reference Lua's
# behavior and prevents infinite recursion when a module file's name
# collides with a package on the search path (e.g. a test file named
# `utf8.lua` calling `require'utf8'`).
#
# The sentinel is overwritten with the module's actual return value (or
# `true` if the module returned nothing) once execution completes.
defp parse_and_execute_module(modname, file_path, content, state) do
state = cache_module_result(state, modname, true)
with {:ok, ast} <- Lua.Parser.parse(content),
{:ok, proto} <- Lua.Compiler.compile(ast),
# `require` runs mid-evaluation: inherit the caller's instruction
# budget instead of resetting it, so a looping module body counts
# against the same `:max_instructions` and the pre-require work is preserved.
{:ok, results, state} <- Lua.VM.execute(proto, state, reset_instructions: false) do
# Get the return value (or true if no return value)
result =
case results do
[value | _] -> value
[] -> true
end
# Overwrite the sentinel with the actual result
state = cache_module_result(state, modname, result)
{[result], state}
else
{:error, msg} ->
raise RuntimeError,
value: "error loading module '#{modname}' from file '#{file_path}':\n#{msg}"
end
end
# Cache the module result in package.loaded
defp cache_module_result(state, modname, result) do
{:tref, loaded_id} = get_package_loaded_ref(state)
State.update_table(state, {:tref, loaded_id}, fn loaded_table ->
Table.put(loaded_table, modname, result)
end)
end
# Get the package.loaded table reference
defp get_package_loaded_ref(state) do
with {:tref, pkg_id} <- State.get_global(state, "package"),
package = Map.fetch!(state.tables, pkg_id),
{:tref, _loaded_id} = loaded_ref <- Map.fetch!(package.data, "loaded") do
loaded_ref
end
end
# Find a module file by searching the patterns
defp find_module_file(modname, patterns) do
resolved = String.replace(modname, ".", "/")
Enum.find_value(patterns, {:error, :not_found}, fn pattern ->
file_path = String.replace(pattern, "?", resolved)
case File.read(file_path) do
{:ok, content} -> {:ok, file_path, content}
{:error, _} -> nil
end
end)
end
# Convert a value to string, checking for __tostring metamethod
defp value_to_string_with_mt(value, state) do
case value do
{:tref, id} ->
table = Map.fetch!(state.tables, id)
case table.metatable do
{:tref, mt_id} ->
mt = Map.fetch!(state.tables, mt_id)
case Map.get(mt.data, "__tostring") do
nil ->
{Value.to_string(value), state}
tostring_fn ->
{results, state} = Executor.call_function(tostring_fn, [value], state)
{List.first(results) || "nil", state}
end
_ ->
{Value.to_string(value), state}
end
_ ->
{Value.to_string(value), state}
end
end
# collectgarbage stub — no-op accepting all standard modes
defp lua_collectgarbage(args, state) do
mode = List.first(args) || "collect"
case mode do
"count" ->
# Return plausible memory usage in KB and remainder bytes
{[0.0, 0], state}
"isrunning" ->
{[true], state}
_ ->
# "collect", "stop", "restart", "step", "setpause", "setstepmul", "generational", "incremental"
{[0], state}
end
end
# dofile stub — not supported in embedded mode
defp lua_dofile(_args, _state) do
raise RuntimeError, value: "dofile not supported in embedded mode"
end
# Install global 'unpack' as alias for table.unpack
defp install_unpack_alias(state) do
case State.get_global(state, "table") do
{:tref, id} ->
table = Map.fetch!(state.tables, id)
case Map.get(table.data, "unpack") do
nil -> state
unpack_fn -> State.set_global(state, "unpack", unpack_fn)
end
_ ->
state
end
end
end