Feel.Analysis.Calls (feel v0.1.0)

Copy Markdown View Source

The part of analyzing a call of a built-in function that does not need the analyzer (D-56).

Feel.Analysis.Analyzer analyzes the arguments, which takes the whole analysis, and asks this module the rest:

  • callee/3 says what the callee of a call is: a built-in function that is written, one that is not, or a value (D-82): a name that the scope or a context around has, which shadows the built-in of the same name (D-26), a name that is nothing, or any other expression.
  • arrange/2 puts the arguments in the order of the parameters, which is the form both backends take (Feel.Builtin). Positional arguments are in order already, and their number has to be one that a signature accepts. Named arguments are for the first signature that has a parameter of each name (with the signatures that differ from it only in types, which the names do not tell apart), and a parameter they leave out is null: time(hour: 1, minute: 2, second: 3) is time(1, 2, 3, null), and is(value1: x) is is(x, null) (the TCK, 0103). Two arguments of one name, and a name no signature has, are errors, as is a wrong number of arguments.
  • mismatch/3 says whether the arguments, as the analyzer typed them, are ones that no signature that fits takes (D-56): context put(context: c, key: ["y", "a"], value: 2) has a list of two for a key that is a string. Only a certain mismatch is one, so an argument that may be null or of any type is not. Nor do the signatures take arguments that have to be of one kind and cannot be (D-155): before("a", 2), or max(d, 2) for a date d that is not null.
  • result_type/3 is the type of the result: what the signatures that the arguments may be for can give (abs has one for each kind it takes, and abs(d) of a duration is a duration), and null unless the function never gives it.

An error is not an analysis error: the call is a null with that error when it runs, as the TCK has it (day of year(value: @"1970-01-01") is null, with an error), and the analyzer warns. Type mismatches keep the ordinary runtime call boundary, which checks actual argument values and constructs its diagnostic independently of the analysis tier. Structural errors such as invalid arity carry a fixed diagnostic on the call instead.

Summary

Types

An argument in the order of the parameters: an expression, or :missing for a parameter the call leaves out.

Functions

Puts the arguments of a call in the order of the parameters of the function, with the signatures that the call fits. {:error, diagnostic} if it fits none.

Whether the arguments of a call, analyzed and in the order of the parameters, are ones that none of the signatures it fits takes (D-56): the call is then null with an :argument_out_of_domain error, which is what the function would say when it runs, but a function is given the values and not the signature that the names of a call chose, so one that reads a list for a key as keys does not (context put, TCK 1146 nested008).

The places (from 0) of the arguments of a call that are read quietly (D-100): those where every signature that the call fits has a quiet parameter (Feel.Builtin.Function.parameter/3).

The type of the result of a call: the types that the signatures give which the arguments may be for, and null too unless the function is never null.

The one signature that a call by name has chosen, if these are its signatures: those that differ only in types are one (Feel.Builtin.Function.merge_variants/1), so abs(n: x) has the signature of n that takes the kinds of all three. nil if there are others, or none.

The expressions of the arguments as they are written, for a call that has to be null with its error: they are analyzed all the same, so that their errors are reported.

Types

argument()

@type argument() :: Feel.AST.expression() | :missing

An argument in the order of the parameters: an expression, or :missing for a parameter the call leaves out.

Functions

arrange(function, arg)

Puts the arguments of a call in the order of the parameters of the function, with the signatures that the call fits. {:error, diagnostic} if it fits none.

iex> {:ok, function} = Feel.Builtin.function("time")
iex> {:ok, ast} = Feel.parse("time(minute: 2, hour: 1, second: 3)")
iex> {:call, _meta, _callee, arguments} = ast
iex> {:ok, arranged, _signatures} = Feel.Analysis.Calls.arrange(function, arguments)
iex> Enum.map(arranged, fn :missing -> :missing; {:literal, _, value} -> Decimal.to_integer(value) end)
[1, 2, 3, :missing]

callee(callee, stack, extensions \\ Feel.Extensions.none())

@spec callee(
  Feel.AST.expression(),
  Feel.Analysis.ScopeStack.t() | nil,
  Feel.Extensions.t()
) ::
  {:builtin, Feel.Builtin.Function.t()} | {:unwritten, Feel.Name.t()} | :value

What the callee of a call is.

iex> {:ok, ast} = Feel.parse(~s|date("2020-01-01")|)
iex> {:call, _meta, callee, _arguments} = ast
iex> {:builtin, function} = Feel.Analysis.Calls.callee(callee, nil)
iex> function.name
"date"

mismatch(function, signatures, arguments)

@spec mismatch(Feel.Builtin.Function.t(), [Feel.Builtin.Function.signature()], [
  Feel.IR.t()
]) ::
  :ok | {:error, Feel.Diagnostic.t()}

Whether the arguments of a call, analyzed and in the order of the parameters, are ones that none of the signatures it fits takes (D-56): the call is then null with an :argument_out_of_domain error, which is what the function would say when it runs, but a function is given the values and not the signature that the names of a call chose, so one that reads a list for a key as keys does not (context put, TCK 1146 nested008).

Only a certain mismatch is one. An argument that may be null or may be of any type is not, and the conversions of section 10.3.2.9.4 are allowed: a list of one element is that element where a value is wanted, and a value is the list of it where a list is wanted, and a date is a date and time where one is wanted (D-56). The length of a list is the one of the node that is a list literal or a constant (D-193), and unknown for any other. A parameter that the registry types any takes every argument, as does a signature the call is not the only one for.

A signature that says some arguments have to be of one kind (same_kind, D-155) rejects arguments whose kinds have nothing in common, though each is taken by its parameter: a string and a number are two points, and before takes no pair of them. The kinds an argument may be of are those of the members of its type that have an order (a number, a string, a date, a time, a date and time, a duration of either kind); null, Any and the name of a model type may be of every kind, as they are taken by every parameter, so one of them is never in disagreement. Nor is a list that may have no element, whose elements are not compared when it has none (max([]) is null, with no error); the items of a list literal or a constant are each one that the call has, and are compared. A range is not looked at: its type holds the range with no endpoint, which can be related to a point of any kind.

iex> const = fn value -> {:const, %{span: {0, 0}, type: Feel.Type.Union.of_value(value)}, value} end
iex> {:ok, function} = Feel.Builtin.function("context put")
iex> [key_signature | _] = function.signatures
iex> arguments = [const.(%{"x" => Decimal.new(1)}), const.(["y", "a"]), const.(Decimal.new(2))]
iex> {:error, diagnostic} = Feel.Analysis.Calls.mismatch(function, [key_signature], arguments)
iex> diagnostic.message
"context put is not defined for context, list, number"
iex> arguments = [const.(%{"x" => Decimal.new(1)}), const.(["y"]), const.(Decimal.new(2))]
iex> Feel.Analysis.Calls.mismatch(function, [key_signature], arguments)
:ok
iex> {:ok, before} = Feel.Builtin.function("before")
iex> {:error, diagnostic} = Feel.Analysis.Calls.mismatch(before, before.signatures, [const.("a"), const.(Decimal.new(2))])
iex> diagnostic.message
"before is not defined for string, number"
iex> Feel.Analysis.Calls.mismatch(before, before.signatures, [const.("a"), const.("b")])
:ok

quiet_places(signatures, count)

The places (from 0) of the arguments of a call that are read quietly (D-100): those where every signature that the call fits has a quiet parameter (Feel.Builtin.Function.parameter/3).

iex> {:ok, abs} = Feel.Builtin.function("abs")
iex> Feel.Analysis.Calls.quiet_places(abs.signatures, 1)
[]

result_type(function, signatures, arguments \\ nil)

The type of the result of a call: the types that the signatures give which the arguments may be for, and null too unless the function is never null.

A signature that the arguments are certainly not for (mismatch/3 says it of all of them for a call that is null with its error) is not one that the call can be answered by, so abs(duration("P1D")) is a duration, though abs has signatures for numbers and for the other kind of duration. Only a certain mismatch leaves a signature out: an argument that may be of any type is for every signature, and so is any argument of a function that does not check its arguments (Feel.Builtin.Function). A null that an argument may be is not for every signature as it is in mismatch/3, since what the call gives for a null is null, which its type has anyway, and the rest of the argument says which signature gives the other results: abs(x) of a number that may be null is a number that may be null, and not a duration. A parameter that takes null among its types takes it, and an argument that is only null is for every signature. With no argument that tells them apart, the result is that of all the signatures. Without the list of the arguments at all (nil, the default) the signatures alone are asked about: the types they declare for the result, and not those that follow the arguments (result_from), and no rule of a family (Feel.Analysis.Images) is asked. A call with no argument (now(), concatenate()) is the empty list, and a rule may answer it.

iex> {:ok, function} = Feel.Builtin.function("date")
iex> signatures = Feel.Builtin.Function.with_arity(function, 1)
iex> Feel.Analysis.Calls.result_type(function, signatures)
[:date, :null]
iex> {:ok, is} = Feel.Builtin.function("is")
iex> Feel.Analysis.Calls.result_type(is, is.signatures)
[:boolean]
iex> {:ok, abs} = Feel.Builtin.function("abs")
iex> Feel.Analysis.Calls.result_type(abs, abs.signatures)
[:number, :years_and_months_duration, :days_and_time_duration, :null]
iex> duration = {:var, %{span: {0, 0}, type: [:days_and_time_duration]}, "d"}
iex> abs |> Feel.Analysis.Calls.result_type(abs.signatures, [duration]) |> Feel.Type.Union.format()
"days and time duration in [PT0S..max]"
iex> maybe_number = {:var, %{span: {0, 0}, type: [:number, :null]}, "x"}
iex> abs |> Feel.Analysis.Calls.result_type(abs.signatures, [maybe_number]) |> Feel.Type.Union.format()
"number in [0..max] | Null"

single(signatures)

The one signature that a call by name has chosen, if these are its signatures: those that differ only in types are one (Feel.Builtin.Function.merge_variants/1), so abs(n: x) has the signature of n that takes the kinds of all three. nil if there are others, or none.

iex> {:ok, abs} = Feel.Builtin.function("abs")
iex> {:ok, _arranged, signatures} = Feel.Analysis.Calls.arrange(abs, {:named, [{["n"], {:literal, %{span: {0, 0}}, Decimal.new(1)}}]})
iex> length(signatures)
3
iex> Enum.map(Feel.Analysis.Calls.single(signatures).parameters, & &1.types)
[[:number, :days_and_time_duration, :years_and_months_duration]]
iex> Feel.Analysis.Calls.single([])
nil

written(arg)

@spec written(Feel.AST.arguments()) :: [Feel.AST.expression()]

The expressions of the arguments as they are written, for a call that has to be null with its error: they are analyzed all the same, so that their errors are reported.