Predicator.Vocabulary (predicator v9.4.0)

Copy Markdown View Source

The grammar's fixed vocabulary, enumerated for editor tooling.

An expression editor that offers completion needs to know what the language accepts: which operators exist, which words are reserved, which duration units follow a number, and which functions are callable. Function names have always been reachable - Predicator.Context.new/2 resolves them from Predicator.FunctionProvider modules - but the operator and keyword lexemes lived only inside Predicator.Lexer's private clause heads, so a consumer wanting completion had to hard-code a second copy of the grammar and keep it in sync by hand. This module is the first copy made public instead.

It is a reading surface, not a parsing one. Nothing here participates in lexing, parsing, compiling, or evaluating; the entries describe the grammar the lexer already implements, and adding a category or a doc string changes no program's meaning. test/predicator/vocabulary_sync_test.exs binds the two together: it round-trips every enumerated lexeme through Predicator.Lexer.tokenize/1, and it checks the enumeration against the lexer's own Predicator.Lexer.token/0 union, its classify_identifier/1 clause heads, and its duration_unit?/1 clause heads, so a token added to the lexer without a matching entry here turns the suite red rather than quietly shipping an editor that cannot complete it.

Entries

Every static entry is a map with at least these five keys:

  • :lexeme - the exact source text, e.g. ">=", "contains"
  • :token_type - the Predicator.Lexer.token/0 tag it lexes to
  • :category - one of categories/0
  • :display - what an editor shows in a completion list, which differs from the lexeme only where the bare lexeme reads badly on its own ("a + b" for "+", "1d" for the duration unit "d")
  • :doc - one line of prose, sentence case, no trailing period

A function entry carries the same five keys plus :arity, since a function's arity is part of what an editor needs to complete a call. Its :doc is nil: a provider binds a name to {arity, atom} and carries no description, so there is nothing truthful to put there, and an invented sentence in a documented field is worse than an absent one.

Operator entries

An entry in an operator category - the six categories operators/0 returns - carries four more keys, because a structured editor needs more about an operator than a completion list does. A picklist has to name the operator in a sentence, know how many operands it takes, know which atom the parser will put in the AST for it, and know which kinds of value it is worth offering it for at all:

  • :label - a short phrase read in place of the operator, e.g. "is at least" for ">=". :display is a template and :doc is a sentence of prose about the semantics; neither is a UI string, which is why this is its own key
  • :arity - how many operands the operator takes. "-" takes [1, 2], since it is both subtraction and negation
  • :ast_op - the atom Predicator.Parser.parse/2 puts in the node it builds, which differs from :token_type for three operators (:in_op becomes :in, :contains_op becomes :contains, :strict_equal becomes :strict_eq). nil where no node carries the operator as an atom of its own
  • :value_kinds - the value_kind/0s the operator is worth offering for, as the kind of the value on its right. nil for an operator that does not compare a field against a value at all - the logical, arithmetic, temporal and cast categories

Entries outside an operator category carry none of the four. That is the shape function_entry/0 already has against entry/0: an entry carries the keys its kind of thing has, and no placeholder keys for the ones it does not.

What :value_kinds admits, and two deliberate exclusions

The admissions follow Predicator.Evaluator's own comparison semantics. Ordered comparison (>, >=, <, <=) is admitted where the evaluator orders the kind meaningfully: numbers and strings through its types_match guard, dates and datetimes chronologically, durations as the numbers they reduce to. Equality is admitted for every scalar kind. in takes a list on its right and nothing else; contains takes a list on its left, so the value beside it is a scalar of any kind.

Two things the evaluator does answer are still not admitted, because a picklist offering them would be offering nonsense:

ExcludedWhy
Ordered comparison of booleanstypes_match admits them, so true > false evaluates - by Erlang term order, which is not a fact about the author's data
Equality against a listLists compare with ==, but the operator an author reaches for beside a list is in, and offering both invites writing the one that is almost never meant

Neither exclusion narrows the grammar. Both are the kind of judgement Predicator.Simple already records for the shapes it leaves out: the language still accepts them, and the structured surface still does not offer them.

Case

The word operators are accepted in two cases - and and AND both lex to :and_op - and both are enumerated, as separate entries with the same :token_type. An editor offering only one of them would be offering a house style the grammar does not have. if, else, while, and the temporal words are lower-case only, and are enumerated only that way.

Examples

iex> Predicator.Vocabulary.by_category(:arithmetic) |> Enum.map(& &1.lexeme)
["+", "-", "*", "/", "%"]

iex> Predicator.Vocabulary.tokens() |> Enum.find(&(&1.lexeme == ">=")) |> Map.take([:token_type, :category])
%{token_type: :gte, category: :comparison}

iex> Predicator.Vocabulary.functions() |> Enum.any?(&(&1.lexeme == "len"))
true

Summary

Types

The kind of thing an entry is, which is what an editor groups a completion list by.

A fixed lexeme of the grammar - an operator, keyword, literal word, separator, or duration unit.

A callable function, resolved from the providers rather than from the lexer.

An operator entry: an entry/0 plus what a structured editor needs in order to offer the operator as a choice. See "Operator entries" above.

The kind of a value on the right of an operator, as a structured editor models it.

Functions

Every entry: tokens/0 followed by functions/1 on the same opts.

The entries in one category.

Every category an entry can carry, in the order tokens/0 groups them.

The callable functions, resolved the same way Predicator.Context.new/2 resolves them.

The word-shaped entries: everything an editor must not offer as a plain identifier, because the lexer classifies it as something else.

The entries that combine or compare values: the comparison, logical, arithmetic, membership, temporal, and cast categories.

Every fixed lexeme of the grammar: operators, keywords, literal words, brackets, separators, and duration units.

Every kind of value an operator can be offered for.

Types

category()

@type category() ::
  :comparison
  | :logical
  | :arithmetic
  | :membership
  | :temporal
  | :control
  | :literal
  | :grouping
  | :punctuation
  | :cast
  | :duration_unit
  | :function

The kind of thing an entry is, which is what an editor groups a completion list by.

:cast holds the single :: postfix operator (ADR-0011); :grouping holds the bracket pairs; :punctuation holds the separators that are neither.

entry()

@type entry() :: %{
  lexeme: binary(),
  token_type: atom(),
  category: category(),
  display: binary(),
  doc: binary()
}

A fixed lexeme of the grammar - an operator, keyword, literal word, separator, or duration unit.

function_entry()

@type function_entry() :: %{
  lexeme: binary(),
  token_type: :function_name | :qualified_function_name,
  category: :function,
  display: binary(),
  doc: nil,
  arity: Predicator.Evaluator.function_arity()
}

A callable function, resolved from the providers rather than from the lexer.

:token_type is :qualified_function_name for a namespaced name ("Math.abs") and :function_name for a bare one ("len"), matching what the lexer produces for a call to it.

operator_entry()

@type operator_entry() :: %{
  lexeme: binary(),
  token_type: atom(),
  category: category(),
  display: binary(),
  doc: binary(),
  label: binary(),
  arity: 0 | 1 | 2 | [1 | 2, ...],
  ast_op: atom() | nil,
  value_kinds: [value_kind()] | nil
}

An operator entry: an entry/0 plus what a structured editor needs in order to offer the operator as a choice. See "Operator entries" above.

value_kind()

@type value_kind() ::
  :string | :number | :boolean | :date | :datetime | :duration | :list

The kind of a value on the right of an operator, as a structured editor models it.

These are the kinds Predicator.Simple admits as a scalar or as a list of them, and no others.

:number covers integer and float literals together. They are two shapes in Predicator.Simple.scalar/0, because the AST literal differs and the round-trip has to preserve which one was written, but they are one kind here: every operator worth offering beside 19.99 is worth offering beside 500, so a :float kind would duplicate :number's list exactly and make every editor branch on a distinction that changes nothing it renders (px-gv1).

There is no :relative_date either, because 3d ago is a datetime by the time anything compares it.

Functions

all(opts \\ [])

@spec all(keyword()) :: [entry() | operator_entry() | function_entry(), ...]

Every entry: tokens/0 followed by functions/1 on the same opts.

Examples

iex> length(Predicator.Vocabulary.all()) == length(Predicator.Vocabulary.tokens()) + length(Predicator.Vocabulary.functions())
true

by_category(category)

@spec by_category(category()) :: [entry() | operator_entry() | function_entry()]

The entries in one category.

The argument is guarded against categories/0, so a misspelled category raises FunctionClauseError rather than returning an empty list that reads like a category the grammar happens not to use.

Examples

iex> Predicator.Vocabulary.by_category(:cast) |> Enum.map(& &1.lexeme)
["::"]

iex> Predicator.Vocabulary.by_category(:literal) |> Enum.map(& &1.lexeme)
["true", "false", "null", "undefined"]

categories()

@spec categories() :: [category(), ...]

Every category an entry can carry, in the order tokens/0 groups them.

Examples

iex> :duration_unit in Predicator.Vocabulary.categories()
true

functions(opts \\ [])

@spec functions(keyword()) :: [function_entry()]

The callable functions, resolved the same way Predicator.Context.new/2 resolves them.

opts takes :builtins, :providers, and :functions, and is passed straight to Predicator.Context.resolve_functions/1, so the names returned here are exactly the names a context built with the same options will accept - including a host's own providers, which is the case an editor embedded in a host application actually has.

Entries are sorted by name, since a dispatch map has no order and a completion list needs one.

Examples

iex> Predicator.Vocabulary.functions(builtins: false)
[]

iex> Predicator.Vocabulary.functions() |> Enum.find(&(&1.lexeme == "len")) |> Map.take([:display, :arity, :doc])
%{display: "len(...)", arity: 1, doc: nil}

iex> Predicator.Vocabulary.functions() |> Enum.find(&(&1.lexeme == "Math.abs")) |> Map.fetch!(:token_type)
:qualified_function_name

keywords()

@spec keywords() :: [entry() | operator_entry(), ...]

The word-shaped entries: everything an editor must not offer as a plain identifier, because the lexer classifies it as something else.

Duration units are excluded. d is an ordinary identifier everywhere except immediately after a number, so treating it as a reserved word would be wrong; by_category(:duration_unit) is where those live.

Examples

iex> Predicator.Vocabulary.keywords() |> Enum.map(& &1.lexeme) |> Enum.member?("contains")
true

iex> Predicator.Vocabulary.keywords() |> Enum.map(& &1.lexeme) |> Enum.member?("d")
false

operators()

@spec operators() :: [operator_entry(), ...]

The entries that combine or compare values: the comparison, logical, arithmetic, membership, temporal, and cast categories.

Every entry here is an operator_entry/0, carrying :label, :arity, :ast_op, and :value_kinds on top of what tokens/0 carries. See "Operator entries" in the module documentation for what each one means.

Examples

iex> Predicator.Vocabulary.operators() |> Enum.all?(&(&1.category != :literal))
true

iex> Predicator.Vocabulary.operators() |> Enum.find(&(&1.lexeme == ">=")) |> Map.take([:label, :arity, :ast_op])
%{label: "is at least", arity: 2, ast_op: :gte}

iex> Predicator.Vocabulary.operators() |> Enum.find(&(&1.lexeme == "IN")) |> Map.fetch!(:value_kinds)
[:list]

tokens()

@spec tokens() :: [entry() | operator_entry(), ...]

Every fixed lexeme of the grammar: operators, keywords, literal words, brackets, separators, and duration units.

Functions are not here - they depend on which providers a caller resolves, so they come from functions/0 and functions/1 instead. all/0 is the two lists together.

Examples

iex> Predicator.Vocabulary.tokens() |> Enum.map(& &1.category) |> Enum.uniq() |> Enum.member?(:function)
false

value_kinds()

@spec value_kinds() :: [value_kind(), ...]

Every kind of value an operator can be offered for.

The vocabulary of :value_kinds on an operator_entry/0, enumerated so a caller can iterate the kinds rather than hard-code them.

Examples

iex> Predicator.Vocabulary.value_kinds()
[:string, :number, :boolean, :date, :datetime, :duration, :list]