Pin the column group toggle to the cursor; guard run identity in e265's enumeration (improvise-uv9e, improvise-6u6s)

uv9e: ToggleGroupAtCursor { is_row: false } (gz) had no command-level
test, and a version reading col_group_for(0) passed the workspace. A new
test seats the cursor at (0, 2) with Month on the columns and requires
Q2. Reading column 0, or reading the row half of the cursor, both fail
it. The row half was already closed: the grid tests sit on row 2, and
injecting row_group_for(0) fails five of them. The browser's z is still
Upstream with no resolver arm (improvise-uln5), so it has no cursor
reading to test.

6u6s: the e265 enumeration could not see run identity. A pruner keying
runs by (level, category, group name) instead of header position
passed. The enumeration now:
- compares every level's run name and whether each survivor shares a
  run with the previous one;
- adds three fixtures: a non-contiguous group, a level with no groups,
  and two adjacent outer groups;
- builds each layout once per collapse subset and drives
  prune_axis_items with a keep predicate per row set: 4,992 cases.

The name-keyed rival now fails at fixture 2, mask 0b101. open_runs is
the one reader of the markers. improvise-core's lib went from
0.86-0.92s to 0.17-0.23s on an Apple M4 Max under load average 39-77.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Edward Langley
2026-09-23 17:08:02 -07:00
parent d6e366dceb
commit 2ea2407456
5 changed files with 307 additions and 133 deletions
+8
View File
@@ -296,6 +296,14 @@ closes the deeper runs innermost-out before it touches a level, as
equivalent but not equal (`End0` where the input has `End1, End0`), and the
enumeration test asserts equality whenever every row survives.
**How it is tested** (improvise-6u6s). An enumeration compares each
surviving row's whole open-run stack, and whether it shares a run with the
previous survivor, pruned against unpruned. It runs over five fixtures,
including a non-contiguous group, a level with no groups, and two adjacent
outer groups. Without the non-contiguous fixture, a pruner that keyed runs by
name instead of header position passed every test. `open_runs` is the one
reading of the markers: `innermost_group` and the tests both use it.
A header's level also names its category (`cats[level]`) on the wire, where
`FrameGroup` carries `level` and not the category (improvise-7xyx removed that
field): see `context/code/protocol.md` §3a.
+23
View File
@@ -6237,3 +6237,26 @@ folds `Q1`, the innermost group, and not `Upper`. `z` on `North/Mar` folds
`Upper`, and not `Q1`. The flat-membership rival fails it: `z` on `North/Mar`
found no group and did nothing. It is not a lockstep test, because a browser's
`z` is still a no-op (improvise-uln5).
### a_column_toggle_names_the_group_under_the_cursors_column
improvise-uv9e. `ToggleGroupAtCursor { is_row: false }`
(`toggle-col-group-under-cursor`, bound to `gz`) had no command-level test. A
version that read `col_group_for(0)` passed the whole workspace: 1,946 passed,
0 failed, with that rival injected. The fixture puts `Month`, with `Q1 = {Jan,
Feb}` and `Q2 = {Apr, May}`, on the columns and seats the cursor at `(0, 2)`,
on `Apr`. That is off the origin on the axis the toggle reads, and on the origin
of the other. The command must name `Q2`, and applying it must fold `Q2` and
leave `Q1`.
Rivals, each injected into `ToggleGroupAtCursor::execute` and restored, over
the whole workspace:
- `col_group_for(0)`: only this test fails;
- `col_group_for(ctx.selected.0)`, the row half of the cursor: only this test
fails.
The **row** half of improvise-uv9e was already closed when this landed. The
ticket was written at `a4910eb4`, when every row test sat at `(0, 0)`. Since
then, the grid tests seat the cursor on `Apr` (row 2). Injecting
`row_group_for(0)` fails five of them, plus `z_on_a_row_past_an_inner_run_folds_the_outer_group`
from improvise-e265.
+60 -25
View File
@@ -1512,41 +1512,68 @@ are each compared with the same document with Month on `Axis::None`. At
cannot pass. Rival: `read_coordinate` ignoring the reach (`if false && …`).
This test fails.
### pruning_does_not_move_a_surviving_row_to_another_group
### pruning_keeps_every_surviving_rows_runs_at_every_level
improvise-e265. On a two-level grouped axis, `prune_empty` used to change a
surviving row's group. Pruning the inner header dropped its `GroupEnd` too, and
under the flat reading the ungrouped inner item then joined the outer group.
The test enumerates every funded-cell mask and every collapse subset of two
fixtures:
- `Region` over `Month` (two outer items, four inner, three groups);
- `Region` over `Month` over `Kind` (three levels).
improvise-e265, strengthened by improvise-6u6s. On a two-level grouped axis,
`prune_empty` used to change a surviving row's group. Pruning the inner header
dropped its `GroupEnd` too, and under the flat reading the ungrouped inner item
then joined the outer group.
That is about 4,000 cases in both prune states, built once in a `OnceLock` for
both enumerating tests. For every surviving row it asserts that the innermost
group (`innermost_group`, which `row_group_for` is) is the same pruned as
unpruned. When every row survives
it also asserts the pruned `row_items` equal the unpruned ones, marker for
marker.
**What it asserts.** For every surviving row and at every level:
- the run the row is in names the same `(category, group)` pruned as unpruned
(the whole open-run stack, from `open_runs`, not only the innermost);
- whether it is in the **same run** as the previous survivor is the same
answer pruned as unpruned, where "the same run" means the same header entry.
Failed at base 202e7eac on `mask 0b100` (only `North/Mar` funded): pruned said
`Upper`, unpruned said `None`. Rivals, each injected and restored:
- a flat `innermost_group` (every marker read at level 0) fails it;
- a pruner that owes no `GroupEnd` (u6gc's shape: a run whose closing header
was pruned is left open) fails it;
- a pruner that keeps headers as they arrive fails it.
When every row survives, the pruned entries must equal the unpruned ones,
marker for marker.
**The fixtures** are five, each under every collapse subset of its groups:
- `Region` over `Month`;
- three levels, `Region` over `Month` over `Kind`;
- `Month = [Jan in Q1, Mar, Feb in Q1]`, a non-contiguous group, so one parent
holds two `Q1` runs (improvise-h0iv);
- a middle level with no groups at all (`Kind` between `Region` and `Month`);
- two outer groups side by side (`North` in `Upper`, `South` in `Lower`).
**How it is built** (improvise-6u6s). Each fixture's layout is built once per
collapse subset, with no cells, so every row is present. `prune_axis_items` is
then called directly with a keep predicate for every non-empty set of rows.
That makes 4,992 cases. The first version built two `Workbook`s per funded-cell
mask, about 4,000 cases, and put `improvise-core`'s lib at 0.86–0.92s on an
Apple M4 Max (16 cores) under load average 39–77, against about 0.16s without
it. The reviewer measured 0.94–2.46s under heavier load, once past the 2s
tripwire. Now the lib runs in 0.17–0.23s on the same machine, against 0.16–0.18s
with the module skipped. `prune_empty` is still covered, because it calls the
same pruner, and `prune_empty_keeps_a_row_past_an_inner_run_in_the_outer_group`
pins that wiring.
**Red and rivals.** The e265 form failed at base 202e7eac, on `mask 0b100` with
only `North/Mar` funded: pruned said `Upper`, unpruned said `None`. Rivals, each
injected and restored:
- a flat reader (every marker read at level 0) fails it;
- a pruner that owes no `GroupEnd` fails it (u6gc's shape: a run whose closing
header was pruned is left open);
- a pruner that keeps headers as they arrive fails it;
- **runs keyed by `(level, category, group name)` instead of header position**
fails it, at `fixture 2, mask 0b101, collapsed []: run identity at level 1,
row 1`. Pruning `Mar` must leave North's two `Q1` runs as two, and the rival
merges them.
That last rival passed every test in #250 (improvise-6u6s). Those fixtures had
no non-contiguous group and compared only the innermost group, so they could not
see a merged run.
The equality half was added after the first version of `remark` emitted `End0`
where the input had `End1, End0`. That output was equivalent but not equal, and
the new assertion caught it.
the assertion caught it.
### a_pruned_axis_draws_no_header_over_nothing_and_ends_on_no_marker
improvise-e265's other property, over the same enumeration. Every kept header
improvise-e265's other property, over the same 4,992 cases. Every kept header
must be followed, past any deeper headers, by a data item, and the axis must
not end on a `GroupEnd`. Both failed at base 202e7eac: `mask 0b1` left a
trailing `GroupEnd`, and the ticket counted the header-over-nothing shape on
main before #200.
not end on a `GroupEnd`. At base 202e7eac, `mask 0b1` left a trailing
`GroupEnd`. The ticket counted the header-over-nothing shape on main before #200.
Rivals:
- a pruner that keeps headers as they arrive fails the first half;
@@ -1558,6 +1585,14 @@ emitted only before an item. So the obvious rival, deleting a trailing-marker
strip, has nothing to delete. An earlier draft had that strip, and it was dead
code.
### prune_empty_keeps_a_row_past_an_inner_run_in_the_outer_group
The enumeration drives `prune_axis_items` directly, so this test pins that
`prune_empty` reaches it. With only `North/Mar` funded, the pruned grid has one
row, and it is in `Upper`. It is a wiring test, not a discriminating one: #200's
pruner also answered `Upper` here. What it catches is a `prune_empty` that
stops pruning (the row count) or a reader that loses the outer run.
### a_row_after_an_inner_groups_end_is_still_in_the_outer_group
The user's ruling on improvise-e265, at the layout: membership is per level,
@@ -177,6 +177,41 @@ mod tests {
);
}
/// The column twin had no command-level test: an `is_row: false` toggle that
/// read column 0 passed the workspace (improvise-uv9e). Evidence file.
#[test]
fn a_column_toggle_names_the_group_under_the_cursors_column() {
use crate::command::key::{KeyCode, KeyModifiers};
use crate::ui::app::App;
use crate::view::Axis;
let mut wb = two_groups_on_the_row_axis();
wb.active_view_mut().set_axis("Month", Axis::Column);
wb.active_view_mut().set_axis("Type", Axis::Row);
let mut app = App::new(wb, None);
app.rebuild_layout();
// Row 0 and column 2: off the origin on the axis the toggle reads, and on
// the origin of the other, so reading the wrong half is seen too.
app.cursor_mut().selected = (0, 2);
assert_eq!(app.layout.col_label(2), "Apr", "the cursor sits on Apr");
let effects = {
let ctx = app.cmd_context(KeyCode::Char('z'), KeyModifiers::NONE);
ToggleGroupAtCursor { is_row: false }.execute(&ctx)
};
let dbg = effects_debug(&effects);
assert!(
dbg.contains(r#"category: "Month", group: "Q2""#),
"Apr is in Q2, got: {dbg}"
);
app.apply_effects(effects);
let view = app.model_state.workbook.active_view();
assert!(view.is_group_collapsed("Month", "Q2"));
assert!(
!view.is_group_collapsed("Month", "Q1"),
"the neighbour stays"
);
}
/// Evidence: `context/test-evidence/command.md`.
#[test]
fn toggling_a_group_collapses_that_group_and_the_cursor_follows_it() {
+181 -108
View File
@@ -348,34 +348,38 @@ fn remark(input: &OpenRuns, output: &mut Vec<Option<usize>>, kept: &mut Vec<Axis
}
}
/// The innermost run open at the data item `idx` — the group nearest it, which
/// is what `z` toggles (improvise-e265's ruling).
fn innermost_group(items: &[AxisEntry], idx: usize) -> Option<(String, String)> {
let mut open: OpenRuns = Vec::new();
let mut data_idx = 0usize;
/// Each data item's open runs, by level, as the position of the header that
/// opened each: the one reading of the markers (improvise-e265).
fn open_runs(items: &[AxisEntry]) -> Vec<Vec<Option<usize>>> {
let mut open: Vec<Option<usize>> = Vec::new();
let mut out = Vec::new();
for (at, entry) in items.iter().enumerate() {
match entry {
AxisEntry::GroupHeader { level, .. } => {
open_at(&mut open, *level, Some((at, entry.clone())))
open.truncate(*level);
open.resize(*level, None);
open.push(Some(at));
}
AxisEntry::GroupEnd { level } => open_at(&mut open, *level, None),
AxisEntry::DataItem { .. } if data_idx == idx => {
return open.into_iter().rev().find_map(|run| match run {
Some((
_,
AxisEntry::GroupHeader {
cat_name,
group_name,
..
},
)) => Some((cat_name, group_name)),
_ => None,
});
}
AxisEntry::DataItem { .. } => data_idx += 1,
AxisEntry::GroupEnd { level } => open.truncate(*level),
AxisEntry::DataItem { .. } => out.push(open.clone()),
}
}
None
out
}
/// The innermost run open at the data item `idx` — the group nearest it, which
/// is what `z` toggles (improvise-e265's ruling).
fn innermost_group(items: &[AxisEntry], idx: usize) -> Option<(String, String)> {
let runs = open_runs(items);
let at = runs.get(idx)?.iter().rev().find_map(|run| *run)?;
match &items[at] {
AxisEntry::GroupHeader {
cat_name,
group_name,
..
} => Some((cat_name.clone(), group_name.clone())),
AxisEntry::GroupEnd { .. } | AxisEntry::DataItem { .. } => None,
}
}
/// One category constraining which records are visible: what it is fixed to,
@@ -2059,16 +2063,15 @@ mod tests {
}
/// Group membership on a multi-level axis is per level, and pruning cannot move
/// it (improvise-e265). `context/test-evidence/core.md`.
/// it (improvise-e265, improvise-6u6s). `context/test-evidence/core.md`.
#[cfg(test)]
mod group_membership {
use super::{AxisEntry, GridLayout, data_items, innermost_group};
use crate::model::cell::{CellKey, CellValue};
use super::{AxisEntry, GridLayout, data_items, open_runs, prune_axis_items};
use crate::view::Axis;
use crate::workbook::Workbook;
use std::sync::OnceLock;
type Level<'a> = (&'a str, &'a [(&'a str, Option<&'a str>)]);
type Fixture<'a> = (&'a [Level<'a>], &'a [(&'a str, &'a str)]);
const TWO: &[Level] = &[
("Region", &[("North", Some("Upper")), ("South", None)]),
@@ -2089,24 +2092,55 @@ mod group_membership {
("Kind", &[("Food", Some("Goods")), ("Tools", None)]),
];
/// Every row-coordinate tuple of `levels`, outermost first.
fn tuples(levels: &[Level]) -> Vec<Vec<String>> {
levels.iter().fold(vec![vec![]], |acc, (_, items)| {
acc.iter()
.flat_map(|t| {
items.iter().map(move |(item, _)| {
let mut t = t.clone();
t.push(item.to_string());
t
})
})
.collect()
})
}
/// `Q1` is not contiguous, so one parent holds two `Q1` runs (h0iv).
const NONCONTIGUOUS: &[Level] = &[
("Region", &[("North", Some("Upper")), ("South", None)]),
(
"Month",
&[("Jan", Some("Q1")), ("Mar", None), ("Feb", Some("Q1"))],
),
];
/// `levels` on Row, one column, the tuples `mask` sets funded, the groups
/// `collapsed` names folded.
fn book(levels: &[Level], mask: u32, collapsed: &[(&str, &str)], prune: bool) -> Workbook {
/// A middle level with no groups at all.
const HOLE: &[Level] = &[
("Region", &[("North", Some("Upper")), ("South", None)]),
("Kind", &[("Food", None), ("Tools", None)]),
("Month", &[("Jan", Some("Q1")), ("Mar", None)]),
];
/// Two outer groups side by side, with no ungrouped item between them.
const ADJACENT: &[Level] = &[
(
"Region",
&[
("North", Some("Upper")),
("South", Some("Lower")),
("West", None),
],
),
("Month", &[("Jan", Some("Q1")), ("Mar", None)]),
];
const FIXTURES: &[Fixture] = &[
(
TWO,
&[("Region", "Upper"), ("Month", "Q1"), ("Month", "Q2")],
),
(
THREE,
&[("Region", "Upper"), ("Month", "Q1"), ("Kind", "Goods")],
),
(NONCONTIGUOUS, &[("Region", "Upper"), ("Month", "Q1")]),
(HOLE, &[("Region", "Upper"), ("Month", "Q1")]),
(
ADJACENT,
&[("Region", "Upper"), ("Region", "Lower"), ("Month", "Q1")],
),
];
/// `levels` on Row, one column, the groups `collapsed` names folded. No cell
/// is set: the unpruned layout has every row, and pruning is driven directly.
fn book(levels: &[Level], collapsed: &[(&str, &str)]) -> Workbook {
let mut wb = Workbook::new("T");
for (cat, items) in levels {
wb.add_category(*cat).unwrap();
@@ -2121,22 +2155,9 @@ mod group_membership {
wb.add_category("Col").unwrap();
wb.add_item("Col", "X").unwrap();
wb.active_view_mut().set_axis("Col", Axis::Column);
for (i, tuple) in tuples(levels).iter().enumerate() {
if mask & (1 << i) != 0 {
let mut coords: Vec<(String, String)> = levels
.iter()
.zip(tuple)
.map(|((cat, _), item)| (cat.to_string(), item.clone()))
.collect();
coords.push(("Col".into(), "X".into()));
wb.model
.set_cell(CellKey::new(coords), CellValue::Number(1.0));
}
}
for (cat, group) in collapsed {
wb.active_view_mut().toggle_group_collapse(cat, group);
}
wb.active_view_mut().prune_empty = prune;
wb
}
@@ -2144,70 +2165,106 @@ mod group_membership {
GridLayout::with_frozen_records(&wb.model, wb.active_view(), None)
}
/// One enumerated case: its name, then the unpruned and pruned row entries.
type Case = (String, Vec<AxisEntry>, Vec<AxisEntry>);
/// One case: its name, the kept-row mask, and the unpruned and pruned rows.
struct Case {
name: String,
mask: u32,
full: Vec<AxisEntry>,
pruned: Vec<AxisEntry>,
}
/// Every (mask, collapse subset) of `levels`, unpruned and pruned.
fn every_case(levels: &[Level], groups: &[(&str, &str)]) -> Vec<Case> {
let cells = tuples(levels).len();
/// Every collapse subset of every fixture, one layout each, pruned by every
/// non-empty set of kept rows. The layout is the cost, so it is built once.
fn cases() -> Vec<Case> {
let mut out = Vec::new();
for folds in 0..(1u32 << groups.len()) {
let collapsed: Vec<(&str, &str)> = (0..groups.len())
.filter(|g| folds & (1 << g) != 0)
.map(|g| groups[g])
.collect();
for mask in 1..(1u32 << cells) {
let case = format!("mask {mask:#b}, collapsed {collapsed:?}");
let mut wb = book(levels, mask, &collapsed, false);
let full = layout(&wb).row_items;
wb.active_view_mut().prune_empty = true;
out.push((case, full, layout(&wb).row_items));
for (fixture, (levels, groups)) in FIXTURES.iter().enumerate() {
for folds in 0..(1u32 << groups.len()) {
let collapsed: Vec<(&str, &str)> = (0..groups.len())
.filter(|g| folds & (1 << g) != 0)
.map(|g| groups[g])
.collect();
let full = layout(&book(levels, &collapsed)).row_items;
let rows = data_items(&full).count();
for mask in 1..(1u32 << rows) {
let mut pruned = full.clone();
prune_axis_items(&mut pruned, |i| mask & (1 << i) != 0);
out.push(Case {
name: format!("fixture {fixture}, mask {mask:#b}, collapsed {collapsed:?}"),
mask,
full: full.clone(),
pruned,
});
}
}
}
out
}
/// Built once for both enumerating tests: the enumeration is the cost.
fn cases() -> &'static [Case] {
static CASES: OnceLock<Vec<Case>> = OnceLock::new();
CASES.get_or_init(|| {
let mut all = every_case(
TWO,
&[("Region", "Upper"), ("Month", "Q1"), ("Month", "Q2")],
);
all.extend(every_case(
THREE,
&[("Region", "Upper"), ("Month", "Q1"), ("Kind", "Goods")],
));
all
})
/// The `(category, group)` a run's header names.
fn named(items: &[AxisEntry], run: Option<usize>) -> Option<(&str, &str)> {
match &items[run?] {
AxisEntry::GroupHeader {
cat_name,
group_name,
..
} => Some((cat_name, group_name)),
AxisEntry::GroupEnd { .. } | AxisEntry::DataItem { .. } => None,
}
}
fn at(runs: &[Option<usize>], level: usize) -> Option<usize> {
runs.get(level).copied().flatten()
}
#[test]
fn pruning_does_not_move_a_surviving_row_to_another_group() {
for (case, full, pruned) in cases() {
let full_rows: Vec<&Vec<String>> = data_items(full).collect();
let pruned_rows: Vec<&Vec<String>> = data_items(pruned).collect();
if pruned_rows.len() == full_rows.len() {
assert_eq!(pruned, full, "{case}: nothing pruned");
fn pruning_keeps_every_surviving_rows_runs_at_every_level() {
for case in cases() {
let Case {
name,
mask,
full,
pruned,
} = &case;
let kept: Vec<usize> = (0..data_items(full).count())
.filter(|i| mask & (1 << i) != 0)
.collect();
let (full_runs, pruned_runs) = (open_runs(full), open_runs(pruned));
assert_eq!(pruned_runs.len(), kept.len(), "{name}: the survivors");
if kept.len() == full_runs.len() {
assert_eq!(pruned, full, "{name}: nothing pruned");
}
for (r, tuple) in pruned_rows.iter().enumerate() {
let u = full_rows
.iter()
.position(|t| t == tuple)
.expect("a pruned row is a row of the unpruned grid");
assert_eq!(
innermost_group(pruned, r),
innermost_group(full, u),
"{case}: row {tuple:?}\n pruned {pruned:?}\n full {full:?}",
);
let levels = full_runs.iter().map(Vec::len).max().unwrap_or(0);
for (r, &u) in kept.iter().enumerate() {
for level in 0..levels {
assert_eq!(
named(pruned, at(&pruned_runs[r], level)),
named(full, at(&full_runs[u], level)),
"{name}: row {r}, level {level}\n pruned {pruned:?}\n full {full:?}"
);
if r == 0 {
continue;
}
let same = |runs: &[Vec<Option<usize>>], a: usize, b: usize| {
at(&runs[a], level).is_some() && at(&runs[a], level) == at(&runs[b], level)
};
assert_eq!(
same(&pruned_runs, r - 1, r),
same(&full_runs, kept[r - 1], u),
"{name}: run identity at level {level}, row {r}\n pruned {pruned:?}\n full {full:?}"
);
}
}
}
}
#[test]
fn a_pruned_axis_draws_no_header_over_nothing_and_ends_on_no_marker() {
for (case, _, items) in cases() {
for Case {
name,
pruned: items,
..
} in cases()
{
for (i, entry) in items.iter().enumerate() {
if let AxisEntry::GroupHeader { .. } = entry {
let next = items[i + 1..]
@@ -2215,24 +2272,40 @@ mod group_membership {
.find(|e| !matches!(e, AxisEntry::GroupHeader { .. }));
assert!(
matches!(next, Some(AxisEntry::DataItem { .. })),
"{case}: header {entry:?} has no member in {items:?}"
"{name}: header {entry:?} has no member in {items:?}"
);
}
}
assert!(
!matches!(items.last(), Some(AxisEntry::GroupEnd { .. })),
"{case}: trailing marker in {items:?}"
"{name}: trailing marker in {items:?}"
);
}
}
/// `prune_empty` reaches the same pruner from funded cells: the ticket's own
/// shape, only `North/Mar` funded, keeps `North/Mar` in `Upper`.
#[test]
fn prune_empty_keeps_a_row_past_an_inner_run_in_the_outer_group() {
let mut wb = book(TWO, &[]);
let cell = crate::model::cell::CellKey::new(vec![
("Region".into(), "North".into()),
("Month".into(), "Mar".into()),
("Col".into(), "X".into()),
]);
wb.model
.set_cell(cell, crate::model::cell::CellValue::Number(1.0));
wb.active_view_mut().prune_empty = true;
let l = layout(&wb);
assert_eq!(l.row_count(), 1);
assert_eq!(l.row_group_for(0), Some(("Region".into(), "Upper".into())));
}
/// The user's ruling on improvise-e265: membership is per level, and `z`
/// names the innermost open group — the one nearest the cursor.
#[test]
fn a_row_after_an_inner_groups_end_is_still_in_the_outer_group() {
// North/Jan (0) and North/Mar (2) funded; nothing is pruned or folded.
let wb = book(TWO, 0b101, &[], false);
let l = layout(&wb);
let l = layout(&book(TWO, &[]));
let at = |tuple: [&str; 2]| {
(0..l.row_count())
.find(|&r| l.row_tuple(r) == Some(&[tuple[0].into(), tuple[1].into()][..]))