Lua for Elixir

repository·main·Indexed 41 days ago

https://github.com/tv-labs/lua

A pure Elixir implementation of the Lua 5.3 virtual machine. It provides a sandboxed scripting runtime for running untrusted code, such as AI-generated scripts or user plugins, without requiring NIFs or C dependencies.

Tokens
12.6K
Snippets
43
Records
60
Agent score
51%

What's inside Lua

  1. Understand the three layers of Lua security

    main

    Running untrusted Lua scripts safely requires a multi-layered approach. Lua provides three distinct layers of protection:

    1. Capability sandboxing: Controls which standard-library functions the script can call (e.g., restricting filesystem, OS, or module loading access).
    2. Resource limits: Bounds on a single script's allocations and recursion, enforced internally by the VM (e.g., preventing massive string allocations or infinite recursion).
    3. Host-level isolation: Enforces wall-clock time and total memory usage. These are not enforced by the VM itself and must be managed by the host (e.g., using Elixir processes with timeouts and heap limits).
  2. Use Metatables and Metamethods in Lua

    main

    The library supports full metamethod dispatch, allowing you to use idiomatic Lua object-oriented patterns (e.g., __index, __add, __call, __tostring) within your scripts.

    {[result], _lua} = Lua.eval!(~LUA"""
    ...> local Vec = {}
    ...> Vec.__index = Vec
    ...> Vec.__add = function(a, b) return setmetatable({x = a.x + b.x}, Vec) end
    ...> local a = setmetatable({x = 1}, Vec)
    ...> local b = setmetatable({x = 2}, Vec)
    ...> return (a + b).x
    ...> """
    ")
    # result => 3
  3. Call protocol: pcall, varargs, and multiple returns

    main

    The call protocol is a critical surface for host embedding. Performance characteristics include:

    • Protected Calls (pcall): The 'success path' (where pcall succeeds) is highly optimized and is a primary cost for hosts wrapping script entry points. In version 1.0.2, this path is significantly faster than Luerl.
    • Error Handling: Protected calls that catch errors (raising a string error and catching it) involve error construction and stack unwinding, which is more expensive than the success path.
    • Variadic Handling: Handling select(#, ...), table.pack/unpack, and multiple-return destructuring is a standard part of the protocol and carries its own performance profile.
  4. Pass Elixir terms as Userdata

    main

    To pass an arbitrary Elixir term (like a struct or map) across the boundary without Lua being able to inspect its internals, wrap it in a {:userdata, term} tuple. Lua can hold the reference and pass it back to Elixir, but it remains opaque to the Lua VM.

    lua = Lua.set!(Lua.new(), [:thing], {:userdata, %{secret: 42}})
    {[{:userdata, %{secret: 42}}], _lua} = Lua.eval!(lua, "return thing")
  5. Limitations and exclusions in the Lua runtime

    main

    When choosing this library for your project, be aware of the following design decisions and limitations:

    • No Coroutines: The coroutine library is explicitly not implemented in v1.0.0 and v1.0.2. If your Lua workload requires coroutines, you must use v0.4.0 (which uses Luerl).
    • Error Handling: In v1.0.2, error() preserves table values, whereas v0.4.0 stringifies them.
    • Sandboxing: The library supports sandboxing, but ensure your performance expectations account for the boundary costs of encode!/decode! when using sandboxed environments.
  6. Handle encoded Lua value changes in 1.0.0

    main

    In 1.0.0, encoded Lua values use a new representation compared to the Luerl-based 0.4.0.

    New Tag Mappings:

    • Table reference: {:tref, integer()}
    • Userdata reference: {:udref, integer()}
    • Elixir callable: {:native_func, fun}
    • Compiled Lua func: {:lua_closure, _, _}

    Best Practice: Avoid pattern-matching on these internal tuple shapes. Instead, use Lua.decode!/2 to convert encoded values into plain Elixir data for durable matching.

    # Durable approach: decode to plain Elixir and match
    case Lua.decode!(lua, value) do
      map when is_map(map) -> handle_table(map)
    end
  7. Understand the `bench_results/` directory structure

    main

    The bench_results/ directory contains the output of benchmark runs. It is not used by the library at runtime and is not included in the Hex package.

    Version Directory Layout

    Each version directory (e.g., v1.0.2/) contains:

    • environment.md: Metadata including ref, commit, mode, CPU, OTP/Elixir version, and timestamp.
    • summary.json: Parsed results containing workload, case, and job metrics.
    • <workload>.txt: The verbatim stdout from the corresponding mix run benchmarks/<workload>.exs command.
    • Environment probes: cpu.txt, versions.txt, timestamp.txt, and commit.txt.
  8. Performance characteristics of Table operations

    main

    The library's table operations show varying performance profiles depending on the operation and table structure:

    • Hash-part iteration: Iterating over string-keyed tables (forcing entries into the hash part) is highly optimized. In version 1.0.2, pairs iteration is approximately 1.35× faster than Luerl because it uses a memoized order-index, making each next step O(1).
    • Sorting: table.sort is a relatively expensive operation compared to other table tasks. While version 1.0.2 improved performance by rebuilding the array part with a single :array.from_list/2 write-back, the comparison-driven sort itself remains a bottleneck.
    • Array/Hash split: Operations like building, summing, and mapping/reducing on arrays are efficient, but performance varies based on whether the table is being treated as an array or a hash-map.
  9. Performance characteristics of String patterns

    main

    Lua pattern engine performance varies by use case:

    • Extraction: Using string.find and string.match for field extraction (character classes, quantifiers, captures) is highly efficient.
    • Substitution: string.gsub template substitution is efficient and performs significantly better in terms of memory allocation compared to Luerl.
    • Tokenization: Tokenizers that rely on repeated string.find calls with advancing offsets incur a setup penalty for each restart, making them slightly slower than pure extraction or substitution tasks.
  10. Known performance gaps in the native Lua VM

    main

    While the native VM has improved, certain workloads remain slower than the Luerl control:

    • Varargs and multiple returns: This is a genuine performance loss. Variadic collection and table.pack/unpack round-tripping result in higher allocation and slower execution compared to Luerl.
    • Protected calls (pcall) with raises: The native VM (v1.0.2) is slower during error raising because it performs more work to produce a conformant, position-prefixed error message (e.g., "<eval>:1: error_message") compared to Luerl's bare error strings. This is considered a correctness improvement rather than pure overhead.
    • table.sort: Large table sorting remains slightly slower than the Luerl control.
    • String patterns: Find-based tokenization is slightly slower than Luerl.
  11. Handle Lua runtime errors with source and line info

    main

    Runtime errors raise Lua.RuntimeException. This exception includes :source (the chunk name, e.g., <eval>) and :line (the line number where the error occurred), allowing you to report precise error locations.

    import Lua
    
    try do
      Lua.eval!(~LUA"""
      local x = 1
      error("something went wrong")
      """
      ")
    rescue
      e in Lua.RuntimeException ->
        e.line    # => 2
        e.source  # => "<eval>"
        Exception.message(e)
    end
  12. Understand the Lua VM release roadmap and versioning

    main

    The project follows a strategic roadmap to stabilize its Elixir-native VM rewrite. Understanding the versioning helps you anticipate API stability and feature availability:

    • 1.0.0 (Current/Stabilization): Focuses on freezing the public API. It includes the completed Elixir-native rewrite, structured errors, parse_structured/1, :max_instructions, and O(1) #t/pairs. It does not include coroutines, full io, or advanced GC/weak-tables (these are documented non-goals).
    • 1.1.0 (Additive DX): Introduces the Lua.Encoder protocol, deflua auto-marshalling, and VFS (Virtual File System) plumbing for os/require IO.
    • 2.0.0 (Full Virtualization): A major version bump that flips the sandbox default. In this version, os/io operations target the VFS by default, meaning sandbox refusals from 1.x become successful VFS operations.