commands: the aggregation expression evaluator

The whole corpus is green: 46 pass, 0 fail, byte-identical to mongod 8.3.7 on
every case including all 27 expressions and the compound `_id` that was the
last accumulator failure.

Expressions are *compiled once per pipeline and evaluated per document*, and
that split is the point rather than an optimisation: it keeps the property M2's
refusals bought, which is that a pipeline that cannot be answered is refused
before a single document is read instead of half way through with part of the
work already reported. `Expr` is the compiled tree, `compile_expr` reports,
`eval_expr` cannot.

Nineteen operators: `$literal`, the five arithmetic ones, seven comparisons,
`$and`/`$or`/`$not`, `$cond` in both its forms, `$ifNull` and `$switch`. Plus
the two shapes that are not operators at all -- a compound document, which is
what a `$group` `_id` usually is, and an array.

Everything the corpus recorded, and none of it guessable:

  - absent and a present null are *different* internally, because `$ifNull`
    treats them alike and `$push` does not. Hence `?bson.Value` throughout,
    where the obvious shortcut is to fold absent into `.null` at the boundary
    and lose the distinction for good.
  - arithmetic over absent or null is `null` -- not an error, not zero -- and
    over a string is an error, 7157723.
  - `$divide` by zero is 4848401, `$switch` with no branch and no default is
    40069, and both are failures only a document can produce, so `EvalError`
    exists and `report_eval_error` maps it.
  - two operators in one expression document is 15983 and *not* `$group`'s
    40238: mongod distinguishes an expression from an accumulator there.
  - truthiness is MongoDB's, so `-5` is true and `0.0` is false.
  - `$mod` follows the dividend's sign, so -5 mod 4 is -1.
  - `$not` takes a bare argument as readily as a one-element array.

`compile_expr` and `compile_operator` call each other, so their error set is
written out rather than inferred -- Zig cannot infer a cycle, and the failure
mode is a "dependency loop" message that says nothing about expressions.

Three cases left the Tier 0 refusal test, because a compound `_id`, `$literal`
and a `$multiply` argument all work now. What is refused should be what is
missing, so an unknown operator and a wrong operand count took their place.

191/191 unit tests in ReleaseFast and ReleaseSafe, 83/83 fuzz, e2e 49, e2e2
concurrent, e2e3 16, e2e4 17, e2e6 72, e2e7 86, crud corpus unchanged at
201/90/196.
This commit is contained in:
A.Shakhmatov
2026-08-09 22:26:30 +03:00
parent 1c72aa8938
commit 37cfa863ee
2 changed files with 447 additions and 74 deletions

View File

@@ -107,6 +107,15 @@ pub const ErrorCode = enum(i32) {
location_project_mixed = 31254,
location_project_unknown_expression = 31325,
location_write_stage_not_last = 40601,
// Expression codes, measured with the corpus recorder against mongod
// 8.3.7. Note 15983 rather than $group's 40238 for "two operators in one
// document": mongod distinguishes an expression from an accumulator there,
// and a client switching on the code would notice if we did not.
location_two_expression_operators = 15983,
location_wrong_operand_count = 16020,
location_switch_no_default = 40069,
location_divide_by_zero = 4848401,
location_non_numeric_arithmetic = 7157723,
location_unknown_group_operator = 15952,
location_group_needs_id = 15955,
location_accumulator_not_object = 40234,
@@ -2699,56 +2708,427 @@ fn stream_path(
};
}
/// The whole vocabulary this engine can evaluate: a field path, or a constant.
/// A compiled aggregation expression.
///
/// There is no expression evaluator (PLAN amendment A6 puts one in M2.5), and
/// until there is, anything else has to be *refused*. It used to fall through
/// to a zero: `{$avg: "$x"}` answered `0` with `ok: 1`, and so did `$max` and
/// `$push`, and a compound `_id` collapsed every document into one group keyed
/// by the unevaluated expression. A wrong number that reports success is worse
/// than an error, because nobody files it.
const GroupExpr = union(enum) {
/// Compiled once per pipeline and evaluated per document, and that split is
/// what preserves the property M2's refusals bought: a pipeline that cannot be
/// answered is refused before a single document is read, rather than half way
/// through with part of the work already reported.
const Expr = union(enum) {
/// A `$`-prefixed string, holding the path without its sigil.
path: []const u8,
constant: bson.Value,
/// `{a: <expr>, b: <expr>}`: a document whose values are expressions. This
/// is what a compound `$group` `_id` is, and it used to collapse every
/// document into one group keyed by the unevaluated document.
fields: []const Field,
array: []const Expr,
op: Operator,
const Field = struct { key: []const u8, value: Expr };
};
/// Classify `v`, or answer the client and return null.
///
/// `what` names the position for the message -- mongod's own messages name the
/// field, and a refusal that does not say what it refused is only half an
/// improvement on a silent zero.
fn classify_expr(reply: *wire.Reply, v: bson.Value, what: []const u8) !?GroupExpr {
const detail = switch (v) {
.string => |str| {
// "$x" is a path; "x" is the string itself. This is the only place
// that distinction is made now, where it used to be open-coded at
// each use and disagree between them.
if (str.len > 0 and str[0] == '$') return GroupExpr{ .path = str[1..] };
return GroupExpr{ .constant = v };
},
// An operator document is the one shape mongod also refuses, and 168 is
// the code it uses, naming the operator. A compound expression
// (`{a: "$x"}`) mongod would happily evaluate -- so the code is the
// same and the message says what is actually true here instead.
.doc => |d| if (d.len > 0 and d[0].key.len > 0 and d[0].key[0] == '$')
try std.fmt.allocPrint(reply.arena_alloc(), "Unrecognized expression '{s}'", .{d[0].key})
else
try std.fmt.allocPrint(
reply.arena_alloc(),
"{s} must be a field path or a constant: this server evaluates no expressions",
.{what},
),
.array => try std.fmt.allocPrint(
reply.arena_alloc(),
"{s} must be a field path or a constant: this server evaluates no expressions",
.{what},
),
else => return GroupExpr{ .constant = v },
const OpKind = enum {
literal,
add,
subtract,
multiply,
divide,
mod,
eq,
ne,
lt,
lte,
gt,
gte,
cmp,
all_of,
any_of,
not,
cond,
if_null,
switch_,
};
try reply.put_error(@intFromEnum(ErrorCode.invalid_pipeline_operator), "InvalidPipelineOperator", detail);
const Operator = struct {
kind: OpKind,
args: []const Expr,
/// `$switch` only, and only when it had one. Kept apart from `args`,
/// because "no default" and "a default of null" are different answers --
/// the first is an error, measured as 40069.
fallback: ?*const Expr = null,
};
fn op_kind(name: []const u8) ?OpKind {
const table = .{
.{ "$literal", OpKind.literal },
.{ "$add", OpKind.add },
.{ "$subtract", OpKind.subtract },
.{ "$multiply", OpKind.multiply },
.{ "$divide", OpKind.divide },
.{ "$mod", OpKind.mod },
.{ "$eq", OpKind.eq },
.{ "$ne", OpKind.ne },
.{ "$lt", OpKind.lt },
.{ "$lte", OpKind.lte },
.{ "$gt", OpKind.gt },
.{ "$gte", OpKind.gte },
.{ "$cmp", OpKind.cmp },
.{ "$and", OpKind.all_of },
.{ "$or", OpKind.any_of },
.{ "$not", OpKind.not },
.{ "$cond", OpKind.cond },
.{ "$ifNull", OpKind.if_null },
.{ "$switch", OpKind.switch_ },
};
inline for (table) |e| {
if (std.mem.eql(u8, name, e[0])) return e[1];
}
return null;
}
/// Operands an operator takes, or null when it is variadic. mongod checks this
/// at parse time and answers 16020, which is why this is a compile-time table
/// rather than a runtime length test.
fn op_arity(kind: OpKind) ?usize {
return switch (kind) {
.literal, .not => 1,
.subtract, .divide, .mod, .cmp => 2,
.cond => 3,
else => null,
};
}
/// Compilation allocates and reports; it reads no documents, so nothing else
/// can go wrong. Written out rather than inferred because `compile_expr` and
/// `compile_operator` call each other, and Zig cannot infer a cycle.
const CompileError = std.mem.Allocator.Error;
/// Compile `v` into an expression, or answer the client and return null.
///
/// `what` names the position, because a refusal that does not say what it
/// refused is only half an improvement on a silent zero.
fn compile_expr(reply: *wire.Reply, arena: std.mem.Allocator, v: bson.Value, what: []const u8) CompileError!?Expr {
switch (v) {
.string => |str| {
// "$x" is a path; "x" is the string itself. The one place that
// distinction is made, where it used to be open-coded per use.
if (str.len > 0 and str[0] == '$') return Expr{ .path = str[1..] };
return Expr{ .constant = v };
},
.array => |items| {
const out = try arena.alloc(Expr, items.len);
for (items, 0..) |item, i| {
out[i] = (try compile_expr(reply, arena, item, what)) orelse return null;
}
return Expr{ .array = out };
},
.doc => |d| {
const leads_with_op = d.len > 0 and d[0].key.len > 0 and d[0].key[0] == '$';
if (!leads_with_op) {
// A compound expression: every value is itself an expression.
const out = try arena.alloc(Expr.Field, d.len);
for (d, 0..) |p, i| {
const sub = (try compile_expr(reply, arena, p.value, what)) orelse return null;
out[i] = .{ .key = p.key, .value = sub };
}
return Expr{ .fields = out };
}
if (d.len != 1) {
// 15983, and deliberately not `$group`'s 40238: mongod uses a
// different code for an expression than for an accumulator, and
// a client that switches on the code would notice.
const detail = try std.fmt.allocPrint(
reply.arena_alloc(),
"an expression specification must contain exactly one field, the name of the expression",
.{},
);
try reply.put_error(@intFromEnum(ErrorCode.location_two_expression_operators), "Location15983", detail);
return null;
}
const kind = op_kind(d[0].key) orelse {
const detail = try std.fmt.allocPrint(
reply.arena_alloc(),
"Unrecognized expression '{s}'",
.{d[0].key},
);
try reply.put_error(@intFromEnum(ErrorCode.invalid_pipeline_operator), "InvalidPipelineOperator", detail);
return null;
};
return compile_operator(reply, arena, kind, d[0].key, d[0].value, what);
},
else => return Expr{ .constant = v },
}
}
fn compile_operator(
reply: *wire.Reply,
arena: std.mem.Allocator,
kind: OpKind,
name: []const u8,
spec: bson.Value,
what: []const u8,
) CompileError!?Expr {
// `$literal` is the one operator whose argument is *not* an expression --
// that is the whole of what it does.
if (kind == .literal) {
const one = try arena.alloc(Expr, 1);
one[0] = .{ .constant = spec };
return Expr{ .op = .{ .kind = kind, .args = one } };
}
if (kind == .switch_) return compile_switch(reply, arena, spec, what);
// `$cond` has a document form as well as its three-element array.
if (kind == .cond and spec == .doc and spec.doc.len > 0 and spec.doc[0].key[0] != '$') {
const args = try arena.alloc(Expr, 3);
const names = [_][]const u8{ "if", "then", "else" };
for (names, 0..) |field, i| {
const sub = bson.get_pair(spec.doc, field) orelse {
const detail = try std.fmt.allocPrint(reply.arena_alloc(), "Missing '{s}' parameter to $cond", .{field});
try reply.put_error(@intFromEnum(ErrorCode.location_wrong_operand_count), "Location16020", detail);
return null;
};
args[i] = (try compile_expr(reply, arena, sub, what)) orelse return null;
}
return Expr{ .op = .{ .kind = kind, .args = args } };
}
// Everything else takes its operands as an array, or as a bare value when
// there is one of them -- `{$not: "$f"}` is as legal as `{$not: ["$f"]}`.
var args: []const Expr = undefined;
if (spec == .array) {
const out = try arena.alloc(Expr, spec.array.len);
for (spec.array, 0..) |item, i| {
out[i] = (try compile_expr(reply, arena, item, what)) orelse return null;
}
args = out;
} else {
const one = try arena.alloc(Expr, 1);
one[0] = (try compile_expr(reply, arena, spec, what)) orelse return null;
args = one;
}
if (op_arity(kind)) |want| {
if (args.len != want) {
const detail = try std.fmt.allocPrint(
reply.arena_alloc(),
"Expression {s} takes exactly {d} arguments. {d} were passed in.",
.{ name, want, args.len },
);
try reply.put_error(@intFromEnum(ErrorCode.location_wrong_operand_count), "Location16020", detail);
return null;
}
}
return Expr{ .op = .{ .kind = kind, .args = args } };
}
/// `{$switch: {branches: [{case, then}, ...], default: <expr>}}`. The branches
/// flatten into `args` as case, then, case, then; the default stays apart, so
/// that having none is distinguishable from having one that is null.
fn compile_switch(reply: *wire.Reply, arena: std.mem.Allocator, spec: bson.Value, what: []const u8) CompileError!?Expr {
const d = doc_arg(spec) orelse {
try bad_value(reply, "$switch requires an object");
return null;
};
const branches = switch (bson.get_pair(d, "branches") orelse bson.Value.null) {
.array => |a| a,
else => {
try bad_value(reply, "$switch requires an array of branches");
return null;
},
};
const args = try arena.alloc(Expr, branches.len * 2);
for (branches, 0..) |b, i| {
const bd = doc_arg(b) orelse {
try bad_value(reply, "$switch branches must be objects");
return null;
};
const case_v = bson.get_pair(bd, "case") orelse {
try bad_value(reply, "$switch branch requires a case");
return null;
};
const then_v = bson.get_pair(bd, "then") orelse {
try bad_value(reply, "$switch branch requires a then");
return null;
};
args[i * 2] = (try compile_expr(reply, arena, case_v, what)) orelse return null;
args[i * 2 + 1] = (try compile_expr(reply, arena, then_v, what)) orelse return null;
}
var fallback: ?*const Expr = null;
if (bson.get_pair(d, "default")) |dv| {
const boxed = try arena.create(Expr);
boxed.* = (try compile_expr(reply, arena, dv, what)) orelse return null;
fallback = boxed;
}
return Expr{ .op = .{ .kind = .switch_, .args = args, .fallback = fallback } };
}
/// A failure only a document can produce, so it cannot be caught at compile
/// time. Both codes measured against mongod.
const EvalError = error{ DivideByZero, SwitchNoDefault, NonNumericArithmetic } ||
std.mem.Allocator.Error || error{ EndOfStream, Overflow, InvalidBson };
/// Turn a failure only a document could produce into the reply mongod gives.
fn report_eval_error(reply: *wire.Reply, err: EvalError) !void {
switch (err) {
error.DivideByZero => try reply.put_error(
@intFromEnum(ErrorCode.location_divide_by_zero),
"Location4848401",
"can't $divide by zero",
),
error.SwitchNoDefault => try reply.put_error(
@intFromEnum(ErrorCode.location_switch_no_default),
"Location40069",
"$switch could not find a matching branch for an input, and no default was specified.",
),
error.NonNumericArithmetic => try reply.put_error(
@intFromEnum(ErrorCode.location_non_numeric_arithmetic),
"Location7157723",
"only numbers are allowed in an $add or $subtract expression",
),
else => return err,
}
}
/// Where an expression reads its document from.
const EvalCtx = struct {
arena: std.mem.Allocator,
coll: *const Collection,
src: Stream,
i: usize,
};
/// Evaluate `e` against one document. `null` is *absent*, which several
/// operators distinguish from a present BSON null -- `$ifNull` treats them
/// alike, `$push` does not.
fn eval_expr(ec: EvalCtx, e: Expr) EvalError!?bson.Value {
switch (e) {
.path => |path| return stream_path(ec.arena, ec.coll, ec.src, ec.i, path),
.constant => |v| return v,
.fields => |fs| {
const pairs = try ec.arena.alloc(bson.Pair, fs.len);
var n: usize = 0;
for (fs) |f| {
// A field whose expression resolves to nothing is left out,
// which is how a compound `_id` drops a missing path.
const v = (try eval_expr(ec, f.value)) orelse continue;
pairs[n] = .{ .key = f.key, .value = v };
n += 1;
}
return bson.Value{ .doc = pairs[0..n] };
},
.array => |items| {
const out = try ec.arena.alloc(bson.Value, items.len);
for (items, 0..) |item, i| out[i] = (try eval_expr(ec, item)) orelse .null;
return bson.Value{ .array = out };
},
.op => |o| return eval_operator(ec, o),
}
}
fn eval_operator(ec: EvalCtx, o: Operator) EvalError!?bson.Value {
switch (o.kind) {
.literal => return o.args[0].constant,
.add, .multiply, .subtract, .divide, .mod => return eval_arithmetic(ec, o),
.eq, .ne, .lt, .lte, .gt, .gte, .cmp => {
// Absent compares as null, which is what makes
// `{$eq: ["$missing", null]}` true.
const a = (try eval_expr(ec, o.args[0])) orelse .null;
const b = (try eval_expr(ec, o.args[1])) orelse .null;
const ord = bson.compare(a, b);
if (o.kind == .cmp) return bson.Value{ .int32 = switch (ord) {
.lt => -1,
.eq => 0,
.gt => 1,
} };
return bson.Value{ .bool = switch (o.kind) {
.eq => ord == .eq,
.ne => ord != .eq,
.lt => ord == .lt,
.lte => ord != .gt,
.gt => ord == .gt,
.gte => ord != .lt,
else => unreachable,
} };
},
.all_of, .any_of => {
const want = o.kind == .any_of;
for (o.args) |arg| {
if (expr_truthy(try eval_expr(ec, arg)) == want) return bson.Value{ .bool = want };
}
return bson.Value{ .bool = !want };
},
.not => return bson.Value{ .bool = !expr_truthy(try eval_expr(ec, o.args[0])) },
.cond => {
const take: usize = if (expr_truthy(try eval_expr(ec, o.args[0]))) 1 else 2;
return eval_expr(ec, o.args[take]);
},
.if_null => {
// Every operand but the last is a candidate; the last is the
// fallback, and is returned whether or not it is itself null.
for (o.args[0 .. o.args.len - 1]) |arg| {
const v = try eval_expr(ec, arg);
if (v) |present| {
if (present != .null) return present;
}
}
return eval_expr(ec, o.args[o.args.len - 1]);
},
.switch_ => {
var i: usize = 0;
while (i < o.args.len) : (i += 2) {
if (expr_truthy(try eval_expr(ec, o.args[i]))) return eval_expr(ec, o.args[i + 1]);
}
const fallback = o.fallback orelse return error.SwitchNoDefault;
return eval_expr(ec, fallback.*);
},
}
}
/// MongoDB's truthiness: false, null, absent and any numeric zero are false,
/// and everything else -- including a negative number and an empty string --
/// is true.
fn expr_truthy(v: ?bson.Value) bool {
return switch (v orelse return false) {
.bool => |b| b,
.null => false,
.int32 => |n| n != 0,
.int64 => |n| n != 0,
.double => |n| n != 0,
else => true,
};
}
fn eval_arithmetic(ec: EvalCtx, o: Operator) EvalError!?bson.Value {
var acc: f64 = if (o.kind == .multiply) 1 else 0;
for (o.args, 0..) |arg, i| {
const v = try eval_expr(ec, arg);
// Absent or null makes the whole expression null -- not an error, and
// not a zero. Measured: `{$add: ["$missing", 1]}` is null.
const present = v orelse return bson.Value.null;
const x: f64 = switch (present) {
.int32 => |n| @floatFromInt(n),
.int64 => |n| @floatFromInt(n),
.double => |n| n,
.null => return bson.Value.null,
else => return error.NonNumericArithmetic,
};
switch (o.kind) {
.add => acc += x,
.multiply => acc *= x,
.subtract => acc = if (i == 0) x else acc - x,
.divide, .mod => {
if (i == 0) {
acc = x;
} else {
if (x == 0) return error.DivideByZero;
acc = if (o.kind == .divide) acc / x else @rem(acc, x);
}
},
else => unreachable,
}
}
return numeric_value(acc);
}
/// A running total as MongoDB reports it: an integral value inside int32 range
/// comes back an int32, anything else a double. The count fast path in
/// `cmd_aggregate` mirrors this exactly, and a divergence between them would
@@ -2784,7 +3164,7 @@ fn acc_kind(name: []const u8) ?AccKind {
const Accumulator = struct {
key: []const u8,
kind: AccKind,
arg: GroupExpr,
arg: Expr,
};
/// What one accumulator has seen of one group so far.
@@ -2824,7 +3204,7 @@ fn run_group(
);
return null;
};
const id_class = (try classify_expr(reply, id_expr, "the _id of a $group")) orelse return null;
const id_class = (try compile_expr(reply, arena, id_expr, "the _id of a $group")) orelse return null;
// Every accumulator is validated before a single document is read, so a
// pipeline that cannot be answered is refused rather than half-answered.
@@ -2868,11 +3248,11 @@ fn run_group(
// `$count` takes `{}` and nothing else, so it never reaches the
// expression classifier -- an empty document is exactly what that
// refuses.
const arg: GroupExpr = if (kind == .count)
const arg: Expr = if (kind == .count)
.{ .constant = .null }
else blk: {
const what = try std.fmt.allocPrint(arena, "the argument of '{s}'", .{p.key});
break :blk (try classify_expr(reply, spec[0].value, what)) orelse return null;
break :blk (try compile_expr(reply, arena, spec[0].value, what)) orelse return null;
};
try accs.append(arena, .{ .key = p.key, .kind = kind, .arg = arg });
}
@@ -2897,10 +3277,11 @@ fn run_group(
defer walk_arena.deinit();
var i: usize = 0;
while (i < src.len()) : (i += 1) {
const id_value: bson.Value = switch (id_class) {
.path => |path| (try stream_path(walk_arena.allocator(), coll, src, i, path)) orelse .null,
.constant => |v| v,
};
const ec: EvalCtx = .{ .arena = walk_arena.allocator(), .coll = coll, .src = src, .i = i };
const id_value: bson.Value = (eval_expr(ec, id_class) catch |err| {
try report_eval_error(reply, err);
return null;
}) orelse .null;
id_key_buf.clearRetainingCapacity();
try bson.write_value(id_value, ctx.gpa, &id_key_buf);
@@ -2924,9 +3305,9 @@ fn run_group(
// A path that resolves to nothing is *absent*, which several of
// these treat differently from a present null: `$push` skips it
// where it would push an explicit null, and `$min` ignores it.
const found: ?bson.Value = switch (acc.arg) {
.path => |path| try stream_path(walk_arena.allocator(), coll, src, i, path),
.constant => |c| c,
const found: ?bson.Value = eval_expr(ec, acc.arg) catch |err| {
try report_eval_error(reply, err);
return null;
};
switch (acc.kind) {
.count => unreachable,
@@ -4788,31 +5169,24 @@ test "$group refuses what it cannot compute instead of answering zero" {
.group = &.{.{ .key = "v", .value = sum_one }},
.code = 15955,
},
// A compound `_id`, `$literal` and a `$multiply` argument stood here
// until the expression evaluator landed. What is refused is still what
// is missing, and these are the two that remain missing.
.{
.name = "a compound _id",
.name = "an unknown expression operator",
.group = &.{
.{ .key = "_id", .value = .{ .doc = &.{.{ .key = "k", .value = .{ .string = "$k" } }} } },
.{ .key = "_id", .value = .{ .doc = &.{.{ .key = "$bogusExpr", .value = path_x }} } },
.{ .key = "v", .value = sum_one },
},
.code = 168,
},
.{
.name = "an operator expression in _id",
.name = "an operator given the wrong number of operands",
.group = &.{
.{ .key = "_id", .value = .{ .doc = &.{.{ .key = "$literal", .value = .{ .int32 = 1 } }} } },
.{ .key = "_id", .value = .{ .doc = &.{.{ .key = "$subtract", .value = .{ .array = &.{path_x} } }} } },
.{ .key = "v", .value = sum_one },
},
.code = 168,
},
.{
.name = "an expression argument to $sum",
.group = &.{
.{ .key = "_id", .value = .null },
.{ .key = "v", .value = .{ .doc = &.{.{ .key = "$sum", .value = .{ .doc = &.{
.{ .key = "$multiply", .value = .{ .array = &.{ path_x, .{ .int32 = 2 } } } },
} } }} } },
},
.code = 168,
.code = 16020,
},
};

View File

@@ -62,8 +62,8 @@ Recorded against mongod 8.3.7. At the M2 tip it read 9 pass / 10 fail; with the
accumulators in:
```
group-accumulators.json 18 pass 1 fail 0 skip
expressions.json 1 pass 26 fail 0 skip
group-accumulators.json 19 pass 0 fail 0 skip
expressions.json 27 pass 0 fail 0 skip
```
`group-accumulators` found its first real disagreement on the way to 18:
@@ -71,11 +71,10 @@ expressions.json 1 pass 26 fail 0 skip
that counted documents rather than numbers would have passed every test
anybody would think to write by hand.
`expressions.json` is the next tier's spec, recorded and not yet implemented --
its one pass is the unknown-operator refusal M2 already answers correctly.
Expressions are exercised through `$group`, because `_id` and the accumulator
arguments are the only expression positions that exist until `$addFields` and
`$project`'s computed fields land.
`expressions.json` was recorded before the evaluator was written and read
1 pass / 26 fail against it; it is green now. Expressions are exercised through
`$group`, because `_id` and the accumulator arguments are the only expression
positions that exist until `$addFields` and `$project`'s computed fields land.
What recording it settled, none of which is guessable: