# `Predicator.Analysis`
[🔗](https://github.com/riddler/predicator-ex/blob/v9.5.0/lib/predicator/analysis.ex#L1)

What an expression or a program reads, calls and stores, listed for tooling.

A checker that compares every path a condition reads against a declared
shape, or an editor that lists the functions a program calls, needs those
facts without evaluating anything. This module answers three questions:

  * `references/1` - each variable read, as a path.
  * `calls/1` - each function call, with its name and arity.
  * `assignments/1` - each location a program stores to, as a path.

It is a reading surface, not a parsing one, in the `Predicator.Vocabulary`
mould. Nothing here participates in lexing, parsing, compiling or
evaluating, and no answer depends on a context: a path is read off the
program text, never resolved against data.

## Inputs

Each function accepts any of five forms of one program:

  * a source string, read as a program the way
    `Predicator.parse_program/2` reads it. Every expression is a program of
    one statement, and `=` is never an equality operator (`==` is), so a
    condition and a script read alike;
  * an AST from `Predicator.parse/2`, or any single statement or block;
  * a program from `Predicator.parse_program/2`;
  * an instruction list, from `Predicator.compile/1` or from storage;
  * a `Predicator.Compiled` envelope.

The AST forms and the instruction forms give the same paths, calls and
stores for the same source (the instruction form rebuilds paths by tracking
what the stack holds). Positions come from the AST's own slots, or from the
envelope's position table; a bare instruction list carries no positions, so
every position read from one is `nil`.

## Answers

Every answer lists every occurrence, in source order, as maps. A caller that
wants a set dedupes.

  * A reference is `%{path: [segment], position: position}`.
  * A call is `%{name: name, arity: arity, position: position}`. A qualified
    name (`ns.fn(x)`) is reported as written, dots included; calls nested in
    another call's arguments are listed too, the outer call first.
  * An assignment is `%{path: [segment], position: position}`.

A segment is a string (a variable name or a property name, or a string
literal key), an integer (a literal integer index), or `:dynamic` (any other
bracket key: a computed key, or a literal that is neither a string nor an
integer). The path continues past a `:dynamic` segment, and a computed key's
own reads are listed as references of their own.

A reference is a chain rooted at a variable. A chain rooted anywhere else (a
call's result, a literal) reads no variable, so `f(x).y` lists `x` and the
call, and no reference for `.y`. The location an assignment writes is not a
read; the reads inside its bracket keys are.

A position is the node's trailing slot (`t:Predicator.Parser.position/0`).
For a path - a reference or an assignment - it is the span of the whole
chain when the program was parsed or compiled with spans, and otherwise the
point position of the chain's root variable: either way it starts where the
path starts. A call's position is the call node's own.

## Examples

    iex> Predicator.Analysis.references("score > limit")
    {:ok, [%{path: ["score"], position: {1, 1}}, %{path: ["limit"], position: {1, 9}}]}

    iex> {:ok, program} = Predicator.parse_program("total = price * 2")
    iex> Predicator.Analysis.assignments(program)
    {:ok, [%{path: ["total"], position: {1, 1}}]}

## Errors

A source string that does not parse answers the
`%Predicator.Errors.ParseError{}` `Predicator.compile_program/1` returns for
it. An instruction the analysis cannot read - an opcode outside
`Predicator.Instructions.opcodes/0`, or an operand of the wrong shape -
answers an `%Predicator.Errors.EvaluationError{}` with the evaluator's
`"unknown_instruction"` reason. An instruction list that pops more values
than it pushed is read as far as it goes: the missing values read nothing.

`test/predicator/analysis_clause_coverage_test.exs` keeps the AST clauses in
step with `t:Predicator.Parser.visitable/0` and the instruction clauses in
step with `Predicator.Instructions.opcodes/0`, the retired opcodes included.

# `assignment`

```elixir
@type assignment() :: %{path: [segment()], position: position()}
```

One location a program stores to.

# `call`

```elixir
@type call() :: %{name: String.t(), arity: non_neg_integer(), position: position()}
```

One function call.

# `error`

```elixir
@type error() ::
  Predicator.Errors.ParseError.t() | Predicator.Errors.EvaluationError.t()
```

The error arm: a parse failure for a source string, or an instruction the
analysis cannot read.

# `input`

```elixir
@type input() ::
  String.t()
  | Predicator.Parser.visitable()
  | Predicator.Types.instruction_list()
  | Predicator.Compiled.t()
```

Anything the three functions accept.

# `position`

```elixir
@type position() :: Predicator.Parser.position()
```

A source annotation: a point position, a span, or `nil` when none is known.

# `read`

```elixir
@type read() :: %{path: [segment()], position: position()}
```

One variable read.

# `segment`

```elixir
@type segment() :: String.t() | integer() | :dynamic
```

One step of a path: a name, a literal integer index, or `:dynamic` for any
other bracket key.

# `assignments`

```elixir
@spec assignments(input()) :: {:ok, [assignment()]} | {:error, error()}
```

Lists every location a program stores to, in source order.

The path is read off the assignment's location chain: the root variable,
then one segment per property or bracket access. A bracket key that is a
string or integer literal is that segment; any other key is `:dynamic`. An
expression stores nothing.

## Examples

    iex> {:ok, program} = Predicator.parse_program("a.b = 1; if ok { list[i] = 2 }")
    iex> Predicator.Analysis.assignments(program)
    {:ok, [%{path: ["a", "b"], position: {1, 1}}, %{path: ["list", :dynamic], position: {1, 18}}]}

    iex> {:ok, compiled} = Predicator.compile_program_with_positions("a['b'][0] = 1")
    iex> Predicator.Analysis.assignments(compiled)
    {:ok, [%{path: ["a", "b", 0], position: {1, 1}}]}

# `calls`

```elixir
@spec calls(input()) :: {:ok, [call()]} | {:error, error()}
```

Lists every function call in `input`, in source order, nested calls
included.

## Examples

    iex> Predicator.Analysis.calls("max(len(name), 3) > ns.limit()")
    {:ok,
     [
       %{name: "max", arity: 2, position: {1, 1}},
       %{name: "len", arity: 1, position: {1, 5}},
       %{name: "ns.limit", arity: 0, position: {1, 21}}
     ]}

    iex> {:ok, instructions} = Predicator.compile("max(len(name), 3) > 1")
    iex> {:ok, calls} = Predicator.Analysis.calls(instructions)
    iex> Enum.map(calls, &{&1.name, &1.arity})
    [{"max", 2}, {"len", 1}]

# `references`

```elixir
@spec references(input()) :: {:ok, [read()]} | {:error, error()}
```

Lists every variable read in `input`, in source order.

## Examples

    iex> Predicator.Analysis.references("user.tags[0] == 'x' or items[k].id > 1")
    {:ok,
     [
       %{path: ["user", "tags", 0], position: {1, 1}},
       %{path: ["items", :dynamic, "id"], position: {1, 24}},
       %{path: ["k"], position: {1, 30}}
     ]}

The instruction form gives the same paths, with positions only when the
envelope carries a table:

    iex> {:ok, instructions} = Predicator.compile("user.tags[0] == 'x'")
    iex> Predicator.Analysis.references(instructions)
    {:ok, [%{path: ["user", "tags", 0], position: nil}]}

    iex> {:ok, compiled} = Predicator.compile_with_spans("user.tags[0] == 'x'")
    iex> Predicator.Analysis.references(compiled)
    {:ok, [%{path: ["user", "tags", 0], position: {{1, 1}, {1, 13}}}]}

A program's reads include its bracket keys, never the location it writes:

    iex> {:ok, program} = Predicator.parse_program("totals[day] = totals[day] + amount")
    iex> {:ok, refs} = Predicator.Analysis.references(program)
    iex> Enum.map(refs, & &1.path)
    [["day"], ["totals", :dynamic], ["day"], ["amount"]]

---

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