README.md

# 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.