Feel.Syntax.Parser (feel v0.1.0)

Copy Markdown View Source

Parses a FEEL expression (DMN 1.6, grammar rules 1 and 2) from tokens.

This is a hand-written precedence-climbing parser. The grammar is ambiguous and gives precedence only by the order of its alternatives, so the levels below are the reading in the specification notes (01 §6). The parser reads the tokens of Feel.Syntax.Lexer and returns a Feel.AST expression node; it does not analyze or run anything.

The parser reads the whole expression grammar: literals, names, arithmetic, comparison, and, or, between, in, prefix tests, intervals, lists, context literals, if, for, some, every, function definitions, and the postfix forms (path, descendant, filter, call, instance of). parse_unary_tests/2 reads the other entry point, a list of unary tests. Types such as list<number> are read inside other nodes by Feel.Syntax.TypeParser, and on their own by Feel.Type.parse/2. The parser reads FEEL whatever the dialect: S-FEEL is checked on the tree it returns (Feel.Syntax.SFeel, D-49), and B-FEEL has the same grammar.

Precedence

From the loosest to the tightest. All binary operators are left associative.

LevelConstruct
1or
2and
3= != < <= > >= between in
4+ -
5* /
6**
7prefix - (tighter than **: -4 ** 2 is 16)
8instance of
9.name, ...name, [filter], (arguments)

The right operand of a comparison, and the bounds of between, are parsed at level 4 and above, so x < a + b is x < (a + b), a < b and c is (a < b) and c, and x between 1 and 10 and y is (x between 1 and 10) and y (notes 01, A27). A prefix test such as < a + b reads its operand the same way.

Brackets

(, [ and ] can start a parenthesized expression, a list, or an interval, and the tokens that tell them apart come after the first expression. The parser reads that expression once, and the token after it decides, so it never backtracks:

StartThen ..Then , or ]Then )
(interval, open at the startlist of tests (in only)parentheses
[interval, closed at the startlist
]interval, open at the start

An interval ends with ), ] or [. After in (, a comma makes a list of tests, so x in (1, 2) is {:in, meta, x, [1, 2]}; x in (a) and x in a are the same tree.

A [ right after the upper end of an interval is ambiguous (notes 01, A35): it opens a filter in [1..xs[i]] and closes the interval in ]1..5[. The token after it decides, as in the grammar of lezer-feel (decision D-28): a filter when that token can begin an expression, except ] and the comparison operators, which close the interval. ]1..a[ - 1 is therefore read with a filter and fails; write (]1..a[) - 1.

Names

A name is a run of tokens, and where it ends depends on the names in scope (decision D-05, D-06). At the start of an operand the parser takes the longest run of name tokens that is a name in the :scope or a built-in function (Feel.Builtin), so with a-b in scope, a - b is that name, and (a) - (b) is a subtraction. A longest match is dropped for a shorter one when the token after it could not follow an operand, which is the specification's "the longest match is preferred" without a full backtrack. A name that matches nothing is its first token alone, and the analyzer reports it.

After . and ..., and inside the brackets of a filter, names are keys of a context that exists only at run time (decision D-27). There a name is a name start word followed by further words that are not keywords, and x.age and y stops at and.

Constructs and bound names

if, for, some, every and function start an operand, and the last part of each (the else branch, the body, the test) is read at the lowest precedence, so it extends as far right as the enclosing brackets allow: 1 + if c then 2 else 3 is 1 + (if c then 2 else 3) (notes 01, A28).

The variables of for, some and every, and the parameters of a function, are names in the parts they scope, so for first name in xs return first name reads first name as one name in the body. A variable is visible in the domains after it, not in its own. A binding name ends at the first in (A23), so a variable cannot be called is in stock. Only a for domain can be from..to.

Context literals

The keys of a context are names and strings. A value can use any key of its context, including one that comes after it (notes 01, A22), so the keys are found first, by a scan of the tokens up to the closing brace, and are names while the values are read. A key cannot start with a keyword ({"if": 1} is the way to write it). Duplicate keys, cycles, and self-references are kept in the tree; the analyzer reports them.

Errors

The error is a list of Feel.Diagnostic with the code :syntax_error, :nesting_too_deep when the expression nests deeper than the :limits allow (the nesting of a type counts, D-154), or :name_too_long when a name that the expression gives (to a parameter, a variable, or a key) has more tokens than they allow. The parser never raises for its input.

Example

iex> {:ok, tokens} = Feel.Syntax.Lexer.tokenize("1 + 2")
iex> {:ok, {:binary, meta, :add, _left, _right}} = Feel.Syntax.Parser.parse(tokens)
iex> meta.span
{0, 5}

Summary

Types

Options of parse/2.

Functions

Parses tokens as one expression.

Parses tokens as a list of unary tests (DMN 1.6, grammar rule 15), as in the input entries of a decision table.

Types

option()

@type option() ::
  {:scope, Feel.Scope.t()}
  | {:limits, Feel.Limits.t()}
  | {:extensions, Feel.Extensions.t() | [atom()]}
  | {:index, Feel.Syntax.ScopeIndex.t() | nil}

Options of parse/2.

Functions

parse(tokens, options \\ [])

@spec parse([Feel.Syntax.Token.t()], [option()]) ::
  {:ok, Feel.AST.expression()} | {:error, [Feel.Diagnostic.t(), ...]}

Parses tokens as one expression.

The whole input has to be one expression. Any token left over is an error.

The tokens are what Feel.Syntax.Lexer.tokenize/2 gives. The keys of a context literal are found once, for all the literals, by the offset at which its { starts, which is unique among the tokens of the lexer; a list of tokens that is built by hand, with two tokens at the same offset, is not supported.

Options

  • :scope - a Feel.Scope.t/0 with the names that may contain spaces or operator characters, and the type names instance of may use.
  • :limits - a Feel.Limits.t/0; :max_nesting_depth and :max_name_tokens are used here.
  • :extensions - the extensions that are on (Feel.Extensions, D-81): the names of the functions they add are names of several words wherever they are, as the built-in ones are.

parse_unary_tests(tokens, options \\ [])

@spec parse_unary_tests([Feel.Syntax.Token.t()], [option()]) ::
  {:ok, Feel.AST.unary_tests()} | {:error, [Feel.Diagnostic.t(), ...]}

Parses tokens as a list of unary tests (DMN 1.6, grammar rule 15), as in the input entries of a decision table.

The result is {:unary_tests, meta, kind, tests}:

  • :any for a lone -, with no tests.
  • :negated for not( tests ) that is the whole input.
  • :positive for any other input: expressions separated by commas.

A test is an ordinary expression, so prefix tests, intervals, and the name ? need nothing special. not(x), 2 and not(x) and y are not negations: only an input that is not( followed by tests and the closing parenthesis is, and any other is a list of tests that calls the function not (notes 08, §4.8). That is the one place where the parser reads the input a second time.

Takes the options of parse/2.

iex> {:ok, tokens} = Feel.Syntax.Lexer.tokenize("< 5, [10..20]")
iex> {:ok, {:unary_tests, _meta, :positive, tests}} = Feel.Syntax.Parser.parse_unary_tests(tokens)
iex> Enum.map(tests, &elem(&1, 0))
[:prefix_test, :range]