Feel.Analysis.NullReasons (feel v0.1.0)

Copy Markdown View Source

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.

CauseThe run's codeWhen
a division by zero:division_by_zerothe divisor is zero; a zero to a negative power
too large to write:overflowthe result is past the range of decimal128
too small to write:overflowthe result is not zero and needs a digit below 1E-6176 (D-59)
no value for a power:argument_out_of_domaina negative base to an exponent that is not whole
too many digits:argument_out_of_domainan 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_rangea date, or a date and time, moved past year 999999999
a zone on one only:offset_mismatchthe 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

call_reason()

@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.

cause()

@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.

label()

@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.

reason()

@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

call_sentence(name, map)

@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|

find(operator, left, right, options \\ [])

@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]

find_call(name, types, options \\ [])

@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"]}

sentence(map, labels)

@spec sentence(reason(), {label(), label()}) :: String.t()

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"

show(number)

@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}]|