# `ExDatalog.Program`
[🔗](https://github.com/thanos/ex_datalog/blob/v0.5.0/lib/ex_datalog/program.ex#L1)

Builder for constructing Datalog programs.

A program holds:

- **Relations** — named schemas with arity and type information.
- **Facts** — ground tuples asserted as true for a given relation.
- **Rules** — inference rules that derive new facts from existing ones.

Relation names are string keys stored in a map. They do not collide with
internal atoms like `:positive`, `:negative`, `:wildcard`, or `:constraint`
because those atoms appear in tuple positions (`{:positive, atom}`),
not as map keys.

Programs are built using a pipeline of builder functions. Structural
validation (arity checking, relation existence) is done at build time by
`add_relation/3`, `add_fact/3`, and `add_rule/2`. On failure these return
`{:error, String.t()}` with a human-readable message.

## Error propagation in pipelines

Because builder functions return `t() | {:error, String.t()}`, a failed
step will short-circuit the rest of the pipeline: the `{:error, _}` tuple
passes through unchanged. This means you can pipe freely and check for
errors at the end:

    {:ok, knowledge} =
      Program.new()
      |> Program.add_relation("edge", [:atom, :atom])
      |> Program.add_relation("path", [:atom, :atom])
      |> Program.add_fact("edge", [:a, :b])
      |> Program.add_rule({"path", [:X, :Y]}, [{:positive, {"edge", [:X, :Y]}}])
      |> ExDatalog.materialize()

If `add_relation/3` fails, the `{:error, msg}` tuple flows through
`add_fact/3` and `add_rule/3` without raising, and `ExDatalog.materialize/1`
will detect the error struct and return `{:error, [msg]}`.

Semantic validation (variable safety, stratification, constraint binding)
is done separately by `ExDatalog.Validator.validate/1`, which returns
`{:error, [ExDatalog.Validator.Error.t()]}` with structured error structs.

**Note:** builder methods perform a subset of the same checks as the
validator (relation existence, arity). This is intentional: the builder
provides early feedback for interactive construction, while the validator
is the canonical source of truth and catches issues the builder cannot
(e.g., programs assembled by directly modifying the struct, which bypasses
builder validation).

## Shorthand rule notation

`add_rule/3` and `add_rule/4` accept a more ergonomic tuple-based notation
that avoids the need for explicit `Rule.new/3`, `ExDatalog.Atom.new/2`, and
`Term.var/1` calls:

- **Head** — `{"relation", [terms...]}` where each term follows the
  Prolog convention: uppercase atoms become variables (`:X` → `{:var, "X"}`),
  `:_` becomes a wildcard, lowercase atoms and other values become constants.

- **Body** — `{:positive, {"rel", [terms...]}}` or
  `{:negative, {"rel", [terms...]}}` for each literal.

- **Constraints** — operator tuples like `{:neq, :A, :B}` for comparisons,
  `{:add, :X, :Y, :Z}` for arithmetic, `{:is_integer, :V}` for type
  predicates, `{:starts_with, :E, "prefix"}` for string predicates, and
  `{:member, :X, [:a, :b]}` for membership.

    Program.add_rule(program,
      {"ancestor", [:X, :Z]},
      [
        {:positive, {"parent", [:X, :Y]}},
        {:positive, {"ancestor", [:Y, :Z]}}
      ]
    )

    Program.add_rule(program,
      {"high_earner", [:X]},
      [{:positive, {"income", [:X, :S]}}],
      [{:gt, :S, 100_000}]
    )

The struct-based `add_rule/2` remains available for cases where you need
full control over term types.

## Example

    iex> alias ExDatalog.{Program, Atom, Rule, Term}
    iex> program =
    ...>   Program.new()
    ...>   |> Program.add_relation("parent", [:atom, :atom])
    ...>   |> Program.add_relation("ancestor", [:atom, :atom])
    ...>   |> Program.add_fact("parent", [:alice, :bob])
    ...>   |> Program.add_fact("parent", [:bob, :carol])
    ...>   |> Program.add_rule(
    ...>        {"ancestor", [:X, :Y]},
    ...>        [{:positive, {"parent", [:X, :Y]}}]
    ...>      )
    iex> length(program.facts) == 2
    true
    iex> length(program.rules) == 1
    true

# `body_literal_shorthand`

```elixir
@type body_literal_shorthand() ::
  {:positive | :negative, {String.t(), [ExDatalog.Term.shorthand()]}}
  | {:positive | :negative, ExDatalog.Atom.t()}
  | ExDatalog.Rule.literal()
```

# `constraint_shorthand`

```elixir
@type constraint_shorthand() :: tuple() | ExDatalog.Constraint.t()
```

# `fact_values`

```elixir
@type fact_values() :: [ExDatalog.Term.value()]
```

# `head_shorthand`

```elixir
@type head_shorthand() ::
  {String.t(), [ExDatalog.Term.shorthand()]} | ExDatalog.Atom.t()
```

# `ir_type`

```elixir
@type ir_type() :: :integer | :string | :atom | :any
```

# `relation_name`

```elixir
@type relation_name() :: String.t()
```

# `relation_schema`

```elixir
@type relation_schema() :: %{arity: non_neg_integer(), types: [ir_type()]}
```

# `t`

```elixir
@type t() :: %ExDatalog.Program{
  facts: [{relation_name(), fact_values()}],
  relations: %{required(relation_name()) =&gt; relation_schema()},
  rules: [ExDatalog.Rule.t()]
}
```

# `add_fact`

```elixir
@spec add_fact(
  t(),
  {String.t(), [term()]}
) :: t() | {:error, term()}
```

Adds a fact to the program from a `{relation, values}` tuple.

This is the tuple form produced by Schema relation constructors
(e.g., `MySchema.emp(:alice, :eng)` returns `{"emp", [:alice, :eng]}`).

## Examples

    iex> alias ExDatalog.Program
    iex> program = Program.new() |> Program.add_relation("emp", [:atom, :atom])
    iex> program = Program.add_fact(program, {"emp", [:alice, :eng]})
    iex> program.facts
    [{"emp", [:alice, :eng]}]

# `add_fact`

```elixir
@spec add_fact(t(), relation_name(), fact_values()) :: t() | {:error, String.t()}
```

Adds a ground fact to the program.

The relation must be declared via `add_relation/3` and the number of
values must match the relation's arity.

Returns `{:error, reason}` if:

- The relation is not defined.
- The arity of `values` does not match the relation schema.
- A value in `values` is not an integer, string, or atom (floats are not supported).

## Examples

    iex> alias ExDatalog.Program
    iex> program = Program.new() |> Program.add_relation("parent", [:atom, :atom])
    iex> Program.add_fact(program, "parent", [:alice, :bob])
    %ExDatalog.Program{
      relations: %{"parent" => %{arity: 2, types: [:atom, :atom]}},
      facts: [{"parent", [:alice, :bob]}],
      rules: []
    }

    iex> alias ExDatalog.Program
    iex> program = Program.new() |> Program.add_relation("parent", [:atom, :atom])
    iex> {:error, _} = Program.add_fact(program, "unknown", [:alice])
    {:error, "relation \"unknown\" is not defined"}

# `add_facts`

```elixir
@spec add_facts(t(), [{String.t(), [term()]}]) :: t() | {:error, term()}
```

Adds multiple facts to the program from a list of `{relation, values}` tuples.

## Examples

    iex> alias ExDatalog.Program
    iex> program = Program.new() |> Program.add_relation("emp", [:atom, :atom])
    iex> facts = [{"emp", [:alice, :eng]}, {"emp", [:bob, :eng]}]
    iex> program = Program.add_facts(program, facts)
    iex> length(program.facts)
    2

# `add_relation`

```elixir
@spec add_relation(t(), relation_name(), [ir_type()]) :: t() | {:error, String.t()}
```

Adds a relation schema to the program.

`types` is a list of type atoms (`:integer`, `:string`, `:atom`, `:any`)
with length equal to the arity of the relation.

Returns `{:error, reason}` if:

- `name` is empty.
- `types` is empty.
- The relation already exists.

## Examples

    iex> ExDatalog.Program.add_relation(ExDatalog.Program.new(), "parent", [:atom, :atom])
    %ExDatalog.Program{
      relations: %{"parent" => %{arity: 2, types: [:atom, :atom]}},
      facts: [],
      rules: []
    }

    iex> {:error, _} = ExDatalog.Program.add_relation(ExDatalog.Program.new(), "", [:atom])
    {:error, "relation name must be a non-empty string"}

# `add_rule`

```elixir
@spec add_rule(t(), ExDatalog.Rule.t()) :: t() | {:error, String.t()}
```

Adds a rule to the program.

Performs structural validation:

- The head relation must be declared.
- The head arity must match the relation schema.
- All body atoms must reference declared relations with matching arities.

Semantic validation (variable safety, stratification) is deferred to
`ExDatalog.Validator`.

Returns `{:error, reason}` if any structural check fails.

## Examples

    iex> alias ExDatalog.{Program, Rule, Atom, Term}
    iex> program =
    ...>   Program.new()
    ...>   |> Program.add_relation("parent", [:atom, :atom])
    ...>   |> Program.add_relation("ancestor", [:atom, :atom])
    iex> rule = Rule.new(
    ...>   Atom.new("ancestor", [Term.var("X"), Term.var("Y")]),
    ...>   [{:positive, Atom.new("parent", [Term.var("X"), Term.var("Y")])}]
    ...> )
    iex> result = Program.add_rule(program, rule)
    iex> length(result.rules) == 1
    true

# `add_rule`

```elixir
@spec add_rule(t(), head_shorthand(), [body_literal_shorthand()], [
  constraint_shorthand()
]) ::
  t() | {:error, String.t()}
```

Adds a rule using shorthand notation for the head atom, body literals,
and constraints.

This is a more ergonomic alternative to `add_rule/2` that avoids the need
for explicit `Rule.new/3`, `ExDatalog.Atom.new/2`, and `ExDatalog.Term.var/1` calls.

The **head** is a tuple `{"relation", [terms...]}` where each term follows
the Prolog-inspired convention:

- Uppercase atoms become logic variables (`:X` → `{:var, "X"}`)
- `:_` becomes a wildcard
- Lowercase atoms and other values become constants

Each **body literal** is `{:positive, {"rel", [terms...]}}` or
`{:negative, {"rel", [terms...]}}`. You may also mix in structs like
`{:positive, ExDatalog.Atom.new(...)}`.

Each **constraint** is an operator tuple like `{:neq, :A, :B}` or
`{:add, :X, :Y, :Z}`. You may also use existing `%Constraint{}` structs.

Returns `{:error, reason}` if any structural check fails (same validation
as `add_rule/2`).

## Examples

    iex> alias ExDatalog.Program
    iex> program =
    ...>   Program.new()
    ...>   |> Program.add_relation("parent", [:atom, :atom])
    ...>   |> Program.add_relation("ancestor", [:atom, :atom])
    iex> result = Program.add_rule(program,
    ...>   {"ancestor", [:X, :Y]},
    ...>   [{:positive, {"parent", [:X, :Y]}}]
    ...> )
    iex> length(result.rules) == 1
    true

    iex> alias ExDatalog.Program
    iex> program =
    ...>   Program.new()
    ...>   |> Program.add_relation("income", [:atom, :integer])
    ...>   |> Program.add_relation("high_earner", [:atom])
    iex> result = Program.add_rule(program,
    ...>   {"high_earner", [:X]},
    ...>   [{:positive, {"income", [:X, :S]}}],
    ...>   [{:gt, :S, 100_000}]
    ...> )
    iex> length(result.rules) == 1
    true

# `has_relation?`

```elixir
@spec has_relation?(t(), relation_name()) :: boolean()
```

Returns `true` if the relation is defined in the program.

## Examples

    iex> alias ExDatalog.Program
    iex> program = Program.new() |> Program.add_relation("parent", [:atom, :atom])
    iex> Program.has_relation?(program, "parent")
    true

    iex> alias ExDatalog.Program
    iex> Program.has_relation?(Program.new(), "unknown")
    false

# `materialize`

```elixir
@spec materialize(
  t(),
  keyword()
) :: {:ok, ExDatalog.Knowledge.t()} | {:error, term()}
```

Materializes the program. A pipe-friendly convenience for
`ExDatalog.materialize/2`.

## Examples

    iex> alias ExDatalog.{Program, Knowledge}
    iex> program = Program.new() |> Program.add_relation("edge", [:atom, :atom])
    iex> program = program |> Program.add_fact("edge", [:a, :b])
    iex> {:ok, knowledge} = Program.materialize(program)
    iex> Knowledge.get(knowledge, "edge") |> MapSet.to_list()
    [{:a, :b}]

# `new`

```elixir
@spec new() :: t()
```

Creates a new, empty Datalog program.

## Examples

    iex> ExDatalog.Program.new()
    %ExDatalog.Program{relations: %{}, facts: [], rules: []}

# `relation`

```elixir
@spec relation(t(), relation_name()) :: relation_schema() | nil
```

Returns the schema for a relation, or `nil` if not defined.

## Examples

    iex> alias ExDatalog.Program
    iex> program = Program.new() |> Program.add_relation("parent", [:atom, :atom])
    iex> Program.relation(program, "parent")
    %{arity: 2, types: [:atom, :atom]}

    iex> alias ExDatalog.Program
    iex> Program.relation(Program.new(), "unknown")
    nil

---

*Consult [api-reference.md](api-reference.md) for complete listing*
