45[[nodiscard]]
constexpr bool isName(std::string_view
text)
noexcept {
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)) {
154 std::vector<std::pair<std::string, ExprPtr>>
fields;
166 return static_cast<KindSet>(1U <<
static_cast<unsigned>(kind));
262 return kFunctions.at(
static_cast<std::size_t
>(function));
266inline constexpr std::array<std::string_view, 5>
kEnvNames = {
"widthClass",
"locale",
"platform",
"online",
"backend"};
269inline constexpr std::array<std::string_view, 6>
kSessionNames = {
"principal",
"userName",
"displayName",
270 "userKey",
"tenant",
"authenticated"};
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();
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;
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");
294 segments.emplace_back(segment);
295 if (dot == std::string_view::npos) {
320 [[nodiscard]] std::expected<ExprPtr, Refusal>
parse(
Json const& json, std::string
const& path) {
321 return node(json, path);
326 static constexpr std::array<FormKeys, 20> kForms = {{
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, {}},
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)});
354 [[nodiscard]]
static std::string
const* text(Json
const& json) {
return std::get_if<std::string>(&json.data); }
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;
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);
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");
374 if (std::holds_alternative<Json::array_t>(json.data)) {
375 return refuse(path, R
"(a list is written {"list": [...]}, a constant list {"lit": [...]})");
377 auto const*
const object = std::get_if<Json::object_t>(&json.data);
378 if (
object ==
nullptr) {
381 return refuse(path, value.error());
383 return literal(std::move(*value), path);
385 auto made = std::make_shared<Expr>();
387 if (
auto refused = form(*made, *
object, path)) {
388 return std::unexpected(std::move(*refused));
393 [[nodiscard]] std::optional<Refusal> form(Expr& made, Json::object_t
const&
object, std::string
const& path) {
395 for (
auto const& [key, value] :
object) {
396 static_cast<void>(value);
397 for (
FormKeys const& candidate : kForms) {
398 if (candidate.key != key) {
401 if (chosen !=
nullptr) {
402 return refuse(path,
"an expression has one form; found `" + std::string{chosen->key} +
"` and `" +
409 if (chosen ==
nullptr) {
410 if (
object.empty()) {
411 return refuse(path,
"an empty object is not an expression; a constant record is {\"lit\": {}}")
414 return refuse(path,
"`" +
object.begin()->first +
"` is not an expression form of expr/1").error();
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)) {
420 "`" + key +
"` does not belong to the `" + std::string{chosen->key} +
"` form")
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: {
431 return refuse(primaryPath, value.error()).error();
433 made.literal = std::move(*value);
438 return dotted(made, primary, primaryPath);
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:
447 return controlName(made, primary, primaryPath, chosen->key);
449 case ExprKind::Session:
451 return clientName(made, primary, primaryPath, chosen->key);
453 if (chosen->key ==
"t") {
454 return translate(made,
object, path);
456 return call(made,
object, path);
458 return operands(made.args, primary, primaryPath, 3,
"`if` takes [condition, then, else]");
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);
464 case ExprKind::Filter:
466 return comprehension(made,
object, path);
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();
477 return refuse(path, segments.error()).error();
479 if (!
isName(segments->front())) {
480 return refuse(path,
"`" + segments->front() +
"` is not a name").error();
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()));
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();
493 if (dottedPath->empty()) {
498 return refuse(path, segments.error()).error();
500 made.fieldPath = std::move(*segments);
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"))
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);
528 return refuse(path,
"`" + std::string{key} +
"` does not read this name").error();
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();
540 for (std::size_t index = 0; index < array->size(); ++index) {
541 auto operand = node(array->at(index),
pointerAppend(path, index));
543 return std::move(operand.error());
545 out.push_back(std::move(*operand));
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();
556 for (
auto const& [key, value] : *
object) {
559 return std::move(field.error());
561 out.emplace_back(key, std::move(*field));
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"));
569 if (name ==
nullptr) {
570 return refuse(namePath,
"`fn` names a function as a string").error();
574 if (std::cmp_equal(index,
kFunctions.size())) {
575 return refuse(namePath,
"the function `" + *name +
"` is not in expr/1").error();
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")) {
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) {
589 }
else if (spec.maxArgs != spec.minArgs) {
590 arity +=
" to " + std::to_string(
static_cast<unsigned>(spec.maxArgs));
593 "`" + *name +
"` takes " + arity +
" arguments, not " + std::to_string(made.args.size()))
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();
607 if (fallbackText ==
nullptr) {
608 return refuse(
pointerAppend(path,
"fallback"),
"`t` needs its `fallback` text as a string").error();
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;
616 if (
auto refused = fields(record->fields, *args, record->path)) {
619 made.args.push_back(std::move(record));
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`")
636 if (body ==
nullptr) {
637 return refuse(path,
"`" + std::string{sourceKey} +
"` needs `" + std::string{bodyKey} +
"`").error();
640 auto source = node(*member(
object, sourceKey),
pointerAppend(path, sourceKey));
642 return std::move(source.error());
646 return std::move(each.error());
648 made.args.push_back(std::move(*source));
649 made.args.push_back(std::move(*each));
653 std::size_t _count = 0;
666[[nodiscard]]
inline std::expected<ExprPtr, Refusal>
parseExpression(
Json const& json, std::string
const& path =
"") {
673 std::vector<std::string_view> names;
676 names.push_back(spec.name);
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;
695 [[nodiscard]]
virtual std::expected<Type, std::string>
ref(std::string_view head,
696 std::span<std::string const> path)
const = 0;
701 [[nodiscard]]
virtual std::expected<Type, std::string>
row(std::span<std::string const> path)
const = 0;
707 [[nodiscard]]
virtual std::expected<Type, std::string>
control(
ExprKind form, std::string_view name)
const = 0;
713 [[nodiscard]]
virtual std::expected<Type, std::string>
pref(std::string_view point,
714 std::span<std::string const> path)
const = 0;
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);
734 [[nodiscard]] std::expected<Type, std::string>
row(std::span<std::string const> path)
const override {
735 static_cast<void>(path);
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);
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);
771 return Type::of(kTypes.at(
static_cast<std::size_t
>(value.
kind())));
779[[nodiscard]]
inline std::expected<Type, std::string>
typeAlong(
Type const& type, std::string_view head,
780 std::span<std::string const> path) {
787 return std::unexpected(
"`" + std::string{head} +
"` is " + type.
name() +
" and has no field `" + path.front() +
802 [[nodiscard]] std::expected<Type, Refusal>
check(
Expr const& expr) {
807 return reference(expr);
809 return wrap(expr, _resolver->row(expr.
fieldPath));
816 return wrap(expr, _resolver->control(expr.
kind, expr.
name));
818 return wrap(expr, _resolver->pref(expr.
name, expr.
fieldPath));
823 expr.
name !=
"authenticated");
829 for (
auto const& [name, field] : expr.
fields) {
830 static_cast<void>(name);
831 auto checked =
check(*field);
840 auto checked =
check(*item);
849 return comprehension(expr);
857 [[nodiscard]]
static std::expected<Type, Refusal> wrap(
Expr const& expr, std::expected<Type, std::string> type) {
859 return std::unexpected(
Refusal{.document = {}, .path = expr.
path, .reason = std::move(type.error())});
861 return std::move(*type);
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)});
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));
874 return wrap(expr, _resolver->ref(expr.name, expr.fieldPath));
877 [[nodiscard]]
static bool admitsKind(Type
const& type, KindSet accepted) {
878 return type.kind == TypeKind::Any || (accepted &
kindBit(type.kind)) != 0;
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");
889 [[nodiscard]] std::expected<Type, Refusal> choice(Expr
const& expr) {
890 auto cond = condition(*expr.args.at(0),
"the condition of `if`");
894 auto then =
check(*expr.args.at(1));
898 auto otherwise =
check(*expr.args.at(2));
902 if (then->kind == otherwise->kind && then->kind != TypeKind::List) {
903 return Type::of(then->kind, then->optional || otherwise->optional);
908 [[nodiscard]] std::expected<Type, Refusal> comprehension(Expr
const& expr) {
909 auto source =
check(*expr.args.at(0));
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());
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`");
925 if (expr.kind == ExprKind::Map) {
926 return Type::listOf(*body);
931 [[nodiscard]] std::expected<Type, Refusal> call(Expr
const& expr) {
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));
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");
945 types.push_back(std::move(*type));
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) {
953 return refuse(*fallback,
"the fallback is outside the ui/1 message subset: " + parsed.error());
957 if (spec.result != TypeKind::Any) {
960 if (expr.function == FunctionId::ConcatLists) {
961 return concatenated(types);
963 return numericResult(expr.function, types);
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()) {
974 common = *type.element;
975 }
else if (!(*common == *type.element)) {
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) {
987 if (function == FunctionId::Neg || function == FunctionId::Round) {
988 return Type::of(types.front().kind,
true);
990 Type
const& lhs = types.at(0);
991 Type
const& rhs = types.at(1);
992 if (lhs.isAny() || rhs.isAny()) {
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);
1001 return Type::of(TypeKind::Quantity,
true);
1003 if (lhs.kind == TypeKind::Int && rhs.kind == TypeKind::Int && function != FunctionId::Div) {
1004 return Type::of(TypeKind::Int,
true);
1009 NameResolver
const* _resolver;
1010 std::vector<std::pair<std::string, Type>> _locals;
1040using UnitTable = std::map<std::string, UnitInfo, std::less<>>;
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;
1079 [[nodiscard]]
virtual Value ref(std::string_view head, std::span<std::string const> path)
const = 0;
1084 [[nodiscard]]
virtual Value row(std::span<std::string const> path)
const = 0;
1096 [[nodiscard]]
virtual Value pref(std::string_view point, std::span<std::string const> path)
const = 0;
1111 virtual void report(std::string_view path, std::string_view reason)
const = 0;
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;
1128 if (!digits || index >= items->size()) {
1131 current = &items->at(index);
1134 current = current->field(segment);
1135 if (current ==
nullptr) {
1158 if (std::int64_t
const*
const whole = value.
asInt()) {
1162 return Number{.value = decimal->value, .integer =
false, .unit =
nullptr};
1165 return Number{.value = quantity->value, .integer =
false, .unit = &quantity->unit};
1167 return std::nullopt;
1176 if (unit !=
nullptr) {
1177 return Quantity{.value = value, .unit = *unit};
1193 std::string_view target,
UnitTable const& units) {
1194 if (from == target) {
1197 auto factor = [&units](std::string_view unit) {
1198 auto const found = units.find(unit);
1199 if (found == units.end()) {
1202 return std::pair{found->second.canonical, found->second.toCanonical};
1204 auto const [fromBase, fromFactor] = factor(from);
1205 auto const [toBase, toFactor] = factor(target);
1206 if (fromBase != toBase) {
1207 return std::nullopt;
1215 return std::nullopt;
1219 return std::nullopt;
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) {
1229 for (; denominator % 5U == 0; denominator /= 5U) {
1232 std::uint32_t
const extra = denominator == 1 ? std::max(twos, fives) : 0U;
1233 converted->decimalPlaces =
1243 auto const found = units.find(unit);
1244 return found == units.end() || found->second.display.empty() ? unit : found->second.display;
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)) {
1257 return oneOf(kThree, currency) ? 3U : 2U;
1272 switch (expr.
kind) {
1276 for (
auto const& [name, value] : std::views::reverse(_locals)) {
1277 if (name == expr.
name) {
1290 return _env->control(expr.
kind, expr.
name);
1296 return _env->client(expr.
kind, expr.
name);
1298 return choice(expr);
1301 for (
auto const& [name, field] : expr.
fields) {
1302 record.insert_or_assign(name,
eval(*field));
1308 items.reserve(expr.
args.size());
1310 items.push_back(
eval(*item));
1316 return comprehension(expr);
1324 Value mismatch(
Expr const& expr, std::string_view reason) {
1329 Value choice(Expr
const& expr) {
1330 Value
const cond =
eval(*expr.args.at(0));
1331 if (cond.isNull()) {
1334 bool const*
const flag =
cond.asBool();
1335 if (flag ==
nullptr) {
1336 return mismatch(expr,
"the condition of `if` is not a bool");
1338 return eval(*expr.args.at(*flag ? 1 : 2));
1341 Value comprehension(Expr
const& expr) {
1342 Value
const source =
eval(*expr.args.at(0));
1343 if (source.isNull()) {
1346 ValueList const*
const items = source.asList();
1347 if (items ==
nullptr) {
1348 return mismatch(expr,
"a comprehension runs over a list");
1351 for (Value
const& item : *items) {
1352 _locals.emplace_back(expr.name, item);
1353 Value body =
eval(*expr.args.at(1));
1355 if (expr.kind == ExprKind::Map) {
1356 out.push_back(std::move(body));
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");
1363 if (keep !=
nullptr && *keep) {
1364 out.push_back(item);
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) {
1381 if (!value.isNull()) {
1382 _env->
report(arg->path,
"a logical operand is not a bool");
1393 Value arithmetic(Expr
const& expr, Value
const& lhsValue, Value
const& rhsValue) {
1394 auto const lhs =
numeric(lhsValue);
1395 auto const rhs =
numeric(rhsValue);
1397 return mismatch(expr,
"an arithmetic operand is not a number");
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");
1407 auto converted =
convertUnit(rhs->value, *rhs->unit, *lhs->unit, units);
1409 return mismatch(expr,
"the units `" + *lhs->unit +
"` and `" + *rhs->unit +
"` do not convert");
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);
1416 return mismatch(expr,
"a quantity and a plain number do not combine this way");
1418 unit = lhs->unit !=
nullptr ? lhs->unit : rhs->unit;
1420 if (function == FunctionId::Div && right.isZero()) {
1421 return mismatch(expr,
"division by zero");
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);
1431 result = math::checkedDiv(lhs->value, right);
1434 return mismatch(expr,
"the result overflows");
1437 return makeNumber(*result, lhs->integer && rhs->integer, unit);
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;
1448 math::Rational right = rhsNumber->value;
1449 if (lhsNumber->unit !=
nullptr) {
1452 return std::nullopt;
1456 return lhsNumber->value <=> right;
1458 if (time::DateTime
const*
const lhsTime = lhs.asTimestamp()) {
1459 if (time::DateTime
const*
const rhsTime = rhs.asTimestamp()) {
1460 return *lhsTime <=> *rhsTime;
1462 return std::nullopt;
1464 if (std::string
const*
const lhsText = lhs.asString()) {
1465 if (std::string
const*
const rhsText = rhs.asString()) {
1466 return *lhsText <=> *rhsText;
1469 return std::nullopt;
1473 bool equal(Value
const& lhs, Value
const& rhs) {
1475 auto const ordering = order(lhs, rhs);
1476 return ordering.has_value() && *ordering == std::strong_ordering::equal;
1478 if (lhs.kind() != rhs.kind()) {
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);
1486 if (ValueRecord
const*
const lhsFields = lhs.asRecord()) {
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);
1496 bool sameElement(Value
const& left, Value
const& right) {
1497 if (left.isNull() || right.isNull()) {
1498 return left.isNull() && right.isNull();
1500 return equal(left, right);
1504 std::optional<std::string> display(Value
const& value) {
1505 FormatContext
const& context = _env->
formatting();
1506 if (std::string
const*
const text = value.asString()) {
1509 if (
bool const*
const flag = value.asBool()) {
1510 return std::string{*flag ?
"true" :
"false"};
1512 if (Quantity
const*
const quantity = value.asQuantity()) {
1513 return formatNumber(value, context.numeric).value_or(std::string{}) +
" " +
1516 if (
auto number =
formatNumber(value, context.numeric)) {
1519 if (time::DateTime
const*
const instant = value.asTimestamp()) {
1520 return dateText(*instant,
"medium",
true);
1522 return std::nullopt;
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;
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);
1534 std::string
const iso = local.toIso8601();
1536 return iso.substr(0, 10);
1538 return iso.substr(0, 10) +
" " + iso.substr(11, style ==
"long" ? 8 : 5);
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));
1547 return mismatch(expr,
"`format` cannot show argument " + std::to_string(index + 1));
1549 shown.push_back(std::move(*text));
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) {
1560 return mismatch(expr,
"`format`'s template has an unmatched `}`");
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;
1576 if (!valid || index >= shown.size()) {
1577 return mismatch(expr,
"`format`'s template has a placeholder with no argument");
1579 out += shown.at(index);
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();
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");
1597 std::optional<MessagePattern> message;
1598 if (context.translations) {
1599 if (
auto const pattern = context.translations(key, context.locale)) {
1602 message = std::move(*parsed);
1604 _env->
report(expr.path,
"the catalogue's message `" + key +
1605 "` is outside the ui/1 subset: " + parsed.error());
1612 _env->
report(expr.path,
"the fallback is outside the ui/1 subset: " + parsed.error());
1615 message = std::move(*parsed);
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);
1622 return std::move(formatted.text);
1625 Value listFunction(Expr
const& expr, ValueList
const& items) {
1627 if (function == FunctionId::Count) {
1628 return static_cast<std::int64_t
>(items.size());
1630 if (function == FunctionId::First) {
1631 if (items.empty()) {
1634 return items.front();
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) {
1644 if (flag ==
nullptr && !item.isNull()) {
1645 return mismatch(expr,
"an element is not a bool");
1647 sawNull = sawNull || item.isNull();
1654 if (function == FunctionId::Min || function == FunctionId::Max) {
1655 return extreme(expr, items);
1657 return aggregate(expr, items);
1660 Value extreme(Expr
const& expr, ValueList
const& items) {
1661 if (items.empty()) {
1664 Value
const* best = &items.front();
1665 for (Value
const& item : items) {
1666 if (item.isNull()) {
1669 auto const ordering = order(item, *best);
1671 return mismatch(expr,
"the elements are not of one comparable kind");
1673 bool const better = expr.function == FunctionId::Min ? std::is_lt(*ordering) : std::is_gt(*ordering);
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}};
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()) {
1695 auto const number =
numeric(item);
1697 return mismatch(expr,
"an element is not a number");
1700 unit = number->unit;
1701 }
else if ((unit ==
nullptr) != (number->unit ==
nullptr)) {
1702 return mismatch(expr,
"quantities and plain numbers do not sum together");
1704 math::Rational value = number->value;
1705 if (unit !=
nullptr) {
1708 return mismatch(expr,
"the elements' units do not convert");
1712 places = std::max(places, value.decimalPlaces.value);
1713 integer = integer && number->integer;
1714 auto next = math::checkedAdd(total, value);
1716 return mismatch(expr,
"the sum overflows");
1723 auto const quotient =
1724 math::checkedDiv(total, math::Rational{
static_cast<std::int64_t
>(items.size()), math::DecimalPlaces{0}});
1726 math::Rational
const rounded = math::roundToDecimalPlaces(
1727 quotient.value_or(total), math::DecimalPlaces{places}, math::RoundingMode::HalfEven);
1731 Value call(Expr
const& expr) {
1733 if (function == FunctionId::And || function == FunctionId::Or) {
1734 return kleene(expr, function == FunctionId::Or);
1736 if (function == FunctionId::Coalesce) {
1737 for (ExprPtr
const& arg : expr.args) {
1738 Value value =
eval(*arg);
1739 if (!value.isNull()) {
1745 if (function == FunctionId::Engaged) {
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()) {
1756 return strict(expr, args);
1764#if defined(__GNUC__) && !defined(__clang__)
1765#pragma GCC diagnostic push
1766#pragma GCC diagnostic ignored "-Wstrict-overflow"
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");
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));
1788 return mismatch(expr,
"the operands are not of one comparable kind");
1790 if (expr.function == FunctionId::Lt) {
1791 return std::is_lt(*ordering);
1793 if (expr.function == FunctionId::Le) {
1794 return std::is_lteq(*ordering);
1796 if (expr.function == FunctionId::Gt) {
1797 return std::is_gt(*ordering);
1799 return std::is_gteq(*ordering);
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());
1809 return mismatch(expr,
"`neg` takes a number");
1811 auto negated = math::checkedSub(math::Rational{0, number->value.decimalPlaces}, number->value);
1813 return mismatch(expr,
"the result overflows");
1815 return makeNumber(*negated, number->integer, number->unit);
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");
1824 if (number->integer) {
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) {
1832 if (!math::checkedMul(number->value, math::Rational{scale, math::DecimalPlaces{0}})) {
1833 return mismatch(expr,
"the result overflows");
1835 return makeNumber(math::roundToDecimalPlaces(number->value, math::DecimalPlaces{placesValue},
1836 math::RoundingMode::HalfEven),
1837 false, number->unit);
1839 case FunctionId::Concat: {
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");
1850 case FunctionId::Format:
1851 if (args.front().asString() ==
nullptr) {
1852 return mismatch(expr,
"`format`'s template is a string");
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");
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{};
1865 return text->substr(first,
text->find_last_not_of(kSpace) - first + 1);
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");
1872 std::int64_t points = 0;
1873 for (
char const chr : *text) {
1874 points += (
static_cast<unsigned char>(chr) & 0xC0U) == 0x80U ? 0 : 1;
1878 case FunctionId::IsEmpty: {
1879 if (std::string
const*
const text = args.front().asString()) {
1880 return text->empty();
1882 if (ValueList
const*
const items = args.front().asList()) {
1883 return items->empty();
1885 return mismatch(expr,
"`isEmpty` takes a string or a list");
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");
1893 return text->contains(*part);
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");
1907 return listFunction(expr, *items);
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");
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)};
1921 return expr.function == FunctionId::Includes ? Value{
false} : Value{};
1923 case FunctionId::Number: {
1926 return mismatch(expr,
"`number` takes a number");
1928 return std::move(*text);
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");
1937 return render::formatCanonicalNumber(decimalText(number->value,
static_cast<std::uint32_t
>(*places)),
1940 case FunctionId::QuantityText: {
1942 if (quantity ==
nullptr) {
1943 return mismatch(expr,
"`quantity` takes a quantity");
1946 if (args.size() > 1) {
1947 std::string
const*
const unit = args.at(1).asString();
1948 auto converted = unit ==
nullptr
1952 return mismatch(expr,
"the quantity does not convert to that unit");
1954 shown =
Quantity{.value = *converted, .unit = *unit};
1956 return formatNumber(shown, context.numeric).value_or(std::string{}) +
" " +
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");
1965 return render::formatCanonicalNumber(decimalText(number->value,
currencyDigits(*currency)),
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()) {
1974 }
else if (std::string
const*
const text = args.at(0).asString()) {
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);
1984 return mismatch(expr,
"the date functions take a timestamp and a style");
1986 return std::move(*text);
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");
1997 declared =
static_cast<std::uint32_t
>(*places);
1999 if (text ==
nullptr) {
2000 return mismatch(expr,
"`parseDecimal` takes text and places from 0 to 18");
2003 if (
auto parsed = render::parseDecimal(*text, context.numeric, declared)) {
2008 case FunctionId::ConcatLists: {
2010 for (Value
const& arg : args) {
2011 ValueList const*
const items = arg.asList();
2012 if (items ==
nullptr) {
2013 return mismatch(expr,
"`concatLists` takes lists");
2015 out.insert(out.end(), items->begin(), items->end());
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");
2023 return translate(expr, args);
2024 case FunctionId::And:
2025 case FunctionId::Or:
2026 case FunctionId::Coalesce:
2027 case FunctionId::Engaged:
2032#if defined(__GNUC__) && !defined(__clang__)
2033#pragma GCC diagnostic pop
2036 Environment
const* _env;
2037 std::vector<std::pair<std::string, Value>> _locals;
2093 [[nodiscard]]
Value ref(std::string_view head, std::span<std::string const> path)
const override {
2094 return lookup(
names, head, path);
2114 return lookup(
errors, name, {});
2126 [[nodiscard]]
Value pref(std::string_view point, std::span<std::string const> path)
const override {
2127 return lookup(
prefs, point, path);
2148 void report(std::string_view path, std::string_view reason)
const override {
2150 if (seen.path == path) {
2154 diagnostics.push_back(
Refusal{.document = {}, .path = std::string{path}, .reason = std::string{reason}});
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);
Node text(TextOptions const &options)
A text node.
Element element(std::string name)
An element of the view.
Handle< T > cond(Handle< bool > const &condition, Handle< T > const &then, Handle< T > const &otherwise)
{"if": [c, a, b]}: a choice between two values.
Handle< std::string > quantity(AnyHandle const &value)
quantity(x): a quantity for display in its own unit.
Command close(Element const &dialog)
close: closes a dialog or drawer.
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.
Quantity const * asQuantity() const noexcept
The quantity, if held.
Decimal const * asDecimal() const noexcept
The decimal, if held.
bool isNull() const noexcept
Whether the value is null.
ValueKind kind() const noexcept
The kind of value held.
std::int64_t const * asInt() const noexcept
The int, if held.
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, [...], [...]]}
@ Host
{"host": "name", "args": e?}
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}.
@ 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.
@ 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.
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.
FormattedMessage formatMessage(MessagePattern const &message, ValueRecord const &args, MessageLocale const &locale)
Formats a parsed message with named arguments.
std::expected< MessagePattern, std::string > parseMessage(std::string_view text)
Parses a message in the ui/1 subset of MessageFormat 2.
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...
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.
Strong type for a decimal-precision count.
std::uint32_t value
Raw digit count. Rational's invariant keeps it in [0, kMaxDecimalPlaces].
Strong type for a Rational denominator.
Strong type for a Rational numerator.
Exact rational number with a runtime decimal-precision tag.
constexpr bool isInteger() const noexcept
Whether the value is an integer.
DecimalPlaces decimalPlaces
Decimal-precision tag. Invariant: 0 <= value <= kMaxDecimalPlaces.
std::int64_t numerator
Signed numerator. Carries the sign of the rational value.
The locale facts both control-edge conversions need, in one designated-initialisable aggregate.
A UTC instant with millisecond precision.
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/....
An exact decimal: a math::Rational whose DecimalPlaces are the value's declared places.
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.
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).
Why a document, an expression or a value was refused: where, and the reason.
Children laid out left to right.
A document type (spec 5 §3): a kind, whether null is allowed, and a list's element type.
std::string name() const
The type as a document spells it.
static Type any()
The type the document cannot know.
static Type of(TypeKind kind, bool optional=false)
A scalar or record type.
static Type listOf(Type element, bool optional=false)
A list type.
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 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.
@ Record
record: a record whose fields the document does not declare.
@ Any
Not known to the document: a query result, a reply, an element's value.
glz::generic_u64 Json
The JSON tree the document layer reads and writes: glaze's generic value in its integer-preserving mo...
std::vector< Value > ValueList
A list value.
std::expected< Value, std::string > valueFromJson(Json const &json)
Reads a value from its JSON form, structurally.
std::map< std::string, Value, std::less<> > ValueRecord
A record value: field name to value, ordered by name.
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.
Node text(Text spec)
Static or bound text.