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- thePredicator.Lexer.token/0tag it lexes to:category- one ofcategories/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 five 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, know which kinds of value it is worth offering it for
at all, and - where the same operator is enumerated twice - know which of the
two spellings to write:
:label- a short phrase read in place of the operator, e.g."is at least"for">=".:displayis a template and:docis 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 atomPredicator.Parser.parse/2puts in the node it builds, which differs from:token_typefor three operators (:in_opbecomes:in,:contains_opbecomes:contains,:strict_equalbecomes:strict_eq).nilwhere no node carries the operator as an atom of its own:value_kinds- thevalue_kind/0s the operator is worth offering for, as the kind of the value on its right.nilfor an operator that does not compare a field against a value at all - the logical, arithmetic, temporal and cast categories:canonical- whether this spelling is the onePredicator.decompile/2writes for the operator. See "Case" below: a word operator is enumerated in both cases, and only one of the two entries is the spelling a caller that means to render source should offer
Entries outside an operator category carry none of the five. 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:
| Excluded | Why |
|---|---|
| Ordered comparison of booleans | types_match admits them, so true > false evaluates - by Erlang term order, which is not a fact about the author's data |
| Equality against a list | Lists 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.
Enumerating both cases leaves a caller that renders source with a question
the enumeration alone cannot answer: of the two entries for one operator,
which spelling does Predicator.decompile/2 write? That is a fact about this
vocabulary, so :canonical answers it here rather than leaving each caller
to re-derive it. It is false on exactly one thing: the lower-case entry of
a word operator enumerated in both cases. Every other operator entry is the
only spelling there is and is therefore its own canonical one - ">=", and
the lower-case-only temporal words alike. It says nothing about what parses:
both spellings lex to the same token type, and an editor offering completions
wants both.
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
@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.
@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.
@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.
@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, canonical: boolean() }
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.
@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
@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
@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"]
@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
@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
@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
@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, :value_kinds, and :canonical 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]
iex> Predicator.Vocabulary.operators() |> Enum.filter(&(&1.token_type == :in_op)) |> Enum.map(&{&1.lexeme, &1.canonical})
[{"in", false}, {"IN", true}]
@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
@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]