Why an operation can be null for the values of its operands (D-161, step B).
A type has Null only if a value can cause it (D-161), and a modeler who meets one has to be told what to change. The
analysis of the sets says that an operation can be null (Feel.Analysis.Intervals, DateArithmetic and the rest);
this module says which values do it, by asking the run. For the operands' types it takes the values at which an
operation is most likely to fail (Feel.Type.Witness.edges/2: zero, the smallest numbers there are, the ends of the sets,
the largest number, the first and last day of the calendar), runs the operation on every pair
(Feel.Runtime.Arithmetic.operate/3), and keeps for each cause the first pair that the run turns down. So a reason is a
fact and not a guess: the pair is in the operands' types, and the run says null for it, with the code that the run gives.
| Cause | The run's code | When |
|---|---|---|
| a division by zero | :division_by_zero | the divisor is zero; a zero to a negative power |
| too large to write | :overflow | the result is past the range of decimal128 |
| too small to write | :overflow | the result is not zero and needs a digit below 1E-6176 (D-59) |
| no value for a power | :argument_out_of_domain | a negative base to an exponent that is not whole |
| too many digits | :argument_out_of_domain | an amount of 34 digits or more, which is not a number (the months of a duration) |
| a year out of the calendar | :date_out_of_range | a date, or a date and time, moved past year 999999999 |
| a zone on one only | :offset_mismatch | the difference of a value that has a zone and one that has none |
The search is not complete: a cause whose values are not at an edge is missed. It is sound, though, and it tests the
analysis from the other side: where the sets say an operation can be null and no pair of the edges does it, the sets are
not exact there (test/feel/analysis/null_reasons_test.exs looks for that).
iex> a = [{:number, Feel.Type.NumberSet.interval(Decimal.new(0), Decimal.new(150))}]
iex> b = [{:number, Feel.Type.NumberSet.point(Decimal.new("0.5"))}]
iex> [%{cause: :too_small, left: left}] = Feel.Analysis.NullReasons.find(:multiply, a, b)
iex> Decimal.to_string(left)
"1E-6176"
Summary
Types
A call of a built-in function that is null for these values of its arguments, and the error the run gives, if any.
What makes the run turn the operation down.
What an operand is called in a sentence: its name, nil for an expression, :constant for a value that is written.
A pair of values of the operands for which the run gives null, and why.
Functions
The sentence that tells one reason a call is null: the call with the values, and what the run says.
The reasons an arithmetic operation can be null, for the types of its operands: for each cause that some pair of edge
values shows, the first pair. The operands' null, if they have it, is left out: it is the operand that is null, and not
the operation.
The reasons a call of the built-in function name can be null, for the types of its arguments: for each cause (the code
of the error the run gives, or :null for a null with no error), the first values of the arguments that the run turns down.
The values are those at the edges of the types (Feel.Type.Witness.edges/2), at most :max_calls of them (256), and the
function is the run's own (Feel.Builtin.call/4). The arguments' null, if they have it, is left out.
The sentence that tells one reason, with the names of the operands (nil for an operand that is not a name, :constant
for one that is a value written in the expression, which is not told).
A value as it is written in a reason: a number of a size that is not usual in the scientific notation (1E-6176, and not
the six thousand digits of it), a duration of such an amount in its seconds or months.
Types
@type call_reason() :: %{ cause: atom(), arguments: [Feel.Value.t()], message: String.t() | nil }
A call of a built-in function that is null for these values of its arguments, and the error the run gives, if any.
@type cause() ::
:division_by_zero
| :too_large
| :too_small
| :out_of_domain
| :too_many_digits
| :date_out_of_range
| :offset_mismatch
What makes the run turn the operation down.
@type label() :: String.t() | nil | :constant
What an operand is called in a sentence: its name, nil for an expression, :constant for a value that is written.
@type reason() :: %{cause: cause(), left: Feel.Value.t(), right: Feel.Value.t()}
A pair of values of the operands for which the run gives null, and why.
Functions
@spec call_sentence(String.t(), call_reason()) :: String.t()
The sentence that tells one reason a call is null: the call with the values, and what the run says.
iex> reason = %{cause: :argument_out_of_domain, arguments: ["", Decimal.new(0)], message: "start position is 0"}
iex> Feel.Analysis.NullReasons.call_sentence("substring", reason)
~s|substring("", 0) is null: start position is 0|
@spec find(Feel.IR.arithmetic_operator(), Feel.Type.Union.t(), Feel.Type.Union.t(), same: boolean(), grains: {integer() | nil, integer() | nil} ) :: [reason()]
The reasons an arithmetic operation can be null, for the types of its operands: for each cause that some pair of edge
values shows, the first pair. The operands' null, if they have it, is left out: it is the operand that is null, and not
the operation.
With same: true the two operands are one name (d / d), so they have one value and only a pair of equal values is a reason.
iex> d = [{:number, Feel.Type.NumberSet.interval(Decimal.new(0), Decimal.new(150))}]
iex> Enum.map(Feel.Analysis.NullReasons.find(:divide, d, d), & &1.cause)
[:division_by_zero, :too_large, :too_small]
iex> Enum.map(Feel.Analysis.NullReasons.find(:divide, d, d, same: true), & &1.cause)
[:division_by_zero]
@spec find_call(String.t(), [Feel.Type.Union.t()], [{:max_calls, pos_integer()}]) :: [ call_reason() ]
The reasons a call of the built-in function name can be null, for the types of its arguments: for each cause (the code
of the error the run gives, or :null for a null with no error), the first values of the arguments that the run turns down.
The values are those at the edges of the types (Feel.Type.Witness.edges/2), at most :max_calls of them (256), and the
function is the run's own (Feel.Builtin.call/4). The arguments' null, if they have it, is left out.
iex> a = [{:number, Feel.Type.NumberSet.interval(Decimal.new(0), Decimal.new(150))}]
iex> [reason] = Feel.Analysis.NullReasons.find_call("log", [a])
iex> {reason.cause, Enum.map(reason.arguments, &Decimal.to_string/1)}
{:argument_out_of_domain, ["0"]}
The sentence that tells one reason, with the names of the operands (nil for an operand that is not a name, :constant
for one that is a value written in the expression, which is not told).
iex> reason = %{cause: :division_by_zero, left: Decimal.new(1), right: Decimal.new(0)}
iex> Feel.Analysis.NullReasons.sentence(reason, {:constant, "x"})
"a division by zero when x = 0"
iex> reason = %{cause: :too_small, left: Decimal.new(1, 1, -6176), right: Decimal.new("0.5")}
iex> Feel.Analysis.NullReasons.sentence(reason, {"x", :constant})
"a result too small to write when x = 1E-6176"
@spec show(Feel.Value.t()) :: String.t()
A value as it is written in a reason: a number of a size that is not usual in the scientific notation (1E-6176, and not
the six thousand digits of it), a duration of such an amount in its seconds or months.
iex> Feel.Analysis.NullReasons.show(Decimal.new(1, 1, -6176))
"1E-6176"
iex> Feel.Analysis.NullReasons.show(Decimal.new(150))
"150"
iex> Feel.Analysis.NullReasons.show(Feel.Value.DaysAndTimeDuration.new(Decimal.new(1, 1, -6176)))
"PT1E-6176S"
iex> Feel.Analysis.NullReasons.show(["a", nil, %{"k" => true}])
~s|["a", null, {k: true}]|