dev_lua.erl - Lua 5.3 Execution Device
Overview
Purpose: Execute Lua scripts within HyperBEAM processes
Module: dev_lua
Pattern: Sandboxed Lua VM with AO-Core library integration
Integration: Process execution device, WASM alternative, AOS compatibility
This module provides Lua 5.3 execution capabilities for HyperBEAM processes. It initializes Lua virtual machines, loads modules, applies sandboxing rules, and executes Lua functions. The device includes the full AO-Core library for Lua scripts and supports process state management through snapshots.
Dependencies
- Erlang/OTP:
luerl(Lua implementation in Erlang) - HyperBEAM:
hb_ao,hb_cache,hb_private,hb_opts,hb_util,hb_message - Devices:
dev_lua_lib,dev_message - Arweave:
ar_wallet
Public Functions Overview
%% Device Info
-spec info(Base) -> DeviceInfo.
%% Lifecycle
-spec init(Base, Req, Opts) -> {ok, BaseWithState} | {error, Reason}.
-spec normalize(Base, Req, Opts) -> {ok, BaseWithSnapshot} | {error, Reason}.
-spec snapshot(Base, Req, Opts) -> {ok, Snapshot}.
%% Execution
-spec functions(Base, Req, Opts) -> {ok, [FunctionName]}.
%% Utilities
-spec encode(Term, Opts) -> LuaTerm.
-spec decode(LuaTerm, Opts) -> Term.
-spec pure_lua_process_benchmark(BenchMsgs) -> ok.Public Functions
1. info/1
-spec info(Base) -> DeviceInfo
when
Base :: map(),
DeviceInfo :: #{
default => fun(),
excludes => [binary()]
}.Description: Return device information. Sets default handler to compute/4 and excludes base message keys plus utility functions.
keys,set(message@1.0 functions)encode,decode(public utilities)- All keys present in Base message
-module(dev_lua_info_test).
-include_lib("eunit/include/eunit.hrl").
info_test() ->
Base = #{<<"custom-key">> => <<"value">>},
Info = dev_lua:info(Base),
?assert(is_map(Info)),
?assert(maps:is_key(default, Info)),
?assert(maps:is_key(excludes, Info)).
info_excludes_test() ->
Base = #{<<"my-func">> => <<"test">>},
Info = dev_lua:info(Base),
Excludes = maps:get(excludes, Info),
?assert(lists:member(<<"keys">>, Excludes)),
?assert(lists:member(<<"set">>, Excludes)),
?assert(lists:member(<<"encode">>, Excludes)),
?assert(lists:member(<<"decode">>, Excludes)),
?assert(lists:member(<<"my-func">>, Excludes)).2. init/3
-spec init(Base, Req, Opts) -> {ok, BaseWithState} | {error, Reason}
when
Base :: map(),
Req :: map(),
Opts :: map(),
BaseWithState :: map(),
Reason :: term().Description: Initialize the Lua VM by loading modules and installing libraries.
Initialization Steps:- Check if already initialized (has state)
- Find modules from
modulekey - Load module code (from cache if ID)
- Initialize new Lua state
- Load all modules into state
- Apply sandboxing rules
- Install AO-Core library
- Return base with state in private storage
- Binary string (inline code)
- Message ID (fetch from cache)
- Message with
bodyordatakey - List of any above
-module(dev_lua_init_test).
-include_lib("eunit/include/eunit.hrl").
init_with_inline_module_test() ->
Base = #{
<<"module">> => #{
<<"content-type">> => <<"application/lua">>,
<<"body">> => <<"function test() return 42 end">>
}
},
{ok, Result} = dev_lua:init(Base, #{}, #{}),
?assert(is_map(Result)).
init_with_module_id_test() ->
% Verify module exports init/3
code:ensure_loaded(dev_lua),
?assert(erlang:function_exported(dev_lua, init, 3)).
init_already_initialized_test() ->
State = luerl:init(),
Base = hb_private:set(#{}, <<"state">>, State, #{}),
{ok, Result} = dev_lua:init(Base, #{}, #{}),
?assertEqual(Base, Result).
init_multiple_modules_test() ->
Modules = [
#{
<<"content-type">> => <<"application/lua">>,
<<"body">> => <<"function mod1() return 1 end">>
},
#{
<<"content-type">> => <<"application/lua">>,
<<"body">> => <<"function mod2() return 2 end">>
}
],
Base = #{<<"module">> => Modules},
{ok, Result} = dev_lua:init(Base, #{}, #{}),
?assert(is_map(Result)).3. functions/3
-spec functions(Base, Req, Opts) -> {ok, [FunctionName]} | {error, not_found}
when
Base :: map(),
Req :: map(),
Opts :: map(),
FunctionName :: binary().Description: Return list of all functions in the Lua global environment.
Execution: Runs Lua code to iterate _G table and collect function names.
-module(dev_lua_functions_test).
-include_lib("eunit/include/eunit.hrl").
functions_test() ->
Base = #{
<<"module">> => #{
<<"content-type">> => <<"application/lua">>,
<<"body">> => <<
"function test1() end\n",
"function test2() end\n"
>>
}
},
{ok, Initialized} = dev_lua:init(Base, #{}, #{}),
{ok, Functions} = dev_lua:functions(Initialized, #{}, #{}),
?assert(is_list(Functions)),
?assert(lists:member(<<"test1">>, Functions)),
?assert(lists:member(<<"test2">>, Functions)).
functions_not_initialized_test() ->
Base = #{},
{error, not_found} = dev_lua:functions(Base, #{}, #{}).4. snapshot/3
-spec snapshot(Base, Req, Opts) -> {ok, Snapshot}
when
Base :: map(),
Req :: map(),
Opts :: map(),
Snapshot :: map().Description: Create a snapshot of the current Lua state for persistence.
Snapshot Structure:#{
<<"state">> => SerializedLuaState,
<<"version">> => <<"1.0">>,
<<"timestamp">> => Timestamp
}-module(dev_lua_snapshot_test).
-include_lib("eunit/include/eunit.hrl").
snapshot_test() ->
Base = #{
<<"module">> => #{
<<"content-type">> => <<"application/lua">>,
<<"body">> => <<"x = 42">>
}
},
{ok, Initialized} = dev_lua:init(Base, #{}, #{}),
{ok, Snapshot} = dev_lua:snapshot(Initialized, #{}, #{}),
?assert(is_map(Snapshot)),
?assert(maps:is_key(<<"state">>, Snapshot)).5. normalize/3
-spec normalize(Base, Req, Opts) -> {ok, BaseWithSnapshot} | {error, Reason}
when
Base :: map(),
Req :: map(),
Opts :: map(),
BaseWithSnapshot :: map(),
Reason :: term().Description: Restore Lua state from a snapshot if present.
Normalization Flow:- Check for snapshot in Base
- If found, extract state
- Replace current state with snapshot state
- Remove snapshot from message
-module(dev_lua_normalize_test).
-include_lib("eunit/include/eunit.hrl").
normalize_with_snapshot_test() ->
% Create and snapshot a state
Base1 = #{
<<"module">> => #{
<<"content-type">> => <<"application/lua">>,
<<"body">> => <<"value = 123">>
}
},
{ok, Initialized} = dev_lua:init(Base1, #{}, #{}),
{ok, Snapshot} = dev_lua:snapshot(Initialized, #{}, #{}),
% Create new base with snapshot
Base2 = #{
<<"module">> => #{
<<"content-type">> => <<"application/lua">>,
<<"body">> => <<"value = 0">>
},
<<"snapshot">> => Snapshot
},
{ok, Normalized} = dev_lua:normalize(Base2, #{}, #{}),
?assert(is_map(Normalized)),
?assertNot(maps:is_key(<<"snapshot">>, Normalized)).
normalize_without_snapshot_test() ->
Base = #{},
{ok, Normalized} = dev_lua:normalize(Base, #{}, #{}),
?assertEqual(Base, Normalized).6. encode/2, decode/2
-spec encode(Term, Opts) -> LuaTerm
when
Term :: term(),
Opts :: map(),
LuaTerm :: term().
-spec decode(LuaTerm, Opts) -> Term
when
LuaTerm :: term(),
Opts :: map(),
Term :: term().Description: Convert between Erlang and Lua data structures.
Encoding Rules:- Maps → Lua tables
- Lists → Lua arrays (numeric indices)
- Binaries → Lua strings
- Numbers → Lua numbers
- Atoms → Lua strings
-module(dev_lua_encode_decode_test).
-include_lib("eunit/include/eunit.hrl").
encode_decode_map_test() ->
Term = #{<<"key">> => <<"value">>, <<"num">> => 42},
Encoded = dev_lua:encode(Term, #{}),
Decoded = dev_lua:decode(Encoded, #{}),
?assertEqual(Term, Decoded).
encode_decode_list_test() ->
Term = [1, 2, 3, 4, 5],
Encoded = dev_lua:encode(Term, #{}),
Decoded = dev_lua:decode(Encoded, #{}),
?assertEqual(Term, Decoded).
encode_decode_nested_test() ->
Term = #{
<<"users">> => [
#{<<"name">> => <<"Alice">>, <<"age">> => 30},
#{<<"name">> => <<"Bob">>, <<"age">> => 25}
]
},
Encoded = dev_lua:encode(Term, #{}),
Decoded = dev_lua:decode(Encoded, #{}),
?assertEqual(Term, Decoded).Common Patterns
%% Create Lua process
Process = #{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"module">> => #{
<<"content-type">> => <<"application/lua">>,
<<"body">> => LuaCode
}
},
{ok, Initialized} = hb_ao:resolve(Process, <<"init">>, Opts).
%% Execute Lua function
{ok, Result} = hb_ao:resolve(
Initialized,
#{
<<"path">> => <<"my_function">>,
<<"parameters">> => [Arg1, Arg2]
},
Opts
).
%% Load multiple modules
Process = #{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"module">> => [
#{<<"content-type">> => <<"application/lua">>, <<"body">> => Mod1},
#{<<"content-type">> => <<"application/lua">>, <<"body">> => Mod2}
]
},
{ok, Initialized} = hb_ao:resolve(Process, <<"init">>, Opts).
%% Apply sandboxing
Process = #{
<<"execution-device">> => <<"lua@5.3a">>,
<<"module">> => Module,
<<"sandbox">> => true % Use default sandbox
},
{ok, Initialized} = dev_lua:init(Process, #{}, Opts).
%% Custom sandbox
Process = #{
<<"execution-device">> => <<"lua@5.3a">>,
<<"module">> => Module,
<<"sandbox">> => #{
['_G', os, execute] => <<"sandboxed">>,
['_G', io] => <<"sandboxed">>
}
},
{ok, Initialized} = dev_lua:init(Process, #{}, Opts).
%% Snapshot and restore
{ok, Snapshot} = dev_lua:snapshot(Process, #{}, Opts),
{ok, _} = hb_cache:write(Snapshot, Opts),
% Later, restore
Process2 = Process#{<<"snapshot">> => Snapshot},
{ok, Restored} = dev_lua:normalize(Process2, #{}, Opts).Sandboxing
Default Sandbox
-define(DEFAULT_SANDBOX, [
{['_G', io], <<"sandboxed">>},
{['_G', file], <<"sandboxed">>},
{['_G', os, execute], <<"sandboxed">>},
{['_G', os, exit], <<"sandboxed">>},
{['_G', os, getenv], <<"sandboxed">>},
{['_G', os, remove], <<"sandboxed">>},
{['_G', os, rename], <<"sandboxed">>},
{['_G', os, tmpname], <<"sandboxed">>},
{['_G', package], <<"sandboxed">>},
{['_G', loadfile], <<"sandboxed">>},
{['_G', require], <<"sandboxed">>},
{['_G', dofile], <<"sandboxed">>},
{['_G', load], <<"sandboxed">>},
{['_G', loadstring], <<"sandboxed">>}
]).Sandbox Options
Boolean (default):#{<<"sandbox">> => true}
% Uses DEFAULT_SANDBOX#{
<<"sandbox">> => [
['_G', os],
['_G', io]
]
}#{
<<"sandbox">> => #{
['_G', os, execute] => <<"blocked">>,
['_G', io] => fun() -> error("IO disabled") end
}
}Security Benefits
- Prevents file system access
- Blocks process execution
- Disables module loading
- Protects environment variables
- Prevents code injection
AO-Core Library
Automatic Installation
The dev_lua_lib is automatically installed during init/3:
{ok, State3} = dev_lua_lib:install(Base, State2, Opts)Available Functions
-- AO-Core resolution
ao.resolve(message)
ao.resolve(base, path)
-- Message manipulation
ao.get(key, base)
ao.set(base, key, value)
ao.set(base, updates)
-- Event logging
ao.event(event)
ao.event(group, event)Device Sandbox
#{
<<"device-sandbox">> => [
<<"message@1.0">>,
<<"custom-device@1.0">>
]
}
% Only listed devices available in ao.resolve()Module Loading
Module Sources
Inline Binary:#{
<<"module">> => #{
<<"content-type">> => <<"application/lua">>,
<<"body">> => <<"function test() return 1 end">>
}
}#{
<<"module">> => <<"module-id-abc123...">>
}
% Fetched from cache via hb_cache:read/2#{
<<"module">> => #{
<<"name">> => <<"my-module">>,
<<"body">> => LuaCode
}
}#{
<<"module">> => [
ModuleID1,
#{<<"body">> => Code2},
#{<<"body">> => Code3}
]
}Loading Order
Modules are loaded in order they appear in the list. Later modules can reference functions from earlier modules.
State Management
Private Storage
Lua state is stored in message private data:
hb_private:set(Base, <<"state">>, LuerlState, Opts)State Isolation
Each process has its own Lua VM:
- No shared state between processes
- Each execution independent
- Snapshots capture complete state
State Serialization
% Create snapshot
{ok, Snapshot} = snapshot(Base, #{}, Opts),
% Snapshot contains serialized state
State = maps:get(<<"state">>, Snapshot),
% Restore from snapshot
Base2 = Base#{<<"snapshot">> => Snapshot},
{ok, Restored} = normalize(Base2, #{}, Opts)Execution Model
Function Call
function my_function(arg1, arg2)
return arg1 + arg2
end{ok, Result} = hb_ao:resolve(
Process,
#{
<<"path">> => <<"my_function">>,
<<"parameters">> => [5, 3]
},
Opts
),
% Result: 8Default Handler
Unrecognized paths resolve to Lua function calls:
GET /path/to/function
→ Calls: function(params)Process Integration
As Execution Device
#{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"module">> => LuaModule
}With Scheduler
#{
<<"device">> => <<"process@1.0">>,
<<"scheduler-device">> => <<"scheduler@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"module">> => LuaModule,
<<"authority">> => AuthorityAddress
}AOS Compatibility
AOS Processes
generate_lua_process("path/to/hyper-aos.lua", Opts)Creates process compatible with AOS:
- Eval action support
- Message handlers
- Send() function
- Process state management
AOS Messages
#{
<<"action">> => <<"Eval">>,
<<"data">> => <<"return 1 + 1">>,
<<"target">> => ProcessID
}Performance
Benchmarking
dev_lua:pure_lua_process_benchmark(100)
% Executes 100 messages and reports performanceOptimization Tips
- Minimize module size
- Use local variables in Lua
- Batch operations when possible
- Consider WASM for compute-heavy tasks
References
- Lua Library -
dev_lua_lib.erl - Luerl - Lua in Erlang implementation
- AO Resolution -
hb_ao.erl - Cache -
hb_cache.erl - Private Storage -
hb_private.erl
Notes
- Lua 5.3: Uses Luerl (Lua in Erlang) implementation
- Sandboxing: Default sandbox blocks dangerous operations
- AO Library: Full AO-Core integration via dev_lua_lib
- Multiple Modules: Supports loading multiple Lua files
- State Snapshots: Complete VM state can be saved/restored
- Function Discovery: Can list all global functions
- Module Sources: Inline code, IDs, or messages
- Security: Sandboxing prevents file/process/network access
- Device Sandbox: Restricts available devices in ao.resolve()
- Encode/Decode: Bidirectional Erlang ↔ Lua conversion
- Private State: Lua VM stored in message private data
- Default Handler: Unknown paths → Lua function calls
- Process Compatible: Works as execution-device in processes
- AOS Support: Full compatibility with AOS processes
- Benchmarking: Built-in performance testing utilities