Starlark for Elixir

Starlark embeds the Bazel-compatible Starlark language in Elixir by pairing a tiny wrapper module with a Rustler-powered NIF. It lets you run untrusted scripts with resource limits, capture stdout, exports, and notifications, and even make safe round-trips into Elixir by binding regular functions.

Installation

Add the dependency to your mix.exs (the package name will remain :starlark on Hex):

def deps do
  [
    {:starlark, "~> 0.1"}
  ]
end

Make sure you have a working Rust toolchain (rustup or equivalent). The NIF is compiled automatically when you run mix deps.get followed by mix compile.

Quick start

iex> {:ok, result} =
...>   Starlark.eval("""
...>   load("@stdlib//json", "json")
...>
...>   def average(latency_samples):
...>     total = 0
...>     for value in latency_samples:
...>       total = total + value
...>     return total / len(latency_samples)
...>
...>   notify("checks", json.encode({"avg": average(samples)}))
...>   average(samples)
...>   """,
...>   bindings: %{samples: [90, 110, 100]},
...>   function_bindings: %{log: &Logger.info/1}
...> )
iex> result.value
100
iex> result.notifications
[%Starlark.Notification{channel: "checks", message: "{\"avg\": 100.0}"}]

Every successful call returns %Starlark.Result{} whose fields include:

Failures yield {:error, %Starlark.EvalError{}} with a descriptive :kind (:parse, :runtime, :timeout, :resource_limit, :io, etc.).

Binding Elixir functions

Expose host functionality safely by passing a :function_bindings map. Each function executes in the caller process; the runtime applies JSON encoding/decoding automatically.

double = fn value -> value * 2 end

script = """
def greet(name):
  return "Hello from Starlark, " + name

result = double(counter)
set_var("greeting", greet(user))
result
"""

{:ok, result} =
  Starlark.eval(script,
    bindings: %{counter: 21, user: "root"},
    function_bindings: %{double: double}
  )

result.value
# => 42

result.exports["greeting"]
# => "Hello from Starlark, world"

The dispatcher checks that each published function matches the arity advertised in function_bindings and propagates any exceptions back into the script as runtime errors.

Evaluating scripts from disk

Prefer eval_file/2 when you manage scripts on disk:

case Starlark.eval_file("priv/scripts/check.star", wall_time_ms: 2_000) do
  {:ok, %Starlark.Result{} = result} ->
    IO.inspect(result.exports)

  {:error, %Starlark.EvalError{kind: :io} = error} ->
    Logger.error(error.message)

  {:error, %Starlark.EvalError{} = error} ->
    Logger.error("Script failed: #{error.message}")
end

eval_file/2 returns {:error, %EvalError{kind: :io}} with the formatted reason when the file cannot be read.

Resource limits

The evaluator exposes several guardrails for running untrusted code:

All options are optional and default to the conservative values baked into the Rust runtime.

Notifications and HTTP

Two helper functions are available inside scripts:

Transport requirements

http_get/1 relies on reqwest with rustls. No additional configuration is required on the Elixir side, but the target system must ship with the standard OS TLS roots.

Development

When publishing to Hex, run mix hex.build to verify the package metadata and included files.