Feel.Analysis.Invocations (feel v0.1.0)

Copy Markdown View Source

The type of the result of the call of a value, f(1), (function(a) a)(2), g(3)(4) (D-82), and what can be said of the call before it runs.

The callee has a type, a union of what the value can be, and each alternative gives a result:

  • a function function<T1, ..., Tn> -> R bounds a positional call by R when its parameter domains cover all arguments. Another arity or an uncovered argument domain gives Any | Null: the value can have unrecorded signatures. A call by name is not looked at for the number: the type of a function has the types of the parameters and not their names, so it cannot be known which ones the call supplies. A call with another number of arguments is Any or null: a function type says that the function has an arrow within it and not that it has no other, and a built-in function bound to a name declared function<number> -> number (ceiling, which takes a scale as well) has the arity (D-171). That the call is null is known of a function written in the expression, which the analyzer finds out where the function is known (D-193), but not from an overload type alone;
  • an overload (D-171), a built-in function with several signatures, gives what the arrows that take this many arguments give, made narrower by each refinement whose parameters hold the arguments (Feel.Type.Arrows.application/2). An unrecorded arity or argument domain starts at Any | Null, since the recorded arrows need not be exhaustive;
  • a named call of a function or overload gives Any | Null: parameter names are absent from the type, and the names can select an unrecorded signature with a different result. Known function values are handled separately;

  • Any, and a named type that is not looked into, gives Any or null;
  • null gives null;
  • anything else (a number, a list, ...) is not a function, and gives null.

When the function is known (a function written in the expression, called at once or through the key of a context), the analyzer types the call from its arguments instead (D-193): bind/2 says which argument goes to which parameter, and parameter_types/2 the type each parameter has in a body that is analyzed again. Complete single-signature recursion peers also have separate evidence: a named call is bounded by the written result or null, or only null if it supplies more names than the signature has parameters. This does not prove that the names match; actual binding still checks unknown and duplicate names.

A call that can only be null for the last two reasons is warned about when the callee is not null or a name that is unknown (which is reported as such): the warning says the callee is not a function, or that the number of arguments is not the number of parameters. It is a warning and not an error, as for a built-in (D-56); the run gives the null and reports the error.

Summary

Types

What is known of the arguments of a call: their types, by position, or by name.

What a call of the callee can certainly not do, when it is the same for all the callee can be.

Functions

The types of the arguments of a call in the order of the parameters of a function that has these (name, type) parameters. null is the type of a parameter that a call by name leaves out. :error when the call does not fit: another number of arguments, a name that is no parameter's, a name given twice.

The type of a call that was analyzed again for its arguments (D-193), specialized, once the function types it holds are put right.

The type a parameter has in the body when the call has an argument of this type for it, and whether every argument conforms to its parameter.

The type of the result of a call, and the problem that every function the callee can be has with it, if there is one.

Types

arguments()

@type arguments() ::
  {:positional, [Feel.Type.Union.t()]}
  | {:named, [{Feel.Name.t(), Feel.Type.Union.t()}]}

What is known of the arguments of a call: their types, by position, or by name.

problem()

@type problem() :: {:not_a_function, [atom()]}

What a call of the callee can certainly not do, when it is the same for all the callee can be.

Functions

bind(parameters, arg)

@spec bind([{Feel.Name.t(), Feel.Type.Union.t()}], arguments()) ::
  {:ok, [Feel.Type.Union.t()]} | :error

The types of the arguments of a call in the order of the parameters of a function that has these (name, type) parameters. null is the type of a parameter that a call by name leaves out. :error when the call does not fit: another number of arguments, a name that is no parameter's, a name given twice.

iex> parameters = [{"a", [:number, :null]}, {"b", [:string, :null]}]
iex> Feel.Analysis.Invocations.bind(parameters, {:positional, [[:number], [:string]]})
{:ok, [[:number], [:string]]}
iex> Feel.Analysis.Invocations.bind(parameters, {:named, [{"b", [:string]}]})
{:ok, [[:null], [:string]]}
iex> Feel.Analysis.Invocations.bind(parameters, {:positional, [[:number]]})
:error
iex> Feel.Analysis.Invocations.bind(parameters, {:named, [{"c", [:string]}]})
:error

declared_closures(specialized, unspecialized)

@spec declared_closures(Feel.Type.Union.t(), Feel.Type.Union.t()) ::
  Feel.Type.Union.t()

The type of a call that was analyzed again for its arguments (D-193), specialized, once the function types it holds are put right.

The body of the function is analyzed again for the types of the arguments, and a function that the body writes (function(h) function(a) h gives one) is typed in that analysis: with h at the type of the argument. The function that the run holds is the one that was written, with its parameters and its kind-only result signature, and that is what instance of and a declared type ask about (Feel.Value.Function.types_at/2). So a function that the call gives and that the body writes is an overload (D-171) of the arrow as it was written, which is what the algebra sees, with the arrow of this analysis as a refinement, which types a call and nothing else. unspecialized is the type that the callee's own type gives the call, where the body is as it was written. Equal arrow types do not prove that the call returns an argument unchanged: a new closure can acquire that same type during specialization. Only a unique written arrow is paired; otherwise the unspecialized function alternatives are retained. A literal body already carries its fixed arrow with the specialized call refinement and keeps that overload intact. Arbitrary aliases and selection results still use the conservative correspondence rules. This loses precision for identity functions and for every closure in a returned list, range or context, whose result is widened to Any | Null below ({g: function(a) {h: function(c) a + 1}}.g(1).h(2) is Any | Null, where a written signature that was known gave number | Null).

iex> written = {:function, [[:any, :null]], [:any, :null]}
iex> specialized = {:function, [[:any, :null]], [:number]}
iex> Feel.Analysis.Invocations.declared_closures([specialized], [written, :null])
[{:overload, [written], [specialized]}]
iex> Feel.Analysis.Invocations.declared_closures([specialized], [written])
[{:overload, [written], [specialized]}]
iex> Feel.Analysis.Invocations.declared_closures([:number], [written])
[:number]

A function in a list or a context that the call gives is not paired with an arrow of the callee's type, which has no shape of its container (D-193): it is typed by its parameters and Any | Null, the result that a function written without one has.

iex> specialized = {:function, [[:any, :null]], [:number]}
iex> container = {:context, %{"h" => [specialized]}}
iex> Feel.Analysis.Invocations.declared_closures([container], [:any, :null])
[{:context, %{"h" => [{:function, [[:any, :null]], [:any, :null]}]}}]
iex> Feel.Analysis.Invocations.declared_closures([{:list, []}], [:any, :null])
[{:list, []}]

parameter_types(declared, arguments)

@spec parameter_types([Feel.Type.Union.t()], [Feel.Type.Union.t()]) ::
  {[Feel.Type.Union.t()], boolean()}

The type a parameter has in the body when the call has an argument of this type for it, and whether every argument conforms to its parameter.

An argument that is within the declared type of its parameter is the value of the parameter as it is, so the parameter has the type of the argument. One that may not be is converted to the declared type or the call is null (Feel.Runtime.Invoke), so the body sees a value of the declared type, and the call may be null.

iex> declared = [[:number, :null], [:string, :null]]
iex> Feel.Analysis.Invocations.parameter_types(declared, [[:number], [:string]])
{[[:number], [:string]], true}
iex> Feel.Analysis.Invocations.parameter_types(declared, [[:number], [:number]])
{[[:number], [:string, :null]], false}

result(callee, arguments)

@spec result(Feel.Type.Union.t(), arguments()) ::
  {Feel.Type.Union.t(), problem() | nil}

The type of the result of a call, and the problem that every function the callee can be has with it, if there is one.

iex> function = [{:function, [[:number, :null]], [:number]}]
iex> Feel.Analysis.Invocations.result(function, {:positional, [[:number]]})
{[:number], nil}
iex> Feel.Analysis.Invocations.result(function, {:positional, [[:string]]})
{[:any, :null], nil}
iex> Feel.Analysis.Invocations.result(function, {:positional, []})
{[:any, :null], nil}
iex> overload = [{:overload, [{:function, [[:number, :null]], [:number]}], []}]
iex> Feel.Analysis.Invocations.result(overload, {:positional, []})
{[:any, :null], nil}
iex> Feel.Analysis.Invocations.result([:number], {:positional, []})
{[:null], {:not_a_function, [:number]}}