# Starlark for Elixir
`Starlark` embeds the [Bazel-compatible Starlark language](https://github.com/facebook/starlark-rust) 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):
```elixir
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
```elixir
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:
* `value` / `value_repr` – JSON-friendly value and the underlying Starlark representation.
* `stdout` – any output produced by `print`.
* `exports` – variables set via `set_var/2`.
* `notifications` – messages emitted from `notify/2`.
* `http_calls` – metadata captured for each `http_get/1`.
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.
```elixir
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:
```elixir
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:
* `:max_steps` – caps the number of executed statements.
* `:max_call_depth` – prevents runaway recursion.
* `:max_heap_bytes` – enforces a heap budget (checked before and after statements).
* `:wall_time_ms` – aborts scripts that exceed a wall-clock deadline.
* `:http_timeout_ms` – per-request timeout for `http_get/1`.
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:
* `notify(channel, message)` – append a notification recorded in `%Result.notifications`.
* `http_get(url)` – performs an HTTP GET with an optional timeout. The body is returned to the script and each call is logged to `%Result.http_calls`.
### Transport requirements
`http_get/1` relies on [`reqwest`](https://docs.rs/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
* Run `mix deps.get` then `mix compile` to build the NIF.
* Execute the test suite with `mix test` (integration tests cover bindings, limits, notifications, and I/O failures).
* Generate HTML docs via `mix docs`.
When publishing to Hex, run `mix hex.build` to verify the package metadata and included files.