Feel.Analysis.ScopeStack (feel v0.1.0)

Copy Markdown View Source

The names visible at one point of an expression, innermost first (DMN 1.6, section 10.3.2.11).

Evaluating an expression looks a name up in a stack of contexts. The first context that has the name wins, and nothing is merged. From the innermost out, the stack holds:

  1. The special contexts, innermost construct first: item and the keys of the element in a filter predicate; the iterator variables and partial in a for; the variables of some and every; the parameters of a function being defined; ? in a unary test.
  2. The local context, when the expression is the value of an entry of a context being built: the other entries of that context.
  3. The enclosing contexts of that one, inner to outer.
  4. The global context: the names the expression is given, which come from Feel.Scope.
  5. The built-in context, last. It is not stored here, since the built-in functions are a fixed table; a name that resolve/2 does not find may be a built-in, and the analyzer checks that after the stack.

Because lookup goes in this order, any construct may reuse a name that exists further out, including a built-in's, and shadows it inside its extent. {sum: 1, ...} makes sum a number there, and a call sum(...) in it then finds a value that is not a function.

Names in a stack are canonical (see Feel.Name).

Summary

Types

A binding: its kind and type, and for a local the id that its reads carry in their metadata (:local nodes tell which binding they are of) and, if it is bound to a condition, the origin of that condition (Feel.Analysis.Narrowing.origin/0).

What a name is bound to.

t()

Functions

Returns the number of contexts in the stack.

Starts a stack with the names of a scope as its global context.

Removes the innermost context. Raises when the stack is empty.

Pushes a context on the inside of the stack.

Looks a name up from the innermost context outwards.

Types

binding()

@type binding() :: %{
  :kind => binding_kind(),
  :type => Feel.Type.t(),
  optional(:id) => pos_integer(),
  optional(:origin) => Feel.Analysis.Narrowing.origin()
}

A binding: its kind and type, and for a local the id that its reads carry in their metadata (:local nodes tell which binding they are of) and, if it is bound to a condition, the origin of that condition (Feel.Analysis.Narrowing.origin/0).

binding_kind()

@type binding_kind() :: :var | :local | :item | :item_field

What a name is bound to.

  • :var - a name from the bindings.
  • :local - a variable bound by a construct, such as an iterator variable, a parameter, or a context entry. The analyzer keeps the locals in a stack of their own, and their type is the inferred type of what they are bound to, a Feel.Type.Union.
  • :item - the element in a filter predicate.
  • :item_field - a key of that element, known to exist.

frame()

@type frame() :: %{kind: frame_kind(), names: %{optional(Feel.Name.t()) => binding()}}

frame_kind()

@type frame_kind() :: :special | :local | :enclosing | :global

t()

@type t() :: %Feel.Analysis.ScopeStack{depth: non_neg_integer(), frames: [frame()]}

Functions

depth(scope_stack)

@spec depth(t()) :: non_neg_integer()

Returns the number of contexts in the stack.

new(scope)

@spec new(Feel.Scope.t()) :: t()

Starts a stack with the names of a scope as its global context.

iex> stack = Feel.Analysis.ScopeStack.new(Feel.Scope.put_name(%Feel.Scope{}, "age", :number))
iex> Feel.Analysis.ScopeStack.resolve(stack, "age")
{:ok, %{kind: :var, type: :number}, :global}

pop(stack)

@spec pop(t()) :: t()

Removes the innermost context. Raises when the stack is empty.

push(stack, kind, names)

@spec push(t(), frame_kind(), %{optional(Feel.Name.t()) => binding()}) :: t()

Pushes a context on the inside of the stack.

iex> stack = Feel.Analysis.ScopeStack.new(%Feel.Scope{})
iex> stack = Feel.Analysis.ScopeStack.push(stack, :special, %{"item" => %{kind: :item, type: :number}})
iex> Feel.Analysis.ScopeStack.resolve(stack, "item")
{:ok, %{kind: :item, type: :number}, :special}

resolve(scope_stack, name)

@spec resolve(t(), Feel.Name.t()) :: {:ok, binding(), frame_kind()} | :error

Looks a name up from the innermost context outwards.

Returns the binding and the kind of context it was found in, or :error when no context has the name. The built-in functions are not searched; see the module documentation.

iex> stack = Feel.Analysis.ScopeStack.new(Feel.Scope.put_name(%Feel.Scope{}, "x", :number))
iex> stack = Feel.Analysis.ScopeStack.push(stack, :special, %{"x" => %{kind: :local, type: :string}})
iex> Feel.Analysis.ScopeStack.resolve(stack, "x")
{:ok, %{kind: :local, type: :string}, :special}
iex> Feel.Analysis.ScopeStack.resolve(stack, "y")
:error