Feel.Limits (feel v0.1.0)

Copy Markdown View Source

Caps on how much work an expression may cause.

Expressions can be hostile or simply mistaken: for i in 1..1000000000 return i would otherwise try to build a billion-element list. Limits are checked in the shared runtime, so every backend enforces them. A limit is a non-negative integer or :infinity.

The defaults are provisional and need tuning against real workloads.

Fields

  • :max_source_bytes - the size of the source text. Checked when parsing.
  • :max_nesting_depth - how deeply expressions may nest. Checked when parsing. The nesting of a type counts with the expression it is in: list<list<number>>, the member types of a context<>, the parameters and the result of a function<> (D-154).
  • :max_name_tokens - how many tokens a name that the expression itself gives may have: the name of a parameter of a function, of the variable of a for, some or every, and a key of a context written as a name (first name is two tokens, a-b three). Checked when parsing, with the code :name_too_long. The names of the :scope are the host's and are not limited, and nor is a key written as a string, which is a key whatever its length and can be read as a name if it is not longer than this (D-154).
  • :max_narrowing_facts - how many facts the conditions of an expression may compose about one subject (a name, a local, a path from one) that cannot be answered from what is known of the type the subject is read at, and have to be applied to it at each read (x > 1 and 1 / x > 0 and x > 2 …, with x read in an operation): the work for a subject is the square of the number. The facts that are answered cost no more for the next and are not counted, so a thousand x = "a" or x = "b" or … is within any limit. Checked when analyzing, with the code :too_many_facts (D-154).
  • :max_generated_statements - how many statements the BEAM backend may generate for one expression (the statements of all the functions of the module, the run_1, run_2, … that a long body is cut into among them). The Erlang compiler takes about 20 000 reductions and 6 to 28 KB of memory for each, whatever the expression is: 10 000 of them are about 200 million reductions and 60 to 280 MB at the peak, and the 100 000 bytes that :max_source_bytes lets in can make 50 000 (x + x + …: 1.4 GB). The interpreter makes none, and ignores this. Checked when preparing with backend: :beam, with the code :too_large_to_compile (D-154).
  • :max_list_length - the length of any list a run builds. A for is held to it as it makes its list, and ends at the first result over it (D-154), and so is a built-in function whose list can be as long as the product of the sizes of its arguments (flatten, concatenate, union, split, extract, get entries): it is refused when the items counted pass it, and the list is not made. Any other list is checked when it is made. Lists nested in the returned value are also checked, including folded constants and host bindings.
  • :max_string_length - the byte size of any string a run builds. A built-in function whose string can be as long as the product of the sizes of its arguments (string join, replace, string and to json of a list or a context) is refused when the bytes counted pass it, and the string is not made. Any other string is checked when it is made. Strings nested in the returned value are checked as well.
  • :max_iterations - the total number of elements that the iterators of for, some and every take in one run, across all of them and the ones inside them; a range from..to is walked and not made, so it counts as it goes (D-86).
  • :regex_match_limit - the backtracking budget handed to the regular-expression engine for matches, replace, and split.
  • :max_call_depth - how many calls of a function value may be inside one another (D-82).
  • :max_calls - the total number of calls of a function of FEEL in one run, nested or not (D-154). The depth does not bound it: a function that calls itself twice at each level makes about 2ⁿ calls at a depth of n. The default leaves room for sort of the longest list a run can build with a function of FEEL as its order (about 1.5 million comparisons).
  • :max_diagnostics - how many diagnostics a run may report, warnings and infos with the errors (D-154). A run that reports one in every turn of a loop or call of a function makes as many as it has turns, each about 300 bytes, and max_iterations and max_calls do not bound them. The run that would report one more ends, in either mode, with the diagnostics it has in the error. A strict run ends at its first error whatever this is. A deadline or process heap cap is not part of this struct. Public Feel calls supply these by default through Feel.Worker, with shared binaries included at garbage collection. Early per-value construction checks remain active with either sandbox setting. There is no cumulative allocation quota; a process heap policy is not a complete VM memory bound. See the security model for worker ownership, nested policies and host responsibilities.

Summary

Types

A non-negative integer, or :infinity for no limit.

t()

Functions

Returns the default limits.

Builds limits from the defaults, overriding the given fields.

Whether a value is a limits struct with valid fields, including structs built without new/1.

Types

limit()

@type limit() :: non_neg_integer() | :infinity

A non-negative integer, or :infinity for no limit.

t()

@type t() :: %Feel.Limits{
  max_call_depth: limit(),
  max_calls: limit(),
  max_diagnostics: limit(),
  max_generated_statements: limit(),
  max_iterations: limit(),
  max_list_length: limit(),
  max_name_tokens: limit(),
  max_narrowing_facts: limit(),
  max_nesting_depth: limit(),
  max_source_bytes: limit(),
  max_string_length: limit(),
  regex_match_limit: limit()
}

Functions

default()

@spec default() :: t()

Returns the default limits.

iex> Feel.Limits.default().max_nesting_depth
100

new(overrides \\ [])

@spec new(keyword(limit())) :: t()

Builds limits from the defaults, overriding the given fields.

Raises ArgumentError for an unknown field or for a value that is neither a non-negative integer nor :infinity.

iex> Feel.Limits.new(max_list_length: 10, max_iterations: :infinity).max_list_length
10

valid?(limits)

@spec valid?(term()) :: boolean()

Whether a value is a limits struct with valid fields, including structs built without new/1.