morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
expression.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <algorithm>
5#include <array>
6#include <chrono>
7#include <compare>
8#include <cstddef>
9#include <cstdint>
10#include <expected>
11#include <functional>
12#include <iterator>
13#include <map>
14#include <memory>
15#include <optional>
16#include <ranges>
17#include <set>
18#include <span>
19#include <string>
20#include <string_view>
21#include <utility>
22#include <vector>
23
24#include "../render/i18n.hpp"
26#include "../util/datetime.hpp"
27#include "../util/rational.hpp"
28#include "message.hpp"
29#include "value.hpp"
30
31/// @file
32/// @brief The `expr/1` expression language of the UI document: parsing a JSON expression into an `Expr` tree,
33/// checking it against the names a document declares, and evaluating it.
34///
35/// Specified in `docs/spec/ui/expressions.md`.
36
37namespace morph::ui {
38
39/// @brief The most nodes one expression may have; a document with a larger expression is refused at load.
40inline constexpr std::size_t kMaxExpressionNodes = 256;
41
42/// @brief Whether a string is a document name: `[A-Za-z_][A-Za-z0-9_]*`.
43/// @param text The candidate.
44/// @return Whether @p text is a well-formed name.
45[[nodiscard]] constexpr bool isName(std::string_view text) noexcept {
46 if (text.empty()) {
47 return false;
48 }
49 bool first = true;
50 for (char const chr : text) {
51 bool const letter = (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z') || chr == '_';
52 bool const digit = chr >= '0' && chr <= '9';
53 if (!letter && (first || !digit)) {
54 return false;
55 }
56 first = false;
57 }
58 return true;
59}
60
61/// @brief The forms of an `expr/1` expression (spec 5 §4).
62enum class ExprKind : std::uint8_t {
63 Literal, ///< A JSON scalar, or `{"lit": v}`.
64 Ref, ///< `{"ref": "name.path"}`.
65 Row, ///< `{"row": "path"}`.
66 Query, ///< `{"query": "q"}`: a query's value.
67 Pending, ///< `{"pending": "q"}`: whether a query or mutation is in flight.
68 Error, ///< `{"error": "q"}`: a query's or mutation's error text.
69 Result, ///< `{"result": "m"}`: a mutation's last result.
70 ErrorKind, ///< `{"errorKind": "m"}`: the kind of a query's or mutation's error.
71 Can, ///< `{"can": "m"}`: whether the principal may run a mutation or query.
72 Pref, ///< `{"pref": "point.path"}`: the user's choice at a customization point.
73 Call, ///< `{"fn": "name", "args": [...]}`, and `{"t": ...}`.
74 If, ///< `{"if": [cond, then, else]}`.
75 Object, ///< `{"obj": {...}}`.
76 List, ///< `{"list": [...]}`.
77 Map, ///< `{"map": e, "as": "x", "to": e}`.
78 Filter, ///< `{"filter": e, "as": "x", "where": e}`.
79 Env, ///< `{"env": "name"}`: the client environment.
80 Session, ///< `{"session": "name"}`: a session attribute.
81 Host, ///< `{"host": "name"}`: a value the application registered with the client.
82};
83
84/// @brief The functions of the `expr/1` library.
85enum class FunctionId : std::uint8_t {
86 And,
87 Or,
88 Not,
89 Eq,
90 Ne,
91 Lt,
92 Le,
93 Gt,
94 Ge,
95 Add,
96 Sub,
97 Mul,
98 Div,
99 Neg,
100 Round,
101 Engaged,
102 Coalesce,
103 Concat,
104 Format,
105 Trim,
106 Length,
107 IsEmpty,
108 Contains,
109 Count,
110 Sum,
111 Mean,
112 Min,
113 Max,
114 Any,
115 All,
116 First,
117 Includes,
118 IndexOf,
119 Number,
120 DecimalText,
121 QuantityText,
122 Money,
123 Date,
124 DateTime,
125 Translate,
126 ParseDecimal,
127 ConcatLists,
128};
129
130struct Expr;
131
132/// @brief A shared, immutable expression node.
133using ExprPtr = std::shared_ptr<Expr const>;
134
135/// @brief One node of a parsed expression. Immutable once parsed; shared between the document and its readers.
136struct Expr {
137 /// @brief The form.
139 /// @brief The JSON Pointer of the node inside its document, for refusals and diagnostics.
140 std::string path;
141 /// @brief The constant of a `Literal`.
143 /// @brief The name a form reads: a ref's head, a query or mutation, a pref's point, a comprehension's variable,
144 /// an `env`, `session` or `host` name.
145 std::string name;
146 /// @brief The path after the head of a `Ref` or `Pref`, or the path of a `Row`.
147 std::vector<std::string> fieldPath;
148 /// @brief The function of a `Call`.
149 FunctionId function = FunctionId::And;
150 /// @brief Operands: a call's arguments; `if`'s condition, then and else; a list's items; a comprehension's
151 /// source and body.
152 std::vector<ExprPtr> args;
153 /// @brief The fields of an `Object`, in the order written.
154 std::vector<std::pair<std::string, ExprPtr>> fields;
155};
156
157namespace detail {
158
159/// @brief A set of type kinds, as bits.
160using KindSet = std::uint16_t;
161
162/// @brief The bit of one kind.
163/// @param kind The kind.
164/// @return Its bit.
165[[nodiscard]] constexpr KindSet kindBit(TypeKind kind) noexcept {
166 return static_cast<KindSet>(1U << static_cast<unsigned>(kind));
167}
168
169/// @brief Every kind.
170inline constexpr KindSet kAllKinds = 0xFFFF;
171/// @brief The numbers.
173/// @brief The ordered kinds.
175
176/// @brief The signature of a library function.
178 /// @brief The name a document calls it by.
179 std::string_view name;
180 /// @brief The fewest arguments.
181 std::uint8_t minArgs;
182 /// @brief The most arguments; 255 for any number.
183 std::uint8_t maxArgs;
184 /// @brief The kinds the first, second and every further argument accept.
185 std::array<KindSet, 3> accepts;
186 /// @brief The result's kind; `Any` when it depends on the arguments.
188};
189
190/// @brief The library, indexed by `FunctionId`.
191// NOLINTBEGIN(modernize-use-designated-initializers): one row per function reads as a table; names on every field would not.
192inline constexpr std::array<FunctionSpec, 42> kFunctions = {{
195 {"not", 1, 1, {kindBit(TypeKind::Bool), 0, 0}, TypeKind::Bool},
196 {"eq", 2, 2, {kAllKinds, kAllKinds, 0}, TypeKind::Bool},
197 {"ne", 2, 2, {kAllKinds, kAllKinds, 0}, TypeKind::Bool},
198 {"lt", 2, 2, {kOrdered, kOrdered, 0}, TypeKind::Bool},
199 {"le", 2, 2, {kOrdered, kOrdered, 0}, TypeKind::Bool},
200 {"gt", 2, 2, {kOrdered, kOrdered, 0}, TypeKind::Bool},
201 {"ge", 2, 2, {kOrdered, kOrdered, 0}, TypeKind::Bool},
202 {"add", 2, 2, {kNumeric, kNumeric, 0}, TypeKind::Any},
203 {"sub", 2, 2, {kNumeric, kNumeric, 0}, TypeKind::Any},
204 {"mul", 2, 2, {kNumeric, kNumeric, 0}, TypeKind::Any},
205 {"div", 2, 2, {kNumeric, kNumeric, 0}, TypeKind::Any},
206 {"neg", 1, 1, {kNumeric, 0, 0}, TypeKind::Any},
207 {"round", 2, 2, {kNumeric, kindBit(TypeKind::Int), 0}, TypeKind::Any},
208 {"engaged", 1, 1, {kAllKinds, 0, 0}, TypeKind::Bool},
209 {"coalesce", 1, 255, {kAllKinds, kAllKinds, kAllKinds}, TypeKind::Any},
210 {"concat",
211 1,
212 255,
216 {"trim", 1, 1, {kindBit(TypeKind::String), 0, 0}, TypeKind::String},
217 {"length", 1, 1, {kindBit(TypeKind::String), 0, 0}, TypeKind::Int},
218 {"isEmpty", 1, 1, {kindBit(TypeKind::String) | kindBit(TypeKind::List), 0, 0}, TypeKind::Bool},
220 {"count", 1, 1, {kindBit(TypeKind::List), 0, 0}, TypeKind::Int},
221 {"sum", 1, 1, {kindBit(TypeKind::List), 0, 0}, TypeKind::Any},
222 {"mean", 1, 1, {kindBit(TypeKind::List), 0, 0}, TypeKind::Any},
223 {"min", 1, 1, {kindBit(TypeKind::List), 0, 0}, TypeKind::Any},
224 {"max", 1, 1, {kindBit(TypeKind::List), 0, 0}, TypeKind::Any},
225 {"any", 1, 1, {kindBit(TypeKind::List), 0, 0}, TypeKind::Bool},
226 {"all", 1, 1, {kindBit(TypeKind::List), 0, 0}, TypeKind::Bool},
227 {"first", 1, 1, {kindBit(TypeKind::List), 0, 0}, TypeKind::Any},
228 {"includes", 2, 2, {kindBit(TypeKind::List), kAllKinds, 0}, TypeKind::Bool},
229 {"indexOf", 2, 2, {kindBit(TypeKind::List), kAllKinds, 0}, TypeKind::Int},
230 {"number", 1, 1, {kNumeric, 0, 0}, TypeKind::String},
231 {"decimal", 2, 2, {kNumeric, kindBit(TypeKind::Int), 0}, TypeKind::String},
233 {"money",
234 2,
235 2,
238 {"date",
239 1,
240 2,
243 {"dateTime",
244 1,
245 2,
249 {"parseDecimal", 1, 2, {kindBit(TypeKind::String), kindBit(TypeKind::Int), 0}, TypeKind::Decimal},
250 {"concatLists",
251 1,
252 255,
255}};
256// NOLINTEND(modernize-use-designated-initializers)
257
258/// @brief The signature of a function.
259/// @param function The function.
260/// @return Its entry in `kFunctions`.
261[[nodiscard]] constexpr FunctionSpec const& functionSpec(FunctionId function) {
262 return kFunctions.at(static_cast<std::size_t>(function));
263}
264
265/// @brief The names `{"env": ...}` reads.
266inline constexpr std::array<std::string_view, 5> kEnvNames = {"widthClass", "locale", "platform", "online", "backend"};
267
268/// @brief The names `{"session": ...}` reads.
269inline constexpr std::array<std::string_view, 6> kSessionNames = {"principal", "userName", "displayName",
270 "userKey", "tenant", "authenticated"};
271
272/// @brief Whether a name is one of a closed set.
273/// @param names The set.
274/// @param name The candidate.
275/// @return Whether @p name is in @p names.
276template <std::size_t N>
277[[nodiscard]] constexpr bool oneOf(std::array<std::string_view, N> const& names, std::string_view name) noexcept {
278 return std::ranges::find(names, name) != names.end();
279}
280
281/// @brief Splits `head.a.b` into its segments.
282/// @param text The dotted path.
283/// @return The segments, or the reason the path is malformed.
284[[nodiscard]] inline std::expected<std::vector<std::string>, std::string> splitPath(std::string_view text) {
285 std::vector<std::string> segments;
286 std::size_t start = 0;
287 for (;;) {
288 std::size_t const dot = text.find('.', start);
289 std::string_view const segment =
290 text.substr(start, dot == std::string_view::npos ? std::string_view::npos : dot - start);
291 if (segment.empty()) {
292 return std::unexpected("`" + std::string{text} + "` has an empty segment");
293 }
294 segments.emplace_back(segment);
295 if (dot == std::string_view::npos) {
296 return segments;
297 }
298 start = dot + 1;
299 }
300}
301
302/// @brief Reads one JSON expression tree into `Expr` nodes, counting them against the size bound.
303// NOLINTBEGIN(misc-no-recursion): an expression is a tree and parsing descends it; the node bound limits the depth.
305public:
306 /// @brief The keys a form may carry beside its own.
307 struct FormKeys {
308 /// @brief The form's own key.
309 std::string_view key;
310 /// @brief The form.
312 /// @brief The keys allowed beside it.
313 std::array<std::string_view, 2> companions;
314 };
315
316 /// @brief Parses @p json, whose JSON Pointer is @p path.
317 /// @param json The expression.
318 /// @param path Its JSON Pointer.
319 /// @return The expression, or the refusal.
320 [[nodiscard]] std::expected<ExprPtr, Refusal> parse(Json const& json, std::string const& path) {
321 return node(json, path);
322 }
323
324private:
325 // NOLINTBEGIN(modernize-use-designated-initializers): one row per form reads as a table.
326 static constexpr std::array<FormKeys, 20> kForms = {{
327 {"lit", ExprKind::Literal, {}},
328 {"ref", ExprKind::Ref, {}},
329 {"row", ExprKind::Row, {}},
330 {"query", ExprKind::Query, {}},
331 {"pending", ExprKind::Pending, {}},
332 {"error", ExprKind::Error, {}},
333 {"result", ExprKind::Result, {}},
334 {"errorKind", ExprKind::ErrorKind, {}},
335 {"can", ExprKind::Can, {}},
336 {"pref", ExprKind::Pref, {}},
337 {"fn", ExprKind::Call, {"args", ""}},
338 {"t", ExprKind::Call, {"fallback", "args"}},
339 {"if", ExprKind::If, {}},
340 {"obj", ExprKind::Object, {}},
341 {"list", ExprKind::List, {}},
342 {"map", ExprKind::Map, {"as", "to"}},
343 {"filter", ExprKind::Filter, {"as", "where"}},
344 {"env", ExprKind::Env, {}},
345 {"session", ExprKind::Session, {}},
346 {"host", ExprKind::Host, {}},
347 }};
348 // NOLINTEND(modernize-use-designated-initializers)
349
350 [[nodiscard]] static std::unexpected<Refusal> refuse(std::string const& path, std::string reason) {
351 return std::unexpected(Refusal{.document = {}, .path = path, .reason = std::move(reason)});
352 }
353
354 [[nodiscard]] static std::string const* text(Json const& json) { return std::get_if<std::string>(&json.data); }
355
356 [[nodiscard]] static Json const* member(Json::object_t const& object, std::string_view key) {
357 auto const* const found = object.find(key);
358 return found == object.end() ? nullptr : &found->second;
359 }
360
361 [[nodiscard]] static ExprPtr literal(Value value, std::string path) {
362 auto made = std::make_shared<Expr>();
363 made->kind = ExprKind::Literal;
364 made->literal = std::move(value);
365 made->path = std::move(path);
366 return made;
367 }
368
369 [[nodiscard]] std::expected<ExprPtr, Refusal> node(Json const& json, std::string const& path) {
370 if (++_count > kMaxExpressionNodes) {
371 return refuse(path, "the expression has more than " + std::to_string(kMaxExpressionNodes) +
372 " nodes, the bound of expr/1");
373 }
374 if (std::holds_alternative<Json::array_t>(json.data)) {
375 return refuse(path, R"(a list is written {"list": [...]}, a constant list {"lit": [...]})");
376 }
377 auto const* const object = std::get_if<Json::object_t>(&json.data);
378 if (object == nullptr) {
379 auto value = valueFromJson(json);
380 if (!value) {
381 return refuse(path, value.error());
382 }
383 return literal(std::move(*value), path);
384 }
385 auto made = std::make_shared<Expr>();
386 made->path = path;
387 if (auto refused = form(*made, *object, path)) {
388 return std::unexpected(std::move(*refused));
389 }
390 return made;
391 }
392
393 [[nodiscard]] std::optional<Refusal> form(Expr& made, Json::object_t const& object, std::string const& path) {
394 FormKeys const* chosen = nullptr;
395 for (auto const& [key, value] : object) {
396 static_cast<void>(value);
397 for (FormKeys const& candidate : kForms) {
398 if (candidate.key != key) {
399 continue;
400 }
401 if (chosen != nullptr) {
402 return refuse(path, "an expression has one form; found `" + std::string{chosen->key} + "` and `" +
403 key + "`")
404 .error();
405 }
406 chosen = &candidate;
407 }
408 }
409 if (chosen == nullptr) {
410 if (object.empty()) {
411 return refuse(path, "an empty object is not an expression; a constant record is {\"lit\": {}}")
412 .error();
413 }
414 return refuse(path, "`" + object.begin()->first + "` is not an expression form of expr/1").error();
415 }
416 for (auto const& [key, value] : object) {
417 static_cast<void>(value);
418 if (key != chosen->key && key != chosen->companions.at(0) && key != chosen->companions.at(1)) {
419 return refuse(pointerAppend(path, key),
420 "`" + key + "` does not belong to the `" + std::string{chosen->key} + "` form")
421 .error();
422 }
423 }
424 Json const& primary = *member(object, chosen->key);
425 std::string const primaryPath = pointerAppend(path, chosen->key);
426 made.kind = chosen->kind;
427 switch (chosen->kind) {
428 case ExprKind::Literal: {
429 auto value = valueFromJson(primary);
430 if (!value) {
431 return refuse(primaryPath, value.error()).error();
432 }
433 made.literal = std::move(*value);
434 return std::nullopt;
435 }
436 case ExprKind::Ref:
437 case ExprKind::Pref:
438 return dotted(made, primary, primaryPath);
439 case ExprKind::Row:
440 return rowPath(made, primary, primaryPath);
441 case ExprKind::Query:
442 case ExprKind::Pending:
443 case ExprKind::Error:
444 case ExprKind::Result:
445 case ExprKind::ErrorKind:
446 case ExprKind::Can:
447 return controlName(made, primary, primaryPath, chosen->key);
448 case ExprKind::Env:
449 case ExprKind::Session:
450 case ExprKind::Host:
451 return clientName(made, primary, primaryPath, chosen->key);
452 case ExprKind::Call:
453 if (chosen->key == "t") {
454 return translate(made, object, path);
455 }
456 return call(made, object, path);
457 case ExprKind::If:
458 return operands(made.args, primary, primaryPath, 3, "`if` takes [condition, then, else]");
459 case ExprKind::List:
460 return operands(made.args, primary, primaryPath, 0, "`list` takes an array of expressions");
461 case ExprKind::Object:
462 return fields(made.fields, primary, primaryPath);
463 case ExprKind::Map:
464 case ExprKind::Filter:
465 default:
466 return comprehension(made, object, path);
467 }
468 }
469
470 [[nodiscard]] static std::optional<Refusal> dotted(Expr& made, Json const& primary, std::string const& path) {
471 std::string const* const dottedPath = text(primary);
472 if (dottedPath == nullptr) {
473 return refuse(path, "a name and path is written as a string `name.path`").error();
474 }
475 auto segments = splitPath(*dottedPath);
476 if (!segments) {
477 return refuse(path, segments.error()).error();
478 }
479 if (!isName(segments->front())) {
480 return refuse(path, "`" + segments->front() + "` is not a name").error();
481 }
482 made.name = std::move(segments->front());
483 made.fieldPath.assign(std::make_move_iterator(segments->begin() + 1),
484 std::make_move_iterator(segments->end()));
485 return std::nullopt;
486 }
487
488 [[nodiscard]] static std::optional<Refusal> rowPath(Expr& made, Json const& primary, std::string const& path) {
489 std::string const* const dottedPath = text(primary);
490 if (dottedPath == nullptr) {
491 return refuse(path, "`row` names a field path as a string, \"\" for the whole row").error();
492 }
493 if (dottedPath->empty()) {
494 return std::nullopt;
495 }
496 auto segments = splitPath(*dottedPath);
497 if (!segments) {
498 return refuse(path, segments.error()).error();
499 }
500 made.fieldPath = std::move(*segments);
501 return std::nullopt;
502 }
503
504 [[nodiscard]] static std::optional<Refusal> controlName(Expr& made, Json const& primary, std::string const& path,
505 std::string_view key) {
506 std::string const* const name = text(primary);
507 bool const can = made.kind == ExprKind::Can;
508 bool const hostCommand = can && name != nullptr && name->starts_with("host:") && name->size() > 5;
509 if (name == nullptr || (!isName(*name) && !hostCommand)) {
510 return refuse(path, "`" + std::string{key} + "` takes the name of a " +
511 (can ? "mutation, query or `host:` command" : "query or mutation"))
512 .error();
513 }
514 made.name = *name;
515 return std::nullopt;
516 }
517
518 [[nodiscard]] static std::optional<Refusal> clientName(Expr& made, Json const& primary, std::string const& path,
519 std::string_view key) {
520 std::string const* const name = text(primary);
521 bool known = name != nullptr && !name->empty();
522 if (known && made.kind == ExprKind::Env) {
523 known = oneOf(kEnvNames, *name);
524 } else if (known && made.kind == ExprKind::Session) {
525 known = oneOf(kSessionNames, *name);
526 }
527 if (!known) {
528 return refuse(path, "`" + std::string{key} + "` does not read this name").error();
529 }
530 made.name = *name;
531 return std::nullopt;
532 }
533
534 [[nodiscard]] std::optional<Refusal> operands(std::vector<ExprPtr>& out, Json const& primary,
535 std::string const& path, std::size_t exactly, char const* shape) {
536 auto const* const array = std::get_if<Json::array_t>(&primary.data);
537 if (array == nullptr || (exactly != 0 && array->size() != exactly)) {
538 return refuse(path, shape).error();
539 }
540 for (std::size_t index = 0; index < array->size(); ++index) {
541 auto operand = node(array->at(index), pointerAppend(path, index));
542 if (!operand) {
543 return std::move(operand.error());
544 }
545 out.push_back(std::move(*operand));
546 }
547 return std::nullopt;
548 }
549
550 [[nodiscard]] std::optional<Refusal> fields(std::vector<std::pair<std::string, ExprPtr>>& out, Json const& primary,
551 std::string const& path) {
552 auto const* const object = std::get_if<Json::object_t>(&primary.data);
553 if (object == nullptr) {
554 return refuse(path, "`obj` takes an object of expressions").error();
555 }
556 for (auto const& [key, value] : *object) {
557 auto field = node(value, pointerAppend(path, key));
558 if (!field) {
559 return std::move(field.error());
560 }
561 out.emplace_back(key, std::move(*field));
562 }
563 return std::nullopt;
564 }
565
566 [[nodiscard]] std::optional<Refusal> call(Expr& made, Json::object_t const& object, std::string const& path) {
567 std::string const* const name = text(*member(object, "fn"));
568 std::string const namePath = pointerAppend(path, "fn");
569 if (name == nullptr) {
570 return refuse(namePath, "`fn` names a function as a string").error();
571 }
572 auto const index =
573 std::ranges::distance(kFunctions.begin(), std::ranges::find(kFunctions, *name, &FunctionSpec::name));
574 if (std::cmp_equal(index, kFunctions.size())) {
575 return refuse(namePath, "the function `" + *name + "` is not in expr/1").error();
576 }
577 FunctionSpec const& spec = *std::next(kFunctions.begin(), index);
578 made.function = static_cast<FunctionId>(index);
579 if (Json const* const args = member(object, "args")) {
580 if (auto refused = operands(made.args, *args, pointerAppend(path, "args"), 0,
581 "`args` takes an array of expressions")) {
582 return refused;
583 }
584 }
585 if (made.args.size() < spec.minArgs || made.args.size() > spec.maxArgs) {
586 std::string arity = std::to_string(static_cast<unsigned>(spec.minArgs));
587 if (spec.maxArgs == 255) {
588 arity += " or more";
589 } else if (spec.maxArgs != spec.minArgs) {
590 arity += " to " + std::to_string(static_cast<unsigned>(spec.maxArgs));
591 }
592 return refuse(path,
593 "`" + *name + "` takes " + arity + " arguments, not " + std::to_string(made.args.size()))
594 .error();
595 }
596 return std::nullopt;
597 }
598
599 [[nodiscard]] std::optional<Refusal> translate(Expr& made, Json::object_t const& object, std::string const& path) {
600 made.function = FunctionId::Translate;
601 std::string const* const key = text(*member(object, "t"));
602 Json const* const fallback = member(object, "fallback");
603 std::string const* const fallbackText = fallback == nullptr ? nullptr : text(*fallback);
604 if (key == nullptr || key->empty()) {
605 return refuse(pointerAppend(path, "t"), "`t` names a message key as a string").error();
606 }
607 if (fallbackText == nullptr) {
608 return refuse(pointerAppend(path, "fallback"), "`t` needs its `fallback` text as a string").error();
609 }
610 made.args.push_back(literal(*key, pointerAppend(path, "t")));
611 made.args.push_back(literal(*fallbackText, pointerAppend(path, "fallback")));
612 if (Json const* const args = member(object, "args")) {
613 auto record = std::make_shared<Expr>();
614 record->kind = ExprKind::Object;
615 record->path = pointerAppend(path, "args");
616 if (auto refused = fields(record->fields, *args, record->path)) {
617 return refused;
618 }
619 made.args.push_back(std::move(record));
620 }
621 return std::nullopt;
622 }
623
624 [[nodiscard]] std::optional<Refusal> comprehension(Expr& made, Json::object_t const& object,
625 std::string const& path) {
626 bool const isMap = made.kind == ExprKind::Map;
627 std::string_view const sourceKey = isMap ? "map" : "filter";
628 std::string_view const bodyKey = isMap ? "to" : "where";
629 Json const* const variable = member(object, "as");
630 Json const* const body = member(object, bodyKey);
631 std::string const* const name = variable == nullptr ? nullptr : text(*variable);
632 if (name == nullptr || !isName(*name)) {
633 return refuse(pointerAppend(path, "as"), "`" + std::string{sourceKey} + "` names its element with `as`")
634 .error();
635 }
636 if (body == nullptr) {
637 return refuse(path, "`" + std::string{sourceKey} + "` needs `" + std::string{bodyKey} + "`").error();
638 }
639 made.name = *name;
640 auto source = node(*member(object, sourceKey), pointerAppend(path, sourceKey));
641 if (!source) {
642 return std::move(source.error());
643 }
644 auto each = node(*body, pointerAppend(path, bodyKey));
645 if (!each) {
646 return std::move(each.error());
647 }
648 made.args.push_back(std::move(*source));
649 made.args.push_back(std::move(*each));
650 return std::nullopt;
651 }
652
653 std::size_t _count = 0;
654 // NOLINTEND(misc-no-recursion)
655};
656
657} // namespace detail
658
659/// @brief Parses one expression.
660///
661/// Every form of spec 5 §4 is read; an unknown form, a key that does not belong to its form, a malformed name, an
662/// unknown function or wrong arity, a number with a fraction, or more than `kMaxExpressionNodes` nodes is refused.
663/// @param json The expression's JSON.
664/// @param path Its JSON Pointer inside the document, carried by every node and every refusal.
665/// @return The expression, or the refusal.
666[[nodiscard]] inline std::expected<ExprPtr, Refusal> parseExpression(Json const& json, std::string const& path = "") {
667 return detail::ExprParser{}.parse(json, path);
668}
669
670/// @brief The names of the `expr/1` library, in `FunctionId` order.
671/// @return The names.
672[[nodiscard]] inline std::vector<std::string_view> functionNames() {
673 std::vector<std::string_view> names;
674 names.reserve(detail::kFunctions.size());
675 for (auto const& spec : detail::kFunctions) {
676 names.push_back(spec.name);
677 }
678 return names;
679}
680
681/// @brief What an expression's names resolve to, for `checkExpression`: the document's scopes implement it.
682class NameResolver {
683public:
684 NameResolver() = default;
685 NameResolver(NameResolver const&) = delete;
686 NameResolver& operator=(NameResolver const&) = delete;
687 NameResolver(NameResolver&&) = delete;
688 NameResolver& operator=(NameResolver&&) = delete;
689 virtual ~NameResolver() = default;
690
691 /// @brief Resolves `{"ref": "head.path"}`.
692 /// @param head The first segment.
693 /// @param path The segments after it.
694 /// @return The type read, or why the reference does not resolve.
695 [[nodiscard]] virtual std::expected<Type, std::string> ref(std::string_view head,
696 std::span<std::string const> path) const = 0;
697
698 /// @brief Resolves `{"row": "path"}`.
699 /// @param path The field path inside the row.
700 /// @return The type read, or why there is no row here.
701 [[nodiscard]] virtual std::expected<Type, std::string> row(std::span<std::string const> path) const = 0;
702
703 /// @brief Resolves the name of `query`, `pending`, `error`, `result`, `errorKind` or `can`.
704 /// @param form The form.
705 /// @param name The query or mutation name.
706 /// @return The type read, or why the name does not resolve.
707 [[nodiscard]] virtual std::expected<Type, std::string> control(ExprKind form, std::string_view name) const = 0;
708
709 /// @brief Resolves `{"pref": "point.path"}`.
710 /// @param point The customization point.
711 /// @param path The segments after it.
712 /// @return The type read, or why the point is not declared.
713 [[nodiscard]] virtual std::expected<Type, std::string> pref(std::string_view point,
714 std::span<std::string const> path) const = 0;
715};
716
717/// @brief A resolver that accepts every name as `any`: for an expression checked outside a document.
718class PermissiveResolver final : public NameResolver {
719public:
720 /// @brief Accepts any reference.
721 /// @param head Unused.
722 /// @param path Unused.
723 /// @return `any`.
724 [[nodiscard]] std::expected<Type, std::string> ref(std::string_view head,
725 std::span<std::string const> path) const override {
726 static_cast<void>(head);
727 static_cast<void>(path);
728 return Type::any();
729 }
730
731 /// @brief Accepts any row path.
732 /// @param path Unused.
733 /// @return `any`.
734 [[nodiscard]] std::expected<Type, std::string> row(std::span<std::string const> path) const override {
735 static_cast<void>(path);
736 return Type::any();
737 }
738
739 /// @brief Accepts any query or mutation.
740 /// @param form Unused.
741 /// @param name Unused.
742 /// @return `any`.
743 [[nodiscard]] std::expected<Type, std::string> control(ExprKind form, std::string_view name) const override {
744 static_cast<void>(form);
745 static_cast<void>(name);
746 return Type::any();
747 }
748
749 /// @brief Accepts any point.
750 /// @param point Unused.
751 /// @param path Unused.
752 /// @return `any`.
753 [[nodiscard]] std::expected<Type, std::string> pref(std::string_view point,
754 std::span<std::string const> path) const override {
755 static_cast<void>(point);
756 static_cast<void>(path);
757 return Type::any();
758 }
759};
760
761namespace detail {
762
763/// @brief The static type of a literal.
764/// @param value The literal.
765/// @return Its type; `any` for `null`.
766[[nodiscard]] inline Type typeOfValue(Value const& value) {
767 // The type kind of each value kind, indexed by `ValueKind`; `null` has no type of its own.
768 constexpr std::array<TypeKind, 9> kTypes = {TypeKind::Any, TypeKind::Bool, TypeKind::Int,
771 return Type::of(kTypes.at(static_cast<std::size_t>(value.kind())));
772}
773
774/// @brief The type a field path reads from a value of a known type.
775/// @param type The type of the head.
776/// @param head The head's name, for the reason.
777/// @param path The path.
778/// @return `any` when the path can read something, or why it cannot.
779[[nodiscard]] inline std::expected<Type, std::string> typeAlong(Type const& type, std::string_view head,
780 std::span<std::string const> path) {
781 if (path.empty()) {
782 return type;
783 }
784 if (type.kind == TypeKind::Any || type.kind == TypeKind::Record || type.kind == TypeKind::List) {
785 return Type::any();
786 }
787 return std::unexpected("`" + std::string{head} + "` is " + type.name() + " and has no field `" + path.front() +
788 "`");
789}
790
791/// @brief Walks an expression once, resolving names and checking the statically known types.
792// NOLINTBEGIN(misc-no-recursion): an expression is a tree and checking descends it; the node bound limits the depth.
794public:
795 /// @brief Checks against @p resolver.
796 /// @param resolver What the names resolve to.
797 explicit ExprChecker(NameResolver const& resolver) : _resolver{&resolver} {}
798
799 /// @brief Checks one node and its operands.
800 /// @param expr The node.
801 /// @return Its static type, or the refusal.
802 [[nodiscard]] std::expected<Type, Refusal> check(Expr const& expr) {
803 switch (expr.kind) {
805 return typeOfValue(expr.literal);
806 case ExprKind::Ref:
807 return reference(expr);
808 case ExprKind::Row:
809 return wrap(expr, _resolver->row(expr.fieldPath));
810 case ExprKind::Query:
812 case ExprKind::Error:
813 case ExprKind::Result:
815 case ExprKind::Can:
816 return wrap(expr, _resolver->control(expr.kind, expr.name));
817 case ExprKind::Pref:
818 return wrap(expr, _resolver->pref(expr.name, expr.fieldPath));
819 case ExprKind::Env:
820 return Type::of(expr.name == "online" ? TypeKind::Bool : TypeKind::String, expr.name != "online");
822 return Type::of(expr.name == "authenticated" ? TypeKind::Bool : TypeKind::String,
823 expr.name != "authenticated");
824 case ExprKind::Host:
825 return Type::any();
826 case ExprKind::If:
827 return choice(expr);
828 case ExprKind::Object: {
829 for (auto const& [name, field] : expr.fields) {
830 static_cast<void>(name);
831 auto checked = check(*field);
832 if (!checked) {
833 return checked;
834 }
835 }
837 }
838 case ExprKind::List: {
839 for (ExprPtr const& item : expr.args) {
840 auto checked = check(*item);
841 if (!checked) {
842 return checked;
843 }
844 }
845 return Type::of(TypeKind::List);
846 }
847 case ExprKind::Map:
848 case ExprKind::Filter:
849 return comprehension(expr);
850 case ExprKind::Call:
851 default:
852 return call(expr);
853 }
854 }
855
856private:
857 [[nodiscard]] static std::expected<Type, Refusal> wrap(Expr const& expr, std::expected<Type, std::string> type) {
858 if (!type) {
859 return std::unexpected(Refusal{.document = {}, .path = expr.path, .reason = std::move(type.error())});
860 }
861 return std::move(*type);
862 }
863
864 [[nodiscard]] static std::unexpected<Refusal> refuse(Expr const& expr, std::string reason) {
865 return std::unexpected(Refusal{.document = {}, .path = expr.path, .reason = std::move(reason)});
866 }
867
868 [[nodiscard]] std::expected<Type, Refusal> reference(Expr const& expr) {
869 for (auto const& [name, type] : std::views::reverse(_locals)) {
870 if (name == expr.name) {
871 return wrap(expr, typeAlong(type, expr.name, expr.fieldPath));
872 }
873 }
874 return wrap(expr, _resolver->ref(expr.name, expr.fieldPath));
875 }
876
877 [[nodiscard]] static bool admitsKind(Type const& type, KindSet accepted) {
878 return type.kind == TypeKind::Any || (accepted & kindBit(type.kind)) != 0;
879 }
880
881 [[nodiscard]] std::expected<Type, Refusal> condition(Expr const& expr, std::string_view what) {
882 auto type = check(expr);
883 if (type && !admitsKind(*type, kindBit(TypeKind::Bool))) {
884 return refuse(expr, std::string{what} + " is " + type->name() + "; expected bool");
885 }
886 return type;
887 }
888
889 [[nodiscard]] std::expected<Type, Refusal> choice(Expr const& expr) {
890 auto cond = condition(*expr.args.at(0), "the condition of `if`");
891 if (!cond) {
892 return cond;
893 }
894 auto then = check(*expr.args.at(1));
895 if (!then) {
896 return then;
897 }
898 auto otherwise = check(*expr.args.at(2));
899 if (!otherwise) {
900 return otherwise;
901 }
902 if (then->kind == otherwise->kind && then->kind != TypeKind::List) {
903 return Type::of(then->kind, then->optional || otherwise->optional);
904 }
905 return Type::any();
906 }
907
908 [[nodiscard]] std::expected<Type, Refusal> comprehension(Expr const& expr) {
909 auto source = check(*expr.args.at(0));
910 if (!source) {
911 return source;
912 }
913 if (!admitsKind(*source, kindBit(TypeKind::List))) {
914 return refuse(*expr.args.at(0), "`" + std::string{expr.kind == ExprKind::Map ? "map" : "filter"} +
915 "` runs over a list, not " + source->name());
916 }
917 Type element = source->element ? *source->element : Type::any();
918 _locals.emplace_back(expr.name, element);
919 auto body = expr.kind == ExprKind::Map ? check(*expr.args.at(1))
920 : condition(*expr.args.at(1), "the `where` of `filter`");
921 _locals.pop_back();
922 if (!body) {
923 return body;
924 }
925 if (expr.kind == ExprKind::Map) {
926 return Type::listOf(*body);
927 }
928 return Type::listOf(std::move(element));
929 }
930
931 [[nodiscard]] std::expected<Type, Refusal> call(Expr const& expr) {
932 FunctionSpec const& spec = functionSpec(expr.function);
933 std::vector<Type> types;
934 for (std::size_t index = 0; index < expr.args.size(); ++index) {
935 auto type = check(*expr.args.at(index));
936 if (!type) {
937 return type;
938 }
939 KindSet const accepted = spec.accepts.at(std::min<std::size_t>(index, 2));
940 if (!admitsKind(*type, accepted)) {
941 return refuse(*expr.args.at(index), "argument " + std::to_string(index + 1) + " of `" +
942 std::string{spec.name} + "` is " + type->name() +
943 ", which it does not take");
944 }
945 types.push_back(std::move(*type));
946 }
947 if (expr.function == FunctionId::Translate) {
948 ExprPtr const& fallback = expr.args.at(1);
949 if (std::string const* const pattern = fallback->literal.asString();
950 fallback->kind == ExprKind::Literal && pattern != nullptr) {
951 auto parsed = parseMessage(*pattern);
952 if (!parsed) {
953 return refuse(*fallback, "the fallback is outside the ui/1 message subset: " + parsed.error());
954 }
955 }
956 }
957 if (spec.result != TypeKind::Any) {
958 return Type::of(spec.result, true);
959 }
960 if (expr.function == FunctionId::ConcatLists) {
961 return concatenated(types);
962 }
963 return numericResult(expr.function, types);
964 }
965
966 /// `concatLists`'s type: `list<T>` when every element type that is known is `T`, else `list<any>`.
967 [[nodiscard]] static Type concatenated(std::vector<Type> const& types) {
968 std::optional<Type> common;
969 for (Type const& type : types) {
970 if (type.isAny() || !type.element || type.element->isAny()) {
971 continue;
972 }
973 if (!common) {
974 common = *type.element;
975 } else if (!(*common == *type.element)) {
976 return Type::listOf(Type::any(), true);
977 }
978 }
979 return Type::listOf(common.value_or(Type::any()), true);
980 }
981
982 [[nodiscard]] static Type numericResult(FunctionId function, std::vector<Type> const& types) {
983 if (function == FunctionId::Coalesce || function == FunctionId::Sum || function == FunctionId::Mean ||
984 function == FunctionId::Min || function == FunctionId::Max || function == FunctionId::First) {
985 return Type::any();
986 }
987 if (function == FunctionId::Neg || function == FunctionId::Round) {
988 return Type::of(types.front().kind, true);
989 }
990 Type const& lhs = types.at(0);
991 Type const& rhs = types.at(1);
992 if (lhs.isAny() || rhs.isAny()) {
993 return Type::any();
994 }
995 bool const lhsQuantity = lhs.kind == TypeKind::Quantity;
996 bool const rhsQuantity = rhs.kind == TypeKind::Quantity;
997 if (lhsQuantity || rhsQuantity) {
998 if (function == FunctionId::Div && lhsQuantity && rhsQuantity) {
999 return Type::of(TypeKind::Decimal, true);
1000 }
1001 return Type::of(TypeKind::Quantity, true);
1002 }
1003 if (lhs.kind == TypeKind::Int && rhs.kind == TypeKind::Int && function != FunctionId::Div) {
1004 return Type::of(TypeKind::Int, true);
1005 }
1006 return function == FunctionId::Div ? Type::any() : Type::of(TypeKind::Decimal, true);
1007 }
1008
1009 NameResolver const* _resolver;
1010 std::vector<std::pair<std::string, Type>> _locals;
1011 // NOLINTEND(misc-no-recursion)
1012};
1013
1014} // namespace detail
1015
1016/// @brief Checks an expression against the names a document declares.
1017///
1018/// Every reference is resolved through @p resolver, comprehension variables first; the arguments of a function, the
1019/// condition of `if` and the `where` of `filter` are checked against their accepted kinds where the argument's type
1020/// is statically known; a literal `t` fallback must be in the `ui/1` message subset.
1021/// @param expr The expression.
1022/// @param resolver What the document's names resolve to.
1023/// @return The expression's static type (`any` where it cannot be known), or the refusal.
1024[[nodiscard]] inline std::expected<Type, Refusal> checkExpression(Expr const& expr, NameResolver const& resolver) {
1025 return detail::ExprChecker{resolver}.check(expr);
1026}
1027
1028/// @brief One unit of the server's unit systems, as `x-unitAlternatives` declares it.
1029struct UnitInfo {
1030 /// @brief The canonical unit of the unit's dimension; units with the same canonical unit convert.
1031 std::string canonical;
1032 /// @brief The exact factor from this unit to the canonical one: one of this unit is `toCanonical` canonical
1033 /// units.
1035 /// @brief The unit's display text; empty to show its id.
1036 std::string display;
1037};
1038
1039/// @brief The units a client knows, by id. A unit missing from the table is its own canonical unit.
1040using UnitTable = std::map<std::string, UnitInfo, std::less<>>;
1041
1042/// @brief Formats a date for display: the instant already shifted to the display zone, the style (`short`,
1043/// `medium` or `long`), and whether to show the time.
1044using DateFormatter = std::function<std::string(time::DateTime local, std::string_view style, bool withTime)>;
1045
1046/// @brief What the display functions and `t` format with: the client's locale and its formatting facts.
1048 /// @brief The active BCP 47 locale.
1049 std::string locale = "en";
1050 /// @brief The locale's number formatting. Holds views: the strings it names must outlive the context.
1052 /// @brief The message catalogue; empty shows every fallback.
1054 /// @brief The units quantities convert through.
1056 /// @brief The display zone's offset from UTC, in minutes.
1057 std::int32_t utcOffsetMinutes = 0;
1058 /// @brief The date formatter; empty uses ISO 8601 (`2026-10-11`, `2026-10-11 08:30`).
1060};
1061
1062/// @brief What an evaluated expression reads: the interpreter implements it over its reactive values.
1063///
1064/// Every read returns a value; a name the environment does not hold reads as `null`. `report` is the client's
1065/// diagnostic channel: the evaluator reports each runtime error there, with the expression's path.
1066class Environment {
1067public:
1068 Environment() = default;
1069 Environment(Environment const&) = delete;
1070 Environment& operator=(Environment const&) = delete;
1071 Environment(Environment&&) = delete;
1072 Environment& operator=(Environment&&) = delete;
1073 virtual ~Environment() = default;
1074
1075 /// @brief Reads `{"ref": "head.path"}`.
1076 /// @param head The first segment.
1077 /// @param path The segments after it.
1078 /// @return The value.
1079 [[nodiscard]] virtual Value ref(std::string_view head, std::span<std::string const> path) const = 0;
1080
1081 /// @brief Reads `{"row": "path"}`.
1082 /// @param path The field path inside the innermost row.
1083 /// @return The value.
1084 [[nodiscard]] virtual Value row(std::span<std::string const> path) const = 0;
1085
1086 /// @brief Reads `query`, `pending`, `error`, `result`, `errorKind` or `can`.
1087 /// @param form The form.
1088 /// @param name The query or mutation.
1089 /// @return The value: a `bool` for `pending` and `can`.
1090 [[nodiscard]] virtual Value control(ExprKind form, std::string_view name) const = 0;
1091
1092 /// @brief Reads `{"pref": "point.path"}`.
1093 /// @param point The customization point.
1094 /// @param path The segments after it.
1095 /// @return The value.
1096 [[nodiscard]] virtual Value pref(std::string_view point, std::span<std::string const> path) const = 0;
1097
1098 /// @brief Reads `env`, `session` or `host`.
1099 /// @param form The form.
1100 /// @param name The name read.
1101 /// @return The value.
1102 [[nodiscard]] virtual Value client(ExprKind form, std::string_view name) const = 0;
1103
1104 /// @brief The formatting facts.
1105 /// @return The context the display functions and `t` use.
1106 [[nodiscard]] virtual FormatContext const& formatting() const = 0;
1107
1108 /// @brief Reports a runtime error of an expression.
1109 /// @param path The JSON Pointer of the expression node that failed.
1110 /// @param reason What went wrong.
1111 virtual void report(std::string_view path, std::string_view reason) const = 0;
1112};
1113
1114/// @brief Reads a path out of a value: a record's field by name, a list's element by index.
1115/// @param value The value.
1116/// @param path The segments.
1117/// @return The value at the path, or `null` when a segment does not exist.
1118[[nodiscard]] inline Value walk(Value const& value, std::span<std::string const> path) {
1119 Value const* current = &value;
1120 for (std::string const& segment : path) {
1121 if (ValueList const* const items = current->asList()) {
1122 std::size_t index = 0;
1123 bool digits = !segment.empty();
1124 for (char const chr : segment) {
1125 digits = digits && chr >= '0' && chr <= '9';
1126 index = digits ? (index * 10) + static_cast<std::size_t>(chr - '0') : index;
1127 }
1128 if (!digits || index >= items->size()) {
1129 return Value{};
1130 }
1131 current = &items->at(index);
1132 continue;
1133 }
1134 current = current->field(segment);
1135 if (current == nullptr) {
1136 return Value{};
1137 }
1138 }
1139 return *current;
1140}
1141
1142namespace detail {
1143
1144/// @brief A number taken apart for arithmetic: its exact value, whether it is an `int`, and its unit.
1145struct Number {
1146 /// @brief The exact value.
1148 /// @brief Whether it is an `int`.
1149 bool integer = false;
1150 /// @brief The unit of a quantity; `nullptr` for a plain number.
1151 std::string const* unit = nullptr;
1152};
1153
1154/// @brief Takes a numeric value apart.
1155/// @param value The value.
1156/// @return The number, or `std::nullopt` when it is not one.
1157[[nodiscard]] inline std::optional<Number> numeric(Value const& value) {
1158 if (std::int64_t const* const whole = value.asInt()) {
1159 return Number{.value = math::Rational{*whole, math::DecimalPlaces{0}}, .integer = true, .unit = nullptr};
1160 }
1161 if (Decimal const* const decimal = value.asDecimal()) {
1162 return Number{.value = decimal->value, .integer = false, .unit = nullptr};
1163 }
1164 if (Quantity const* const quantity = value.asQuantity()) {
1165 return Number{.value = quantity->value, .integer = false, .unit = &quantity->unit};
1166 }
1167 return std::nullopt;
1168}
1169
1170/// @brief Builds a numeric value.
1171/// @param value The exact value.
1172/// @param integer Whether every operand was an `int`.
1173/// @param unit The unit, or `nullptr`.
1174/// @return A quantity with a unit; an `int` when integral and every operand was one; a decimal otherwise.
1175[[nodiscard]] inline Value makeNumber(math::Rational const& value, bool integer, std::string const* unit) {
1176 if (unit != nullptr) {
1177 return Quantity{.value = value, .unit = *unit};
1178 }
1179 if (integer && value.isInteger()) {
1180 return Value{value.numerator};
1181 }
1182 return Decimal{value};
1183}
1184
1185/// @brief Converts an exact value between two units of one dimension.
1186/// @param value The value in @p from.
1187/// @param from Its unit.
1188/// @param target The wanted unit.
1189/// @param units The unit table.
1190/// @return The value in @p target, its places widened by the decimal places of the conversion ratio; `std::nullopt`
1191/// when the units do not convert or the result overflows.
1192[[nodiscard]] inline std::optional<math::Rational> convertUnit(math::Rational const& value, std::string_view from,
1193 std::string_view target, UnitTable const& units) {
1194 if (from == target) {
1195 return value;
1196 }
1197 auto factor = [&units](std::string_view unit) {
1198 auto const found = units.find(unit);
1199 if (found == units.end()) {
1200 return std::pair{std::string{unit}, math::Rational{1, math::DecimalPlaces{0}}};
1201 }
1202 return std::pair{found->second.canonical, found->second.toCanonical};
1203 };
1204 auto const [fromBase, fromFactor] = factor(from);
1205 auto const [toBase, toFactor] = factor(target);
1206 if (fromBase != toBase) {
1207 return std::nullopt;
1208 }
1209 auto const ratio =
1210 math::checkedDiv(math::Rational{math::Numerator{fromFactor.numerator},
1211 math::Denominator{fromFactor.denominator}, math::DecimalPlaces{0}},
1212 math::Rational{math::Numerator{toFactor.numerator}, math::Denominator{toFactor.denominator},
1214 if (!ratio) {
1215 return std::nullopt;
1216 }
1217 auto converted = math::checkedMul(value, *ratio);
1218 if (!converted) {
1219 return std::nullopt;
1220 }
1221 // The places widen by the ratio's own decimal places, so 1500 mg shows as 1.500 g rather than rounding to 2 g;
1222 // a ratio that is not a terminating decimal adds none.
1223 auto denominator = static_cast<std::uint64_t>(ratio->denominator);
1224 std::uint32_t twos = 0;
1225 std::uint32_t fives = 0;
1226 for (; denominator % 2U == 0; denominator /= 2U) {
1227 ++twos;
1228 }
1229 for (; denominator % 5U == 0; denominator /= 5U) {
1230 ++fives;
1231 }
1232 std::uint32_t const extra = denominator == 1 ? std::max(twos, fives) : 0U;
1233 converted->decimalPlaces =
1235 return *converted;
1236}
1237
1238/// @brief The display text of a unit.
1239/// @param unit The unit id.
1240/// @param units The unit table.
1241/// @return Its display text, or the id.
1242[[nodiscard]] inline std::string unitDisplay(std::string const& unit, UnitTable const& units) {
1243 auto const found = units.find(unit);
1244 return found == units.end() || found->second.display.empty() ? unit : found->second.display;
1245}
1246
1247/// @brief The fraction digits a currency shows (ISO 4217's minor unit).
1248/// @param currency The currency code.
1249/// @return 0, 2 or 3.
1250[[nodiscard]] inline std::uint32_t currencyDigits(std::string_view currency) {
1251 static constexpr std::array<std::string_view, 9> kNone = {"JPY", "KRW", "VND", "CLP", "ISK",
1252 "UGX", "XAF", "XOF", "PYG"};
1253 static constexpr std::array<std::string_view, 7> kThree = {"BHD", "KWD", "OMR", "JOD", "TND", "IQD", "LYD"};
1254 if (oneOf(kNone, currency)) {
1255 return 0;
1256 }
1257 return oneOf(kThree, currency) ? 3U : 2U;
1258}
1259
1260/// @brief Evaluates expressions against an environment, with the comprehension variables in scope.
1261// NOLINTBEGIN(misc-no-recursion): an expression is a tree and evaluating it descends it; the node bound limits the depth.
1263public:
1264 /// @brief Evaluates against @p env.
1265 /// @param env What the expression reads.
1266 explicit Evaluator(Environment const& env) : _env{&env} {}
1267
1268 /// @brief Evaluates one node.
1269 /// @param expr The node.
1270 /// @return Its value; `null` on a runtime error, which is reported.
1271 [[nodiscard]] Value eval(Expr const& expr) {
1272 switch (expr.kind) {
1273 case ExprKind::Literal:
1274 return expr.literal;
1275 case ExprKind::Ref:
1276 for (auto const& [name, value] : std::views::reverse(_locals)) {
1277 if (name == expr.name) {
1278 return walk(value, expr.fieldPath);
1279 }
1280 }
1281 return _env->ref(expr.name, expr.fieldPath);
1282 case ExprKind::Row:
1283 return _env->row(expr.fieldPath);
1284 case ExprKind::Query:
1285 case ExprKind::Pending:
1286 case ExprKind::Error:
1287 case ExprKind::Result:
1289 case ExprKind::Can:
1290 return _env->control(expr.kind, expr.name);
1291 case ExprKind::Pref:
1292 return _env->pref(expr.name, expr.fieldPath);
1293 case ExprKind::Env:
1294 case ExprKind::Session:
1295 case ExprKind::Host:
1296 return _env->client(expr.kind, expr.name);
1297 case ExprKind::If:
1298 return choice(expr);
1299 case ExprKind::Object: {
1300 ValueRecord record;
1301 for (auto const& [name, field] : expr.fields) {
1302 record.insert_or_assign(name, eval(*field));
1303 }
1304 return record;
1305 }
1306 case ExprKind::List: {
1307 ValueList items;
1308 items.reserve(expr.args.size());
1309 for (ExprPtr const& item : expr.args) {
1310 items.push_back(eval(*item));
1311 }
1312 return items;
1313 }
1314 case ExprKind::Map:
1315 case ExprKind::Filter:
1316 return comprehension(expr);
1317 case ExprKind::Call:
1318 default:
1319 return call(expr);
1320 }
1321 }
1322
1323private:
1324 Value mismatch(Expr const& expr, std::string_view reason) {
1325 _env->report(expr.path, reason);
1326 return Value{};
1327 }
1328
1329 Value choice(Expr const& expr) {
1330 Value const cond = eval(*expr.args.at(0));
1331 if (cond.isNull()) {
1332 return Value{};
1333 }
1334 bool const* const flag = cond.asBool();
1335 if (flag == nullptr) {
1336 return mismatch(expr, "the condition of `if` is not a bool");
1337 }
1338 return eval(*expr.args.at(*flag ? 1 : 2));
1339 }
1340
1341 Value comprehension(Expr const& expr) {
1342 Value const source = eval(*expr.args.at(0));
1343 if (source.isNull()) {
1344 return Value{};
1345 }
1346 ValueList const* const items = source.asList();
1347 if (items == nullptr) {
1348 return mismatch(expr, "a comprehension runs over a list");
1349 }
1350 ValueList out;
1351 for (Value const& item : *items) {
1352 _locals.emplace_back(expr.name, item);
1353 Value body = eval(*expr.args.at(1));
1354 _locals.pop_back();
1355 if (expr.kind == ExprKind::Map) {
1356 out.push_back(std::move(body));
1357 continue;
1358 }
1359 bool const* const keep = body.asBool();
1360 if (keep == nullptr && !body.isNull()) {
1361 return mismatch(expr, "the `where` of `filter` is not a bool");
1362 }
1363 if (keep != nullptr && *keep) {
1364 out.push_back(item);
1365 }
1366 }
1367 return out;
1368 }
1369
1370 /// Three-valued `and` / `or`: the dominant value wins over `null`.
1371 Value kleene(Expr const& expr, bool dominant) {
1372 bool sawNull = false;
1373 for (ExprPtr const& arg : expr.args) {
1374 Value const value = eval(*arg);
1375 if (bool const* const flag = value.asBool()) {
1376 if (*flag == dominant) {
1377 return dominant;
1378 }
1379 continue;
1380 }
1381 if (!value.isNull()) {
1382 _env->report(arg->path, "a logical operand is not a bool");
1383 }
1384 sawNull = true;
1385 }
1386 if (sawNull) {
1387 return Value{};
1388 }
1389 return !dominant;
1390 }
1391
1392 // NOLINTNEXTLINE(readability-function-cognitive-complexity): the four operators over plain numbers and quantities, one rule each.
1393 Value arithmetic(Expr const& expr, Value const& lhsValue, Value const& rhsValue) {
1394 auto const lhs = numeric(lhsValue);
1395 auto const rhs = numeric(rhsValue);
1396 if (!lhs || !rhs) {
1397 return mismatch(expr, "an arithmetic operand is not a number");
1398 }
1399 UnitTable const& units = _env->formatting().units;
1400 FunctionId const function = expr.function;
1401 math::Rational right = rhs->value;
1402 std::string const* unit = nullptr;
1403 if (lhs->unit != nullptr && rhs->unit != nullptr) {
1404 if (function == FunctionId::Mul) {
1405 return mismatch(expr, "a product of two quantities has no declared unit");
1406 }
1407 auto converted = convertUnit(rhs->value, *rhs->unit, *lhs->unit, units);
1408 if (!converted) {
1409 return mismatch(expr, "the units `" + *lhs->unit + "` and `" + *rhs->unit + "` do not convert");
1410 }
1411 right = *converted;
1412 unit = function == FunctionId::Div ? nullptr : lhs->unit;
1413 } else if (lhs->unit != nullptr || rhs->unit != nullptr) {
1414 bool const scales = function == FunctionId::Mul || (function == FunctionId::Div && lhs->unit != nullptr);
1415 if (!scales) {
1416 return mismatch(expr, "a quantity and a plain number do not combine this way");
1417 }
1418 unit = lhs->unit != nullptr ? lhs->unit : rhs->unit;
1419 }
1420 if (function == FunctionId::Div && right.isZero()) {
1421 return mismatch(expr, "division by zero");
1422 }
1423 std::expected<math::Rational, math::RationalError> result = math::Rational{};
1424 if (function == FunctionId::Add) {
1425 result = math::checkedAdd(lhs->value, right);
1426 } else if (function == FunctionId::Sub) {
1427 result = math::checkedSub(lhs->value, right);
1428 } else if (function == FunctionId::Mul) {
1429 result = math::checkedMul(lhs->value, right);
1430 } else {
1431 result = math::checkedDiv(lhs->value, right);
1432 }
1433 if (!result) {
1434 return mismatch(expr, "the result overflows");
1435 }
1436 // A quantity is never an `int`, so a ratio of two quantities comes back a decimal.
1437 return makeNumber(*result, lhs->integer && rhs->integer, unit);
1438 }
1439
1440 /// Orders two values of one comparable kind: numbers exactly (quantities converted), timestamps, strings.
1441 std::optional<std::strong_ordering> order(Value const& lhs, Value const& rhs) {
1442 auto const lhsNumber = numeric(lhs);
1443 auto const rhsNumber = numeric(rhs);
1444 if (lhsNumber && rhsNumber) {
1445 if ((lhsNumber->unit == nullptr) != (rhsNumber->unit == nullptr)) {
1446 return std::nullopt;
1447 }
1448 math::Rational right = rhsNumber->value;
1449 if (lhsNumber->unit != nullptr) {
1450 auto converted = convertUnit(right, *rhsNumber->unit, *lhsNumber->unit, _env->formatting().units);
1451 if (!converted) {
1452 return std::nullopt;
1453 }
1454 right = *converted;
1455 }
1456 return lhsNumber->value <=> right;
1457 }
1458 if (time::DateTime const* const lhsTime = lhs.asTimestamp()) {
1459 if (time::DateTime const* const rhsTime = rhs.asTimestamp()) {
1460 return *lhsTime <=> *rhsTime;
1461 }
1462 return std::nullopt;
1463 }
1464 if (std::string const* const lhsText = lhs.asString()) {
1465 if (std::string const* const rhsText = rhs.asString()) {
1466 return *lhsText <=> *rhsText;
1467 }
1468 }
1469 return std::nullopt;
1470 }
1471
1472 /// Equality over non-null values: numbers by value, lists and records element-wise, other kinds by identity.
1473 bool equal(Value const& lhs, Value const& rhs) {
1474 if (numeric(lhs) && numeric(rhs)) {
1475 auto const ordering = order(lhs, rhs);
1476 return ordering.has_value() && *ordering == std::strong_ordering::equal;
1477 }
1478 if (lhs.kind() != rhs.kind()) {
1479 return false;
1480 }
1481 if (ValueList const* const lhsItems = lhs.asList()) {
1482 return std::ranges::equal(*lhsItems, *rhs.asList(), [this](Value const& left, Value const& right) {
1483 return sameElement(left, right);
1484 });
1485 }
1486 if (ValueRecord const* const lhsFields = lhs.asRecord()) {
1487 // Records are ordered by field name, so equal records have equal sequences of fields.
1488 return std::ranges::equal(*lhsFields, *rhs.asRecord(), [this](auto const& left, auto const& right) {
1489 return left.first == right.first && sameElement(left.second, right.second);
1490 });
1491 }
1492 return lhs == rhs;
1493 }
1494
1495 /// Equality of two elements of a list or record, where `null` equals only `null`.
1496 bool sameElement(Value const& left, Value const& right) {
1497 if (left.isNull() || right.isNull()) {
1498 return left.isNull() && right.isNull();
1499 }
1500 return equal(left, right);
1501 }
1502
1503 /// The display text of a value, as `format` shows an argument.
1504 std::optional<std::string> display(Value const& value) {
1505 FormatContext const& context = _env->formatting();
1506 if (std::string const* const text = value.asString()) {
1507 return *text;
1508 }
1509 if (bool const* const flag = value.asBool()) {
1510 return std::string{*flag ? "true" : "false"};
1511 }
1512 if (Quantity const* const quantity = value.asQuantity()) {
1513 return formatNumber(value, context.numeric).value_or(std::string{}) + " " +
1514 unitDisplay(quantity->unit, context.units);
1515 }
1516 if (auto number = formatNumber(value, context.numeric)) {
1517 return number;
1518 }
1519 if (time::DateTime const* const instant = value.asTimestamp()) {
1520 return dateText(*instant, "medium", true);
1521 }
1522 return std::nullopt;
1523 }
1524
1525 std::optional<std::string> dateText(time::DateTime instant, std::string_view style, bool withTime) {
1526 if (style != "short" && style != "medium" && style != "long") {
1527 return std::nullopt;
1528 }
1529 FormatContext const& context = _env->formatting();
1530 time::DateTime const local = instant + std::chrono::minutes{context.utcOffsetMinutes};
1531 if (context.dates) {
1532 return context.dates(local, style, withTime);
1533 }
1534 std::string const iso = local.toIso8601();
1535 if (!withTime) {
1536 return iso.substr(0, 10);
1537 }
1538 return iso.substr(0, 10) + " " + iso.substr(11, style == "long" ? 8 : 5);
1539 }
1540
1541 Value formatTemplate(Expr const& expr, std::vector<Value> const& args) {
1542 std::string const& pattern = *args.front().asString();
1543 std::vector<std::string> shown;
1544 for (std::size_t index = 1; index < args.size(); ++index) {
1545 auto text = display(args.at(index));
1546 if (!text) {
1547 return mismatch(expr, "`format` cannot show argument " + std::to_string(index + 1));
1548 }
1549 shown.push_back(std::move(*text));
1550 }
1551 std::string out;
1552 for (std::size_t pos = 0; pos < pattern.size(); ++pos) {
1553 char const chr = pattern.at(pos);
1554 if ((chr == '{' || chr == '}') && pos + 1 < pattern.size() && pattern.at(pos + 1) == chr) {
1555 out += chr;
1556 ++pos;
1557 continue;
1558 }
1559 if (chr == '}') {
1560 return mismatch(expr, "`format`'s template has an unmatched `}`");
1561 }
1562 if (chr != '{') {
1563 out += chr;
1564 continue;
1565 }
1566 std::size_t const close = pattern.find('}', pos);
1567 std::string_view const digits = close == std::string::npos
1568 ? std::string_view{}
1569 : std::string_view{pattern}.substr(pos + 1, close - pos - 1);
1570 std::size_t index = 0;
1571 bool valid = !digits.empty() && digits.size() < 4;
1572 for (char const digit : digits) {
1573 valid = valid && digit >= '0' && digit <= '9';
1574 index = valid ? (index * 10) + static_cast<std::size_t>(digit - '0') : index;
1575 }
1576 if (!valid || index >= shown.size()) {
1577 return mismatch(expr, "`format`'s template has a placeholder with no argument");
1578 }
1579 out += shown.at(index);
1580 pos = close;
1581 }
1582 return out;
1583 }
1584
1585 Value translate(Expr const& expr, std::vector<Value> const& args) {
1586 FormatContext const& context = _env->formatting();
1587 std::string const& key = *args.at(0).asString();
1588 std::string const& fallback = *args.at(1).asString();
1589 ValueRecord const empty;
1590 ValueRecord const* named = &empty;
1591 if (args.size() > 2) {
1592 named = args.at(2).asRecord();
1593 if (named == nullptr) {
1594 return mismatch(expr, "the arguments of `t` are a record");
1595 }
1596 }
1597 std::optional<MessagePattern> message;
1598 if (context.translations) {
1599 if (auto const pattern = context.translations(key, context.locale)) {
1600 auto parsed = parseMessage(*pattern);
1601 if (parsed) {
1602 message = std::move(*parsed);
1603 } else {
1604 _env->report(expr.path, "the catalogue's message `" + key +
1605 "` is outside the ui/1 subset: " + parsed.error());
1606 }
1607 }
1608 }
1609 if (!message) {
1610 auto parsed = parseMessage(fallback);
1611 if (!parsed) {
1612 _env->report(expr.path, "the fallback is outside the ui/1 subset: " + parsed.error());
1613 return fallback;
1614 }
1615 message = std::move(*parsed);
1616 }
1617 FormattedMessage formatted =
1618 formatMessage(*message, *named, MessageLocale{.locale = context.locale, .numeric = context.numeric});
1619 for (std::string const& error : formatted.errors) {
1620 _env->report(expr.path, error);
1621 }
1622 return std::move(formatted.text);
1623 }
1624
1625 Value listFunction(Expr const& expr, ValueList const& items) {
1626 FunctionId const function = expr.function;
1627 if (function == FunctionId::Count) {
1628 return static_cast<std::int64_t>(items.size());
1629 }
1630 if (function == FunctionId::First) {
1631 if (items.empty()) {
1632 return Value{};
1633 }
1634 return items.front();
1635 }
1636 if (function == FunctionId::Any || function == FunctionId::All) {
1637 bool const dominant = function == FunctionId::Any;
1638 bool sawNull = false;
1639 for (Value const& item : items) {
1640 bool const* const flag = item.asBool();
1641 if (flag != nullptr && *flag == dominant) {
1642 return dominant;
1643 }
1644 if (flag == nullptr && !item.isNull()) {
1645 return mismatch(expr, "an element is not a bool");
1646 }
1647 sawNull = sawNull || item.isNull();
1648 }
1649 if (sawNull) {
1650 return Value{};
1651 }
1652 return !dominant;
1653 }
1654 if (function == FunctionId::Min || function == FunctionId::Max) {
1655 return extreme(expr, items);
1656 }
1657 return aggregate(expr, items);
1658 }
1659
1660 Value extreme(Expr const& expr, ValueList const& items) {
1661 if (items.empty()) {
1662 return Value{};
1663 }
1664 Value const* best = &items.front();
1665 for (Value const& item : items) {
1666 if (item.isNull()) {
1667 return Value{};
1668 }
1669 auto const ordering = order(item, *best);
1670 if (!ordering) {
1671 return mismatch(expr, "the elements are not of one comparable kind");
1672 }
1673 bool const better = expr.function == FunctionId::Min ? std::is_lt(*ordering) : std::is_gt(*ordering);
1674 if (better) {
1675 best = &item;
1676 }
1677 }
1678 return *best;
1679 }
1680
1681 Value aggregate(Expr const& expr, ValueList const& items) {
1682 bool const mean = expr.function == FunctionId::Mean;
1683 if (items.empty()) {
1684 return mean ? Value{} : Value{std::int64_t{0}};
1685 }
1686 math::Rational total{0, math::DecimalPlaces{0}};
1687 bool integer = true;
1688 std::string const* unit = nullptr;
1689 std::uint32_t places = 0;
1690 for (std::size_t index = 0; index < items.size(); ++index) {
1691 Value const& item = items.at(index);
1692 if (item.isNull()) {
1693 return Value{};
1694 }
1695 auto const number = numeric(item);
1696 if (!number) {
1697 return mismatch(expr, "an element is not a number");
1698 }
1699 if (index == 0) {
1700 unit = number->unit;
1701 } else if ((unit == nullptr) != (number->unit == nullptr)) {
1702 return mismatch(expr, "quantities and plain numbers do not sum together");
1703 }
1704 math::Rational value = number->value;
1705 if (unit != nullptr) {
1706 auto converted = convertUnit(value, *number->unit, *unit, _env->formatting().units);
1707 if (!converted) {
1708 return mismatch(expr, "the elements' units do not convert");
1709 }
1710 value = *converted;
1711 }
1712 places = std::max(places, value.decimalPlaces.value);
1713 integer = integer && number->integer;
1714 auto next = math::checkedAdd(total, value);
1715 if (!next) {
1716 return mismatch(expr, "the sum overflows");
1717 }
1718 total = *next;
1719 }
1720 if (!mean) {
1721 return makeNumber(total, integer, unit);
1722 }
1723 auto const quotient =
1724 math::checkedDiv(total, math::Rational{static_cast<std::int64_t>(items.size()), math::DecimalPlaces{0}});
1725 // A sum of int64 values divided by their count cannot overflow, and the count is never zero here.
1726 math::Rational const rounded = math::roundToDecimalPlaces(
1727 quotient.value_or(total), math::DecimalPlaces{places}, math::RoundingMode::HalfEven);
1728 return makeNumber(rounded, integer, unit);
1729 }
1730
1731 Value call(Expr const& expr) {
1732 FunctionId const function = expr.function;
1733 if (function == FunctionId::And || function == FunctionId::Or) {
1734 return kleene(expr, function == FunctionId::Or);
1735 }
1736 if (function == FunctionId::Coalesce) {
1737 for (ExprPtr const& arg : expr.args) {
1738 Value value = eval(*arg);
1739 if (!value.isNull()) {
1740 return value;
1741 }
1742 }
1743 return Value{};
1744 }
1745 if (function == FunctionId::Engaged) {
1746 return !eval(*expr.args.front()).isNull();
1747 }
1748 std::vector<Value> args;
1749 args.reserve(expr.args.size());
1750 for (ExprPtr const& arg : expr.args) {
1751 args.push_back(eval(*arg));
1752 if (args.back().isNull()) {
1753 return Value{};
1754 }
1755 }
1756 return strict(expr, args);
1757 }
1758
1759 /// The functions whose every argument is required: any `null` argument has already made the result `null`.
1760 // NOLINTNEXTLINE(readability-function-cognitive-complexity): one case per function of the library.
1761 // GCC's -Wstrict-overflow fires on this whole function at -O3, attributing it to the function's opening line
1762 // rather than a specific statement -- a known false positive from its range analysis across the switch's many
1763 // checked-arithmetic calls (Rational's own checkedAdd/checkedSub/checkedMul are all free of signed-overflow UB).
1764#if defined(__GNUC__) && !defined(__clang__)
1765#pragma GCC diagnostic push
1766#pragma GCC diagnostic ignored "-Wstrict-overflow"
1767#endif
1768 Value strict(Expr const& expr, std::vector<Value> const& args) {
1769 FormatContext const& context = _env->formatting();
1770 switch (expr.function) {
1771 case FunctionId::Not: {
1772 bool const* const flag = args.front().asBool();
1773 if (flag == nullptr) {
1774 return mismatch(expr, "`not` takes a bool");
1775 }
1776 return !*flag;
1777 }
1778 case FunctionId::Eq:
1779 return equal(args.at(0), args.at(1));
1780 case FunctionId::Ne:
1781 return !equal(args.at(0), args.at(1));
1782 case FunctionId::Lt:
1783 case FunctionId::Le:
1784 case FunctionId::Gt:
1785 case FunctionId::Ge: {
1786 auto const ordering = order(args.at(0), args.at(1));
1787 if (!ordering) {
1788 return mismatch(expr, "the operands are not of one comparable kind");
1789 }
1790 if (expr.function == FunctionId::Lt) {
1791 return std::is_lt(*ordering);
1792 }
1793 if (expr.function == FunctionId::Le) {
1794 return std::is_lteq(*ordering);
1795 }
1796 if (expr.function == FunctionId::Gt) {
1797 return std::is_gt(*ordering);
1798 }
1799 return std::is_gteq(*ordering);
1800 }
1801 case FunctionId::Add:
1802 case FunctionId::Sub:
1803 case FunctionId::Mul:
1804 case FunctionId::Div:
1805 return arithmetic(expr, args.at(0), args.at(1));
1806 case FunctionId::Neg: {
1807 auto const number = numeric(args.front());
1808 if (!number) {
1809 return mismatch(expr, "`neg` takes a number");
1810 }
1811 auto negated = math::checkedSub(math::Rational{0, number->value.decimalPlaces}, number->value);
1812 if (!negated) {
1813 return mismatch(expr, "the result overflows");
1814 }
1815 return makeNumber(*negated, number->integer, number->unit);
1816 }
1817 case FunctionId::Round: {
1818 auto const number = numeric(args.at(0));
1819 std::int64_t const* const places = args.at(1).asInt();
1820 if (!number || places == nullptr || *places < 0 ||
1821 std::cmp_greater(*places, math::kMaxDecimalPlaces)) {
1822 return mismatch(expr, "`round` takes a number and places from 0 to 18");
1823 }
1824 if (number->integer) {
1825 return args.at(0);
1826 }
1827 auto const placesValue = static_cast<std::uint32_t>(*places);
1828 std::int64_t scale = 1;
1829 for (std::uint32_t digit = 0; digit < placesValue; ++digit) {
1830 scale *= 10;
1831 }
1832 if (!math::checkedMul(number->value, math::Rational{scale, math::DecimalPlaces{0}})) {
1833 return mismatch(expr, "the result overflows");
1834 }
1835 return makeNumber(math::roundToDecimalPlaces(number->value, math::DecimalPlaces{placesValue},
1836 math::RoundingMode::HalfEven),
1837 false, number->unit);
1838 }
1839 case FunctionId::Concat: {
1840 std::string out;
1841 for (Value const& arg : args) {
1842 std::string const* const text = arg.asString();
1843 if (text == nullptr) {
1844 return mismatch(expr, "`concat` takes strings");
1845 }
1846 out += *text;
1847 }
1848 return out;
1849 }
1850 case FunctionId::Format:
1851 if (args.front().asString() == nullptr) {
1852 return mismatch(expr, "`format`'s template is a string");
1853 }
1854 return formatTemplate(expr, args);
1855 case FunctionId::Trim: {
1856 std::string const* const text = args.front().asString();
1857 if (text == nullptr) {
1858 return mismatch(expr, "`trim` takes a string");
1859 }
1860 constexpr std::string_view kSpace = " \t\n\r\f\v";
1861 std::size_t const first = text->find_first_not_of(kSpace);
1862 if (first == std::string::npos) {
1863 return std::string{};
1864 }
1865 return text->substr(first, text->find_last_not_of(kSpace) - first + 1);
1866 }
1867 case FunctionId::Length: {
1868 std::string const* const text = args.front().asString();
1869 if (text == nullptr) {
1870 return mismatch(expr, "`length` takes a string");
1871 }
1872 std::int64_t points = 0;
1873 for (char const chr : *text) {
1874 points += (static_cast<unsigned char>(chr) & 0xC0U) == 0x80U ? 0 : 1;
1875 }
1876 return points;
1877 }
1878 case FunctionId::IsEmpty: {
1879 if (std::string const* const text = args.front().asString()) {
1880 return text->empty();
1881 }
1882 if (ValueList const* const items = args.front().asList()) {
1883 return items->empty();
1884 }
1885 return mismatch(expr, "`isEmpty` takes a string or a list");
1886 }
1887 case FunctionId::Contains: {
1888 std::string const* const text = args.at(0).asString();
1889 std::string const* const part = args.at(1).asString();
1890 if (text == nullptr || part == nullptr) {
1891 return mismatch(expr, "`contains` takes two strings");
1892 }
1893 return text->contains(*part);
1894 }
1895 case FunctionId::Count:
1896 case FunctionId::Sum:
1897 case FunctionId::Mean:
1898 case FunctionId::Min:
1899 case FunctionId::Max:
1900 case FunctionId::Any:
1901 case FunctionId::All:
1902 case FunctionId::First: {
1903 ValueList const* const items = args.front().asList();
1904 if (items == nullptr) {
1905 return mismatch(expr, "the argument is not a list");
1906 }
1907 return listFunction(expr, *items);
1908 }
1909 case FunctionId::Includes:
1910 case FunctionId::IndexOf: {
1911 ValueList const* const items = args.at(0).asList();
1912 if (items == nullptr) {
1913 return mismatch(expr, "the first argument is not a list");
1914 }
1915 for (std::size_t index = 0; index < items->size(); ++index) {
1916 if (!items->at(index).isNull() && equal(items->at(index), args.at(1))) {
1917 return expr.function == FunctionId::Includes ? Value{true}
1918 : Value{static_cast<std::int64_t>(index)};
1919 }
1920 }
1921 return expr.function == FunctionId::Includes ? Value{false} : Value{};
1922 }
1923 case FunctionId::Number: {
1924 auto text = formatNumber(args.front(), context.numeric);
1925 if (!text) {
1926 return mismatch(expr, "`number` takes a number");
1927 }
1928 return std::move(*text);
1929 }
1930 case FunctionId::DecimalText: {
1931 auto const number = numeric(args.at(0));
1932 std::int64_t const* const places = args.at(1).asInt();
1933 if (!number || places == nullptr || *places < 0 ||
1934 std::cmp_greater(*places, math::kMaxDecimalPlaces)) {
1935 return mismatch(expr, "`decimal` takes a number and places from 0 to 18");
1936 }
1937 return render::formatCanonicalNumber(decimalText(number->value, static_cast<std::uint32_t>(*places)),
1938 context.numeric);
1939 }
1940 case FunctionId::QuantityText: {
1941 Quantity const* const quantity = args.at(0).asQuantity();
1942 if (quantity == nullptr) {
1943 return mismatch(expr, "`quantity` takes a quantity");
1944 }
1945 Quantity shown = *quantity;
1946 if (args.size() > 1) {
1947 std::string const* const unit = args.at(1).asString();
1948 auto converted = unit == nullptr
1949 ? std::nullopt
1950 : convertUnit(quantity->value, quantity->unit, *unit, context.units);
1951 if (!converted) {
1952 return mismatch(expr, "the quantity does not convert to that unit");
1953 }
1954 shown = Quantity{.value = *converted, .unit = *unit};
1955 }
1956 return formatNumber(shown, context.numeric).value_or(std::string{}) + " " +
1957 unitDisplay(shown.unit, context.units);
1958 }
1959 case FunctionId::Money: {
1960 auto const number = numeric(args.at(0));
1961 std::string const* const currency = args.at(1).asString();
1962 if (!number || number->unit != nullptr || currency == nullptr) {
1963 return mismatch(expr, "`money` takes an amount and a currency code");
1964 }
1965 return render::formatCanonicalNumber(decimalText(number->value, currencyDigits(*currency)),
1966 context.numeric) +
1967 " " + *currency;
1968 }
1969 case FunctionId::Date:
1970 case FunctionId::DateTime: {
1971 std::optional<time::DateTime> instant;
1972 if (time::DateTime const* const held = args.at(0).asTimestamp()) {
1973 instant = *held;
1974 } else if (std::string const* const text = args.at(0).asString()) {
1975 instant = time::DateTime::fromIso8601(*text);
1976 }
1977 std::string const* const style = args.size() > 1 ? args.at(1).asString() : nullptr;
1978 std::optional<std::string> text;
1979 if (instant && (args.size() == 1 || style != nullptr)) {
1980 text = dateText(*instant, style == nullptr ? std::string_view{"medium"} : *style,
1981 expr.function == FunctionId::DateTime);
1982 }
1983 if (!text) {
1984 return mismatch(expr, "the date functions take a timestamp and a style");
1985 }
1986 return std::move(*text);
1987 }
1988 case FunctionId::ParseDecimal: {
1989 std::string const* const text = args.at(0).asString();
1990 std::optional<std::uint32_t> declared;
1991 if (args.size() > 1) {
1992 std::int64_t const* const places = args.at(1).asInt();
1993 if (places == nullptr || !std::in_range<std::uint32_t>(*places) ||
1994 std::cmp_greater(*places, math::kMaxDecimalPlaces)) {
1995 return mismatch(expr, "`parseDecimal` takes text and places from 0 to 18");
1996 }
1997 declared = static_cast<std::uint32_t>(*places);
1998 }
1999 if (text == nullptr) {
2000 return mismatch(expr, "`parseDecimal` takes text and places from 0 to 18");
2001 }
2002 // Text that does not spell a number is what an entry holds while it is typed: `null`, not an error.
2003 if (auto parsed = render::parseDecimal(*text, context.numeric, declared)) {
2004 return Decimal{*parsed};
2005 }
2006 return Value{};
2007 }
2008 case FunctionId::ConcatLists: {
2009 ValueList out;
2010 for (Value const& arg : args) {
2011 ValueList const* const items = arg.asList();
2012 if (items == nullptr) {
2013 return mismatch(expr, "`concatLists` takes lists");
2014 }
2015 out.insert(out.end(), items->begin(), items->end());
2016 }
2017 return out;
2018 }
2019 case FunctionId::Translate:
2020 if (args.at(0).asString() == nullptr || args.at(1).asString() == nullptr) {
2021 return mismatch(expr, "`t` takes a key and a fallback string");
2022 }
2023 return translate(expr, args);
2024 case FunctionId::And:
2025 case FunctionId::Or:
2026 case FunctionId::Coalesce:
2027 case FunctionId::Engaged:
2028 default:
2029 return Value{};
2030 }
2031 }
2032#if defined(__GNUC__) && !defined(__clang__)
2033#pragma GCC diagnostic pop
2034#endif
2035
2036 Environment const* _env;
2037 std::vector<std::pair<std::string, Value>> _locals;
2038 // NOLINTEND(misc-no-recursion)
2039};
2040
2041} // namespace detail
2042
2043/// @brief Evaluates an expression.
2044///
2045/// Total: it always returns. A function whose required argument is `null` yields `null`; `and`, `or` and `not` are
2046/// three-valued; a type mismatch, an overflow, a division by zero or a unit that does not convert yields `null`
2047/// and is reported through `Environment::report` with the failing node's path.
2048/// @param expr The expression.
2049/// @param env What it reads.
2050/// @return Its value.
2051[[nodiscard]] inline Value evaluate(Expr const& expr, Environment const& env) {
2052 return detail::Evaluator{env}.eval(expr);
2053}
2054
2055/// @brief An environment that holds its values: for tests, and for evaluating outside a mounted screen.
2056///
2057/// Each runtime error is recorded once per expression path in `diagnostics`.
2058class StaticEnvironment final : public Environment {
2059public:
2060 /// @brief The values `ref` reads, by head name.
2062 /// @brief The innermost row.
2064 /// @brief Query values, by name.
2066 /// @brief Mutation results, by name.
2068 /// @brief Error texts, by query or mutation name.
2070 /// @brief Error kinds, by query or mutation name.
2072 /// @brief The queries and mutations in flight.
2073 std::set<std::string, std::less<>> pendingNames;
2074 /// @brief What the principal may run.
2075 std::set<std::string, std::less<>> permitted;
2076 /// @brief Customization choices, by point.
2078 /// @brief `env` values.
2080 /// @brief `session` values.
2082 /// @brief `host` values.
2084 /// @brief The formatting facts.
2086 /// @brief The runtime errors reported, each path once.
2087 mutable std::vector<Refusal> diagnostics;
2088
2089 /// @brief Reads a name and walks its path.
2090 /// @param head The name.
2091 /// @param path The path.
2092 /// @return The value, or `null`.
2093 [[nodiscard]] Value ref(std::string_view head, std::span<std::string const> path) const override {
2094 return lookup(names, head, path);
2095 }
2096
2097 /// @brief Walks the row.
2098 /// @param path The path.
2099 /// @return The value, or `null`.
2100 [[nodiscard]] Value row(std::span<std::string const> path) const override { return walk(rowValue, path); }
2101
2102 /// @brief Reads a control form from the maps.
2103 /// @param form The form.
2104 /// @param name The query or mutation.
2105 /// @return The value.
2106 [[nodiscard]] Value control(ExprKind form, std::string_view name) const override {
2107 if (form == ExprKind::Pending) {
2108 return pendingNames.contains(name);
2109 }
2110 if (form == ExprKind::Can) {
2111 return permitted.contains(name);
2112 }
2113 if (form == ExprKind::Error) {
2114 return lookup(errors, name, {});
2115 }
2116 if (form == ExprKind::ErrorKind) {
2117 return lookup(errorKinds, name, {});
2118 }
2119 return lookup(form == ExprKind::Result ? results : queries, name, {});
2120 }
2121
2122 /// @brief Reads a customization choice.
2123 /// @param point The point.
2124 /// @param path The path.
2125 /// @return The value, or `null`.
2126 [[nodiscard]] Value pref(std::string_view point, std::span<std::string const> path) const override {
2127 return lookup(prefs, point, path);
2128 }
2129
2130 /// @brief Reads `env`, `session` or `host`.
2131 /// @param form The form.
2132 /// @param name The name.
2133 /// @return The value, or `null`.
2134 [[nodiscard]] Value client(ExprKind form, std::string_view name) const override {
2135 if (form == ExprKind::Env) {
2136 return lookup(environment, name, {});
2137 }
2138 return lookup(form == ExprKind::Session ? session : host, name, {});
2139 }
2140
2141 /// @brief The formatting facts.
2142 /// @return `format`.
2143 [[nodiscard]] FormatContext const& formatting() const override { return format; }
2144
2145 /// @brief Records a runtime error, once per path.
2146 /// @param path The expression node's path.
2147 /// @param reason What went wrong.
2148 void report(std::string_view path, std::string_view reason) const override {
2149 for (Refusal const& seen : diagnostics) {
2150 if (seen.path == path) {
2151 return;
2152 }
2153 }
2154 diagnostics.push_back(Refusal{.document = {}, .path = std::string{path}, .reason = std::string{reason}});
2155 }
2156
2157private:
2158 [[nodiscard]] static Value lookup(ValueRecord const& values, std::string_view name,
2159 std::span<std::string const> path) {
2160 auto const found = values.find(name);
2161 return found == values.end() ? Value{} : walk(found->second, path);
2162 }
2163};
2164
2165} // namespace morph::ui
Node text(TextOptions const &options)
A text node.
Definition author.hpp:3126
Element element(std::string name)
An element of the view.
Definition author.hpp:1289
Handle< T > cond(Handle< bool > const &condition, Handle< T > const &then, Handle< T > const &otherwise)
{"if": [c, a, b]}: a choice between two values.
Definition author.hpp:1085
Handle< std::string > quantity(AnyHandle const &value)
quantity(x): a quantity for display in its own unit.
Definition author.hpp:1046
Command close(Element const &dialog)
close: closes a dialog or drawer.
Definition author.hpp:2784
What an evaluated expression reads: the interpreter implements it over its reactive values.
virtual FormatContext const & formatting() const =0
The formatting facts.
virtual Value row(std::span< std::string const > path) const =0
Reads {"row": "path"}.
virtual void report(std::string_view path, std::string_view reason) const =0
Reports a runtime error of an expression.
virtual Value ref(std::string_view head, std::span< std::string const > path) const =0
Reads {"ref": "head.path"}.
virtual Value control(ExprKind form, std::string_view name) const =0
Reads query, pending, error, result, errorKind or can.
virtual Value pref(std::string_view point, std::span< std::string const > path) const =0
Reads {"pref": "point.path"}.
virtual Value client(ExprKind form, std::string_view name) const =0
Reads env, session or host.
What an expression's names resolve to, for checkExpression: the document's scopes implement it.
virtual std::expected< Type, std::string > ref(std::string_view head, std::span< std::string const > path) const =0
Resolves {"ref": "head.path"}.
virtual std::expected< Type, std::string > row(std::span< std::string const > path) const =0
Resolves {"row": "path"}.
virtual std::expected< Type, std::string > pref(std::string_view point, std::span< std::string const > path) const =0
Resolves {"pref": "point.path"}.
virtual std::expected< Type, std::string > control(ExprKind form, std::string_view name) const =0
Resolves the name of query, pending, error, result, errorKind or can.
A resolver that accepts every name as any: for an expression checked outside a document.
std::expected< Type, std::string > pref(std::string_view point, std::span< std::string const > path) const override
Accepts any point.
std::expected< Type, std::string > control(ExprKind form, std::string_view name) const override
Accepts any query or mutation.
std::expected< Type, std::string > ref(std::string_view head, std::span< std::string const > path) const override
Accepts any reference.
std::expected< Type, std::string > row(std::span< std::string const > path) const override
Accepts any row path.
What screens of one client session share (spec 5 §5, §12b): the query cache, the mounted queries,...
An environment that holds its values: for tests, and for evaluating outside a mounted screen.
Value row(std::span< std::string const > path) const override
Walks the row.
Value control(ExprKind form, std::string_view name) const override
Reads a control form from the maps.
std::set< std::string, std::less<> > pendingNames
The queries and mutations in flight.
FormatContext format
The formatting facts.
ValueRecord session
session values.
Value client(ExprKind form, std::string_view name) const override
Reads env, session or host.
Value ref(std::string_view head, std::span< std::string const > path) const override
Reads a name and walks its path.
ValueRecord prefs
Customization choices, by point.
ValueRecord errors
Error texts, by query or mutation name.
Value rowValue
The innermost row.
ValueRecord names
The values ref reads, by head name.
ValueRecord results
Mutation results, by name.
std::vector< Refusal > diagnostics
The runtime errors reported, each path once.
std::set< std::string, std::less<> > permitted
What the principal may run.
ValueRecord host
host values.
ValueRecord queries
Query values, by name.
ValueRecord errorKinds
Error kinds, by query or mutation name.
void report(std::string_view path, std::string_view reason) const override
Records a runtime error, once per path.
Value pref(std::string_view point, std::span< std::string const > path) const override
Reads a customization choice.
ValueRecord environment
env values.
FormatContext const & formatting() const override
The formatting facts.
The document's typed value (spec 5 §3): what every expression evaluates to.
Definition value.hpp:157
Quantity const * asQuantity() const noexcept
The quantity, if held.
Definition value.hpp:232
Decimal const * asDecimal() const noexcept
The decimal, if held.
Definition value.hpp:228
bool isNull() const noexcept
Whether the value is null.
Definition value.hpp:212
ValueKind kind() const noexcept
The kind of value held.
Definition value.hpp:208
std::int64_t const * asInt() const noexcept
The int, if held.
Definition value.hpp:220
Evaluates expressions against an environment, with the comprehension variables in scope.
Value eval(Expr const &expr)
Evaluates one node.
Evaluator(Environment const &env)
Evaluates against env.
Walks an expression once, resolving names and checking the statically known types.
std::expected< Type, Refusal > check(Expr const &expr)
Checks one node and its operands.
ExprChecker(NameResolver const &resolver)
Checks against resolver.
Reads one JSON expression tree into Expr nodes, counting them against the size bound.
std::expected< ExprPtr, Refusal > parse(Json const &json, std::string const &path)
Parses json, whose JSON Pointer is path.
UTC timestamps for morph actions.
@ If
{"if": [cond, [...], [...]]}
Definition document.hpp:259
@ Host
{"host": "name", "args": e?}
Definition document.hpp:252
constexpr std::array< std::string_view, 6 > kSessionNames
The names {"session": ...} reads.
std::optional< math::Rational > convertUnit(math::Rational const &value, std::string_view from, std::string_view target, UnitTable const &units)
Converts an exact value between two units of one dimension.
std::string unitDisplay(std::string const &unit, UnitTable const &units)
The display text of a unit.
std::map< std::string, UnitInfo, std::less<> > UnitTable
The units a client knows, by id. A unit missing from the table is its own canonical unit.
constexpr FunctionSpec const & functionSpec(FunctionId function)
The signature of a function.
std::expected< ExprPtr, Refusal > parseExpression(Json const &json, std::string const &path="")
Parses one expression.
constexpr KindSet kOrdered
The ordered kinds.
std::expected< Type, std::string > typeAlong(Type const &type, std::string_view head, std::span< std::string const > path)
The type a field path reads from a value of a known type.
std::expected< Type, Refusal > checkExpression(Expr const &expr, NameResolver const &resolver)
Checks an expression against the names a document declares.
std::expected< std::vector< std::string >, std::string > splitPath(std::string_view text)
Splits head.a.b into its segments.
Value evaluate(Expr const &expr, Environment const &env)
Evaluates an expression.
std::uint16_t KindSet
A set of type kinds, as bits.
std::vector< std::string_view > functionNames()
The names of the expr/1 library, in FunctionId order.
std::optional< Number > numeric(Value const &value)
Takes a numeric value apart.
constexpr std::array< std::string_view, 5 > kEnvNames
The names {"env": ...} reads.
constexpr KindSet kNumeric
The numbers.
constexpr bool oneOf(std::array< std::string_view, N > const &names, std::string_view name) noexcept
Whether a name is one of a closed set.
constexpr bool isName(std::string_view text) noexcept
Whether a string is a document name: [A-Za-z_][A-Za-z0-9_]*.
constexpr std::array< FunctionSpec, 42 > kFunctions
The library, indexed by FunctionId.
FunctionId
The functions of the expr/1 library.
constexpr KindSet kAllKinds
Every kind.
Type typeOfValue(Value const &value)
The static type of a literal.
ExprKind
The forms of an expr/1 expression (spec 5 §4).
@ ErrorKind
{"errorKind": "m"}: the kind of a query's or mutation's error.
@ Pending
{"pending": "q"}: whether a query or mutation is in flight.
@ Map
{"map": e, "as": "x", "to": e}.
@ Object
{"obj": {...}}.
@ List
{"list": [...]}.
@ Pref
{"pref": "point.path"}: the user's choice at a customization point.
@ Ref
{"ref": "name.path"}.
@ Query
{"query": "q"}: a query's value.
@ Session
{"session": "name"}: a session attribute.
@ If
{"if": [cond, then, else]}.
@ Result
{"result": "m"}: a mutation's last result.
@ Error
{"error": "q"}: a query's or mutation's error text.
@ Row
{"row": "path"}.
@ Env
{"env": "name"}: the client environment.
@ Host
{"host": "name"}: a value the application registered with the client.
@ Call
{"fn": "name", "args": [...]}, and {"t": ...}.
@ Can
{"can": "m"}: whether the principal may run a mutation or query.
@ Filter
{"filter": e, "as": "x", "where": e}.
@ Literal
A JSON scalar, or {"lit": v}.
constexpr std::size_t kMaxExpressionNodes
The most nodes one expression may have; a document with a larger expression is refused at load.
constexpr KindSet kindBit(TypeKind kind) noexcept
The bit of one kind.
Value walk(Value const &value, std::span< std::string const > path)
Reads a path out of a value: a record's field by name, a list's element by index.
std::uint32_t currencyDigits(std::string_view currency)
The fraction digits a currency shows (ISO 4217's minor unit).
Value makeNumber(math::Rational const &value, bool integer, std::string const *unit)
Builds a numeric value.
std::function< std::string(time::DateTime local, std::string_view style, bool withTime)> DateFormatter
Formats a date for display: the instant already shifted to the display zone, the style (short,...
std::shared_ptr< Expr const > ExprPtr
A shared, immutable expression node.
@ Quantity
An exact {num, den, dp} in a unit, with optional convertible alternatives.
@ Decimal
An exact {num, den, dp} with a declared precision and no unit.
Locale numeric-entry normalisation for the control edge.
The MessageFormat 2 subset ui/1 accepts for t(key, fallback, args), and the CLDR plural rules its ....
std::optional< std::string > formatNumber(Value const &value, render::NumericLocale const &numeric)
Formats a number for display in a locale: at its own places, rounded half to even.
Definition message.hpp:97
FormattedMessage formatMessage(MessagePattern const &message, ValueRecord const &args, MessageLocale const &locale)
Formats a parsed message with named arguments.
Definition message.hpp:775
std::expected< MessagePattern, std::string > parseMessage(std::string_view text)
Parses a message in the ui/1 subset of MessageFormat 2.
Definition message.hpp:631
Exact rational arithmetic for morph model/action values.
constexpr std::expected< Rational, RationalError > checkedMul(const Rational &lhs, const Rational &rhs) noexcept
Multiplies two Rationals, reporting overflow instead of saturating.
constexpr std::expected< Rational, RationalError > checkedDiv(const Rational &lhs, const Rational &rhs) noexcept
Divides two Rationals, reporting overflow instead of saturating.
constexpr std::uint32_t kMaxDecimalPlaces
Largest decimal precision the type supports: 10^18 is the greatest power of ten that fits in std::int...
Definition rational.hpp:167
The renderer-side translation-catalog seam over morph's derived message keys (morph::forms::i18n,...
std::function< std::optional< std::string >(std::string_view key, std::string_view bcp47Locale)> TranslationProvider
A host-supplied catalog lookup: key/bcp47Locale -> translated text, or std::nullopt on a miss.
Definition i18n.hpp:27
Strong type for a decimal-precision count.
Definition rational.hpp:121
std::uint32_t value
Raw digit count. Rational's invariant keeps it in [0, kMaxDecimalPlaces].
Definition rational.hpp:123
Strong type for a Rational denominator.
Definition rational.hpp:154
Strong type for a Rational numerator.
Definition rational.hpp:140
Exact rational number with a runtime decimal-precision tag.
Definition rational.hpp:375
constexpr bool isInteger() const noexcept
Whether the value is an integer.
Definition rational.hpp:470
DecimalPlaces decimalPlaces
Decimal-precision tag. Invariant: 0 <= value <= kMaxDecimalPlaces.
Definition rational.hpp:383
std::int64_t numerator
Signed numerator. Carries the sign of the rational value.
Definition rational.hpp:377
The locale facts both control-edge conversions need, in one designated-initialisable aggregate.
A UTC instant with millisecond precision.
Definition datetime.hpp:71
static std::optional< DateTime > fromIso8601(std::string_view text) noexcept
Strictly parses ISO-8601 UTC: YYYY-MM-DDTHH:MM:SS, optionally followed by .m/.mm/....
Definition datetime.hpp:148
An exact decimal: a math::Rational whose DecimalPlaces are the value's declared places.
Definition value.hpp:95
One node of a parsed expression. Immutable once parsed; shared between the document and its readers.
std::vector< std::string > fieldPath
The path after the head of a Ref or Pref, or the path of a Row.
std::vector< std::pair< std::string, ExprPtr > > fields
The fields of an Object, in the order written.
ExprKind kind
The form.
std::string path
The JSON Pointer of the node inside its document, for refusals and diagnostics.
FunctionId function
The function of a Call.
Value literal
The constant of a Literal.
std::string name
The name a form reads: a ref's head, a query or mutation, a pref's point, a comprehension's variable,...
std::vector< ExprPtr > args
Operands: a call's arguments; if's condition, then and else; a list's items; a comprehension's source...
What the display functions and t format with: the client's locale and its formatting facts.
std::int32_t utcOffsetMinutes
The display zone's offset from UTC, in minutes.
render::NumericLocale numeric
The locale's number formatting. Holds views: the strings it names must outlive the context.
render::TranslationProvider translations
The message catalogue; empty shows every fallback.
std::string locale
The active BCP 47 locale.
UnitTable units
The units quantities convert through.
DateFormatter dates
The date formatter; empty uses ISO 8601 (2026-10-11, 2026-10-11 08:30).
An exact decimal with a unit: the id of a unit of the server's unit systems (x-unitAlternatives).
Definition value.hpp:109
Why a document, an expression or a value was refused: where, and the reason.
Definition value.hpp:38
Children laid out left to right.
Definition view.hpp:578
A document type (spec 5 §3): a kind, whether null is allowed, and a list's element type.
Definition value.hpp:311
std::string name() const
The type as a document spells it.
Definition value.hpp:347
static Type any()
The type the document cannot know.
Definition value.hpp:321
static Type of(TypeKind kind, bool optional=false)
A scalar or record type.
Definition value.hpp:327
TypeKind kind
The kind.
Definition value.hpp:313
static Type listOf(Type element, bool optional=false)
A list type.
Definition value.hpp:335
One unit of the server's unit systems, as x-unitAlternatives declares it.
std::string display
The unit's display text; empty to show its id.
math::Rational toCanonical
The exact factor from this unit to the canonical one: one of this unit is toCanonical canonical units...
std::string canonical
The canonical unit of the unit's dimension; units with the same canonical unit convert.
The keys a form may carry beside its own.
std::string_view key
The form's own key.
std::array< std::string_view, 2 > companions
The keys allowed beside it.
The signature of a library function.
std::string_view name
The name a document calls it by.
std::array< KindSet, 3 > accepts
The kinds the first, second and every further argument accept.
TypeKind result
The result's kind; Any when it depends on the arguments.
std::uint8_t maxArgs
The most arguments; 255 for any number.
std::uint8_t minArgs
The fewest arguments.
A number taken apart for arithmetic: its exact value, whether it is an int, and its unit.
std::string const * unit
The unit of a quantity; nullptr for a plain number.
bool integer
Whether it is an int.
math::Rational value
The exact value.
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
TypeKind
The kinds of a Type.
Definition value.hpp:294
@ Quantity
quantity.
Definition value.hpp:300
@ Record
record: a record whose fields the document does not declare.
Definition value.hpp:303
@ Decimal
decimal.
Definition value.hpp:299
@ Timestamp
timestamp.
Definition value.hpp:301
@ Any
Not known to the document: a query result, a reply, an element's value.
Definition value.hpp:295
glz::generic_u64 Json
The JSON tree the document layer reads and writes: glaze's generic value in its integer-preserving mo...
Definition value.hpp:35
std::vector< Value > ValueList
A list value.
Definition value.hpp:126
std::expected< Value, std::string > valueFromJson(Json const &json)
Reads a value from its JSON form, structurally.
Definition value.hpp:570
std::map< std::string, Value, std::less<> > ValueRecord
A record value: field name to value, ordered by name.
Definition value.hpp:129
std::string pointerAppend(std::string_view base, std::string_view token)
Appends one reference token to a JSON Pointer, escaping ~ and / as RFC 6901 requires.
Definition value.hpp:68
Node text(Text spec)
Static or bound text.
Definition view.hpp:1194