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/2reads 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/1or from storage; - a
Predicator.Compiledenvelope.
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 (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 Predicator.Parser.visitable/0 and the instruction clauses in
step with Predicator.Instructions.opcodes/0, the retired opcodes included.
Summary
Types
One location a program stores to.
One function call.
The error arm: a parse failure for a source string, or an instruction the analysis cannot read.
Anything the three functions accept.
A source annotation: a point position, a span, or nil when none is known.
One variable read.
One step of a path: a name, a literal integer index, or :dynamic for any
other bracket key.
Functions
Lists every location a program stores to, in source order.
Lists every function call in input, in source order, nested calls
included.
Lists every variable read in input, in source order.
Types
One location a program stores to.
@type call() :: %{name: String.t(), arity: non_neg_integer(), position: position()}
One function call.
@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.
@type input() :: String.t() | Predicator.Parser.visitable() | Predicator.Types.instruction_list() | Predicator.Compiled.t()
Anything the three functions accept.
@type position() :: Predicator.Parser.position()
A source annotation: a point position, a span, or nil when none is known.
One variable read.
One step of a path: a name, a literal integer index, or :dynamic for any
other bracket key.
Functions
@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}}]}
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}]
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"]]