From 82e4b0fb4e9016764d9af854cb39035acfab4fe9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 20:06:56 +0000 Subject: [PATCH 01/15] =?UTF-8?q?quack:=20DuckDB=20differential=20harness?= =?UTF-8?q?=20=E2=80=94=2013=20relational=20cases=20green,=203=20JOIN=20ca?= =?UTF-8?q?ses=20red=20by=20design?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DuckDB 1.5.5 is the semantic oracle; `tests/duckdb/oracle.py` fills the `expected` column of `cases.tsv` from the committed ERP-shaped fixture (partner 64 / doc 512 / line 4096, seeded, NOT NULL, foreign keys are row indices). The Rust side lowers each case through Quack onto mask-risc and asserts byte-equal encodings; a drift guard proves the committed CSVs equal the in-memory generator. Measured per case (single-threaded, counting allocator): every case runs with alloc_bytes_exec=0, rows_materialized=0 and no index vector, except the explicit projection (`rows_proj`: 33 rows, through the one named sink). GROUP BY is honest about its shape today: `programs=8`, K passes. The three `join_*` cases load their oracle value and then `todo!("NO JOIN LOWERING")` under `#[should_panic]` — no join lowering exists in Quack and none was faked here. They are the falsified seam Wave 2 closes. Two gaps the harness exposed on the way: mask-risc `Pred` has no ORDERED u32 comparison (`doc_id` is exposed as an i32 lane; `cost_center<4` is a `MatchU32` care-mask on a 3-bit domain), and `masked_strided_group_sum` is NOT a keyed group-by — it sums the byte-groups of one V3 register into one scalar and has zero callers — so the "one-terminal GROUP BY SUM" gap cannot be closed by calling it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- .../lance-graph-quack/tests/duckdb/README.txt | 14 + .../lance-graph-quack/tests/duckdb/cases.tsv | 17 + .../tests/duckdb/data/doc.csv | 513 +++ .../tests/duckdb/data/line.csv | 4097 +++++++++++++++++ .../tests/duckdb/data/partner.csv | 65 + .../lance-graph-quack/tests/duckdb/fixture.rs | 288 ++ .../lance-graph-quack/tests/duckdb/oracle.py | 94 + .../tests/duckdb_differential.rs | 613 +++ 8 files changed, 5701 insertions(+) create mode 100644 crates/lance-graph-quack/tests/duckdb/README.txt create mode 100644 crates/lance-graph-quack/tests/duckdb/cases.tsv create mode 100644 crates/lance-graph-quack/tests/duckdb/data/doc.csv create mode 100644 crates/lance-graph-quack/tests/duckdb/data/line.csv create mode 100644 crates/lance-graph-quack/tests/duckdb/data/partner.csv create mode 100644 crates/lance-graph-quack/tests/duckdb/fixture.rs create mode 100644 crates/lance-graph-quack/tests/duckdb/oracle.py create mode 100644 crates/lance-graph-quack/tests/duckdb_differential.rs diff --git a/crates/lance-graph-quack/tests/duckdb/README.txt b/crates/lance-graph-quack/tests/duckdb/README.txt new file mode 100644 index 000000000..d1feade36 --- /dev/null +++ b/crates/lance-graph-quack/tests/duckdb/README.txt @@ -0,0 +1,14 @@ +DuckDB is the semantic oracle for this suite. Expected values in cases.tsv +are NEVER hand-edited — they are always regenerated by running oracle.py. + +To regenerate the fixture + expected values after a change to fixture.rs +or cases.tsv's SQL: + + cargo test -p lance-graph-quack --test duckdb_differential \ + write_fixture -- --ignored + /bin/python tests/duckdb/oracle.py + +Encoding: bare scalar -> decimal string; EXISTS -> "0"/"1"; grouped result +-> "k1:v1;k2:v2;..." sorted by key; rid projection -> sorted comma list. +Both oracle.py and duckdb_differential.rs implement this encoding +independently; they must agree, which is what the suite checks. diff --git a/crates/lance-graph-quack/tests/duckdb/cases.tsv b/crates/lance-graph-quack/tests/duckdb/cases.tsv new file mode 100644 index 000000000..a4cc77547 --- /dev/null +++ b/crates/lance-graph-quack/tests/duckdb/cases.tsv @@ -0,0 +1,17 @@ +id sql expected +sel_count_posted SELECT COUNT(*) FROM line WHERE status=1 2825 +conj_count SELECT COUNT(*) FROM line WHERE status=1 AND amount>1000 AND cost_center<4 1083 +disj_count SELECT COUNT(*) FROM line WHERE status=2 OR amount<0 1200 +not_count SELECT COUNT(*) FROM line WHERE NOT(status=1) 1271 +in_count SELECT COUNT(*) FROM line WHERE gl_account IN (400000,600000) 2052 +sum_posted SELECT SUM(amount) FROM line WHERE status=1 20939713 +min_posted_cc3 SELECT MIN(amount) FROM line WHERE status=1 AND cost_center=3 -4997 +max_posted_cc3 SELECT MAX(amount) FROM line WHERE status=1 AND cost_center=3 19977 +exists_neg SELECT EXISTS(SELECT 1 FROM line WHERE amount<-4990) 1 +rows_proj SELECT rid FROM line WHERE status=2 AND qty>45 ORDER BY rid 133,665,699,764,770,891,954,969,1174,1272,1455,1673,1680,1747,1866,1881,1946,2017,2082,2149,2309,2368,2705,2814,2930,3216,3254,3405,3534,3606,3639,3961,4032 +range_docid SELECT COUNT(*) FROM line WHERE doc_id>=128 AND doc_id<256 1056 +group_count_cc WITH g AS (SELECT UNNEST(GENERATE_SERIES(0,7)) AS cost_center) SELECT g.cost_center, COUNT(l.rid) FROM g LEFT JOIN line l ON l.cost_center=g.cost_center AND l.status=1 GROUP BY g.cost_center ORDER BY g.cost_center 0:359;1:358;2:360;3:347;4:343;5:388;6:347;7:323 +group_sum_cc WITH g AS (SELECT UNNEST(GENERATE_SERIES(0,7)) AS cost_center) SELECT g.cost_center, COALESCE(SUM(l.amount),0) FROM g LEFT JOIN line l ON l.cost_center=g.cost_center AND l.status=1 GROUP BY g.cost_center ORDER BY g.cost_center 0:2930321;1:2840790;2:2760912;3:2446433;4:2404560;5:2827013;6:2512475;7:2217209 +join_sum_country SELECT SUM(l.amount) FROM line l JOIN partner p ON p.rid=l.partner_id WHERE l.status=1 AND p.country=3 1237848 +join_count_docs_with_posted SELECT COUNT(*) FROM doc d WHERE EXISTS(SELECT 1 FROM line l WHERE l.doc_id=d.rid AND l.status=1) 511 +join_group_sum_country SELECT p.country, SUM(l.amount) FROM line l JOIN partner p ON p.rid=l.partner_id WHERE l.status=1 GROUP BY p.country ORDER BY p.country 0:4090182;1:1635484;2:5539383;3:1237848;4:2109547;5:1533177;6:2552339;7:2241753 diff --git a/crates/lance-graph-quack/tests/duckdb/data/doc.csv b/crates/lance-graph-quack/tests/duckdb/data/doc.csv new file mode 100644 index 000000000..958a53843 --- /dev/null +++ b/crates/lance-graph-quack/tests/duckdb/data/doc.csv @@ -0,0 +1,513 @@ +rid,company,period,doc_type +0,0,12,1 +1,1,11,0 +2,0,12,0 +3,1,6,1 +4,2,9,2 +5,2,2,1 +6,2,5,2 +7,0,8,1 +8,2,10,1 +9,2,11,0 +10,1,12,0 +11,0,9,1 +12,0,8,3 +13,1,4,1 +14,1,11,0 +15,2,10,1 +16,0,1,3 +17,2,11,2 +18,1,1,3 +19,1,2,2 +20,1,4,0 +21,2,9,3 +22,2,10,3 +23,0,3,0 +24,1,9,3 +25,2,8,2 +26,1,7,1 +27,1,2,3 +28,2,6,2 +29,2,3,2 +30,1,1,0 +31,1,2,3 +32,2,12,0 +33,0,12,1 +34,2,2,0 +35,2,5,3 +36,0,12,2 +37,2,8,1 +38,0,1,3 +39,0,9,2 +40,1,5,1 +41,2,10,1 +42,1,2,1 +43,1,8,0 +44,2,3,2 +45,1,7,2 +46,1,8,2 +47,0,9,2 +48,2,9,1 +49,0,7,1 +50,1,7,1 +51,2,4,3 +52,2,3,0 +53,0,5,0 +54,1,9,0 +55,0,3,3 +56,1,2,2 +57,2,5,1 +58,0,2,3 +59,1,11,2 +60,0,8,0 +61,0,9,3 +62,1,3,3 +63,0,9,1 +64,0,8,1 +65,1,8,0 +66,1,12,2 +67,1,2,3 +68,0,3,3 +69,2,7,3 +70,0,10,2 +71,0,2,0 +72,0,9,3 +73,0,6,2 +74,1,12,2 +75,0,12,0 +76,0,3,2 +77,2,10,1 +78,2,3,0 +79,2,4,1 +80,2,3,2 +81,2,1,2 +82,1,11,2 +83,2,12,0 +84,1,11,0 +85,1,6,2 +86,2,7,2 +87,1,10,2 +88,1,2,3 +89,2,12,1 +90,1,5,1 +91,1,1,3 +92,2,10,0 +93,1,6,0 +94,0,12,0 +95,1,1,1 +96,2,6,0 +97,1,2,2 +98,1,6,2 +99,0,11,3 +100,0,8,0 +101,1,4,1 +102,1,7,1 +103,0,11,1 +104,0,4,2 +105,2,12,2 +106,0,1,2 +107,0,1,1 +108,2,3,1 +109,1,3,2 +110,1,6,1 +111,2,6,0 +112,1,1,1 +113,2,2,0 +114,0,1,0 +115,1,11,2 +116,0,7,1 +117,0,11,3 +118,0,7,3 +119,0,6,2 +120,1,7,2 +121,2,12,1 +122,2,2,1 +123,2,4,1 +124,1,11,1 +125,1,2,0 +126,0,6,0 +127,0,6,3 +128,2,4,3 +129,1,9,3 +130,2,9,3 +131,2,3,0 +132,1,12,0 +133,2,5,1 +134,0,11,2 +135,0,10,3 +136,0,10,3 +137,2,9,2 +138,0,11,3 +139,1,2,1 +140,2,1,0 +141,2,9,1 +142,0,1,3 +143,0,11,3 +144,1,12,1 +145,0,12,3 +146,0,11,3 +147,0,2,3 +148,2,10,0 +149,0,1,1 +150,1,5,0 +151,0,12,2 +152,1,12,2 +153,1,9,3 +154,2,7,1 +155,1,5,3 +156,1,10,2 +157,0,6,2 +158,0,4,2 +159,1,3,2 +160,1,2,1 +161,1,6,0 +162,0,9,1 +163,2,11,1 +164,0,6,1 +165,2,10,2 +166,1,8,2 +167,1,6,1 +168,1,1,3 +169,1,9,2 +170,1,2,2 +171,1,12,1 +172,1,10,0 +173,1,2,3 +174,0,8,2 +175,2,7,2 +176,0,5,1 +177,0,5,3 +178,0,5,2 +179,2,1,3 +180,0,2,1 +181,2,5,0 +182,2,11,3 +183,2,10,1 +184,1,2,1 +185,0,4,1 +186,1,4,1 +187,0,2,2 +188,2,8,0 +189,0,1,2 +190,1,12,3 +191,0,9,0 +192,0,12,3 +193,1,5,1 +194,0,12,3 +195,0,11,1 +196,2,3,0 +197,2,1,0 +198,1,1,0 +199,2,11,1 +200,2,9,1 +201,1,4,1 +202,2,9,2 +203,0,7,3 +204,0,7,0 +205,0,10,0 +206,1,10,3 +207,2,8,2 +208,0,9,3 +209,0,7,3 +210,0,1,0 +211,1,3,0 +212,2,3,0 +213,2,1,0 +214,2,2,3 +215,2,11,1 +216,1,2,1 +217,1,7,2 +218,1,5,3 +219,0,6,3 +220,0,5,2 +221,2,2,1 +222,0,3,3 +223,0,8,3 +224,2,3,0 +225,2,7,2 +226,1,1,0 +227,2,9,0 +228,0,12,1 +229,0,8,0 +230,1,3,2 +231,1,7,1 +232,2,11,3 +233,1,2,2 +234,1,5,1 +235,2,10,3 +236,2,12,3 +237,1,1,0 +238,2,12,2 +239,2,6,0 +240,1,11,0 +241,2,12,0 +242,2,9,1 +243,0,9,2 +244,2,3,2 +245,1,10,1 +246,1,8,0 +247,2,4,1 +248,1,2,1 +249,2,2,1 +250,0,7,1 +251,0,6,1 +252,2,6,2 +253,2,5,3 +254,0,9,0 +255,2,8,3 +256,2,2,1 +257,2,12,0 +258,1,7,1 +259,0,7,0 +260,0,2,2 +261,1,2,3 +262,2,4,1 +263,1,3,2 +264,2,3,3 +265,1,1,2 +266,2,11,2 +267,1,7,0 +268,2,9,3 +269,0,5,2 +270,0,8,2 +271,0,1,3 +272,0,5,2 +273,1,4,3 +274,1,10,1 +275,2,2,0 +276,0,11,3 +277,0,4,0 +278,0,11,2 +279,2,12,1 +280,1,11,3 +281,0,7,3 +282,2,8,0 +283,2,4,3 +284,0,11,1 +285,1,7,0 +286,2,1,0 +287,1,10,3 +288,0,8,0 +289,1,11,2 +290,1,10,0 +291,2,4,3 +292,0,9,1 +293,0,8,3 +294,2,11,2 +295,1,8,0 +296,0,7,1 +297,1,12,3 +298,1,11,2 +299,2,1,2 +300,2,7,3 +301,1,10,1 +302,2,6,0 +303,2,11,0 +304,2,12,3 +305,2,11,0 +306,2,4,2 +307,2,3,0 +308,2,9,3 +309,1,1,1 +310,1,3,3 +311,0,6,0 +312,1,3,3 +313,2,2,2 +314,0,7,2 +315,2,12,2 +316,0,9,3 +317,1,1,3 +318,2,3,0 +319,1,11,3 +320,0,9,2 +321,0,3,3 +322,0,10,0 +323,0,12,3 +324,1,7,2 +325,0,11,2 +326,1,5,0 +327,0,4,0 +328,0,5,3 +329,0,8,2 +330,1,7,0 +331,2,8,3 +332,0,4,1 +333,1,2,3 +334,1,8,1 +335,0,10,3 +336,0,5,1 +337,0,8,0 +338,2,9,3 +339,0,12,2 +340,2,4,0 +341,1,2,0 +342,0,11,0 +343,1,10,1 +344,2,10,0 +345,2,4,3 +346,1,4,3 +347,2,3,3 +348,2,2,3 +349,2,8,2 +350,1,6,0 +351,0,9,0 +352,2,10,0 +353,1,6,1 +354,1,2,0 +355,1,11,1 +356,0,1,0 +357,0,10,2 +358,2,2,0 +359,1,3,0 +360,1,12,3 +361,1,5,1 +362,1,1,3 +363,2,9,1 +364,2,7,3 +365,2,3,1 +366,1,6,2 +367,0,2,3 +368,2,12,3 +369,2,10,1 +370,1,5,1 +371,2,12,1 +372,0,1,3 +373,2,12,0 +374,2,11,1 +375,2,12,3 +376,1,2,0 +377,2,8,2 +378,0,5,0 +379,1,5,1 +380,1,8,2 +381,1,8,1 +382,0,12,2 +383,0,7,3 +384,0,4,1 +385,0,4,1 +386,0,2,2 +387,0,11,0 +388,0,8,2 +389,1,11,0 +390,2,6,2 +391,1,5,0 +392,1,1,1 +393,2,6,3 +394,1,1,0 +395,2,10,2 +396,2,2,0 +397,2,8,3 +398,2,9,0 +399,1,12,1 +400,2,6,2 +401,2,8,1 +402,0,1,3 +403,1,3,1 +404,2,8,3 +405,0,9,1 +406,2,12,2 +407,0,2,3 +408,1,1,3 +409,2,5,2 +410,1,10,1 +411,1,1,2 +412,0,8,1 +413,1,5,2 +414,2,1,0 +415,0,11,2 +416,0,7,2 +417,2,3,2 +418,0,7,0 +419,1,8,1 +420,2,4,0 +421,1,9,2 +422,0,1,2 +423,1,3,2 +424,2,5,2 +425,0,5,3 +426,1,10,1 +427,2,1,1 +428,1,2,3 +429,1,9,3 +430,0,12,3 +431,0,3,1 +432,1,7,3 +433,0,8,2 +434,0,1,3 +435,1,8,1 +436,0,1,0 +437,1,9,0 +438,0,4,1 +439,2,8,2 +440,0,3,1 +441,2,8,2 +442,0,7,1 +443,1,4,0 +444,2,11,3 +445,2,10,1 +446,0,12,1 +447,0,12,1 +448,0,10,3 +449,2,4,1 +450,1,11,0 +451,2,3,1 +452,2,3,2 +453,1,4,3 +454,2,4,1 +455,1,4,1 +456,2,11,1 +457,2,4,0 +458,2,8,0 +459,1,12,0 +460,2,11,1 +461,2,9,1 +462,0,2,1 +463,2,10,3 +464,0,2,3 +465,1,7,1 +466,1,1,3 +467,1,2,3 +468,1,5,1 +469,2,8,1 +470,2,4,0 +471,2,1,2 +472,2,8,2 +473,1,10,2 +474,0,2,3 +475,2,4,0 +476,1,6,2 +477,0,1,3 +478,2,7,1 +479,2,1,1 +480,0,12,3 +481,2,12,1 +482,2,4,0 +483,1,7,2 +484,1,2,3 +485,0,3,2 +486,0,5,1 +487,2,5,3 +488,0,4,0 +489,1,7,0 +490,1,9,2 +491,0,5,2 +492,1,10,2 +493,2,7,0 +494,1,2,1 +495,1,5,0 +496,2,7,3 +497,1,6,1 +498,0,6,1 +499,1,11,3 +500,0,8,2 +501,1,6,2 +502,0,6,2 +503,0,5,3 +504,1,5,2 +505,1,4,2 +506,2,2,0 +507,0,11,1 +508,1,3,2 +509,0,4,3 +510,1,1,1 +511,2,5,3 diff --git a/crates/lance-graph-quack/tests/duckdb/data/line.csv b/crates/lance-graph-quack/tests/duckdb/data/line.csv new file mode 100644 index 000000000..43dd749ad --- /dev/null +++ b/crates/lance-graph-quack/tests/duckdb/data/line.csv @@ -0,0 +1,4097 @@ +rid,doc_id,partner_id,amount,qty,status,cost_center,gl_account +0,234,12,11692,36,1,4,400000 +1,261,36,4718,19,1,2,410000 +2,336,28,1056,39,1,7,410000 +3,85,3,10830,15,1,2,400000 +4,318,25,17638,43,1,2,400000 +5,352,59,2764,49,0,7,410000 +6,417,6,4795,31,1,2,410000 +7,435,2,8882,12,1,1,600000 +8,308,27,4980,21,0,6,400000 +9,93,49,12904,36,2,7,500000 +10,451,13,9692,42,0,4,600000 +11,365,28,5962,18,1,1,600000 +12,482,25,12055,9,0,1,600000 +13,487,2,18637,30,0,0,600000 +14,322,61,14201,2,1,5,400000 +15,476,58,755,22,1,4,500000 +16,385,36,18491,29,1,2,410000 +17,231,26,10496,13,0,0,600000 +18,218,1,13495,18,1,4,410000 +19,407,42,-4853,15,1,0,410000 +20,388,18,17344,4,1,6,500000 +21,243,29,10433,17,1,3,410000 +22,376,0,-3383,14,2,0,400000 +23,225,53,12923,48,0,0,410000 +24,476,10,-3542,28,1,1,410000 +25,21,42,3200,47,1,4,500000 +26,502,62,17667,31,2,4,410000 +27,71,39,15817,34,0,7,410000 +28,340,23,-179,28,2,1,400000 +29,363,9,5700,45,1,0,410000 +30,48,5,14551,28,0,3,410000 +31,330,34,17642,48,0,0,500000 +32,324,19,16156,42,1,5,410000 +33,217,42,17256,32,1,1,500000 +34,268,0,11891,2,2,6,410000 +35,82,59,6358,27,0,5,410000 +36,195,55,9705,34,1,3,600000 +37,426,17,19721,40,1,1,500000 +38,403,52,9457,26,1,0,410000 +39,150,54,17593,22,1,1,400000 +40,114,5,-3669,6,2,7,500000 +41,473,54,19381,19,1,7,400000 +42,476,0,19007,22,2,7,600000 +43,119,5,7713,4,1,0,410000 +44,346,9,10115,3,0,4,500000 +45,229,17,15066,3,1,2,400000 +46,155,23,-128,9,1,0,410000 +47,465,47,3902,19,1,4,400000 +48,281,47,10750,20,1,1,500000 +49,175,44,11734,7,1,3,500000 +50,445,0,-2794,4,1,4,410000 +51,82,31,17970,13,0,7,400000 +52,357,18,3601,33,0,0,400000 +53,46,37,-815,33,0,0,600000 +54,4,12,-4846,18,1,2,410000 +55,7,35,10316,37,1,4,500000 +56,21,55,16209,11,1,6,600000 +57,104,15,10136,20,1,0,410000 +58,400,63,8773,10,1,3,600000 +59,57,8,19513,14,2,7,500000 +60,260,37,2829,29,1,7,410000 +61,207,61,18410,3,1,0,410000 +62,180,21,18492,5,1,3,500000 +63,496,16,18480,4,1,4,410000 +64,239,50,5947,8,0,5,500000 +65,54,29,19240,19,0,7,400000 +66,347,10,15006,24,1,2,600000 +67,367,39,7603,31,1,2,500000 +68,4,45,-4647,39,0,7,500000 +69,99,30,5817,7,2,6,410000 +70,350,48,14438,30,1,0,400000 +71,179,30,4986,49,1,5,400000 +72,96,4,-4833,5,1,4,400000 +73,327,0,3952,35,1,7,400000 +74,486,42,2897,1,0,6,400000 +75,36,53,4414,17,1,6,600000 +76,301,53,-228,19,1,2,400000 +77,312,59,10319,18,1,7,600000 +78,124,29,17938,18,1,4,500000 +79,344,39,-2648,4,0,1,500000 +80,314,39,9579,23,1,1,400000 +81,129,40,-1382,13,1,6,500000 +82,45,3,5584,41,1,2,500000 +83,3,12,13601,11,1,1,500000 +84,168,62,14756,38,1,0,400000 +85,174,35,11468,18,1,4,500000 +86,337,56,17026,20,1,3,500000 +87,496,1,-4793,4,0,7,600000 +88,166,6,17722,4,2,7,600000 +89,460,8,2660,20,0,1,600000 +90,454,30,129,23,0,4,410000 +91,283,59,71,42,1,0,410000 +92,442,46,7050,31,1,1,500000 +93,90,42,2183,6,1,7,410000 +94,30,60,7410,16,0,3,600000 +95,310,56,5191,23,1,7,600000 +96,419,60,12412,38,1,6,410000 +97,127,35,109,17,1,4,410000 +98,74,60,13462,23,1,2,600000 +99,152,29,-1671,22,0,6,410000 +100,250,44,10362,33,1,1,400000 +101,315,36,-1676,46,1,6,500000 +102,118,5,4829,10,1,6,500000 +103,440,59,18103,40,0,5,410000 +104,415,26,14792,8,1,4,410000 +105,309,20,15212,43,1,2,500000 +106,290,33,-4286,14,1,0,500000 +107,292,46,6215,20,1,2,410000 +108,49,15,-3452,39,1,5,410000 +109,494,5,9269,29,0,6,410000 +110,132,43,3957,37,0,0,400000 +111,333,15,19941,21,2,5,500000 +112,288,13,6281,13,1,1,400000 +113,342,6,2199,48,1,6,600000 +114,7,10,9878,2,1,1,400000 +115,218,33,-3178,46,1,6,500000 +116,314,14,13028,16,2,0,400000 +117,341,22,15595,26,0,7,410000 +118,215,46,4298,12,1,1,410000 +119,25,34,17938,11,1,2,400000 +120,368,58,18084,23,1,0,400000 +121,82,33,809,8,1,5,500000 +122,256,56,1733,34,1,3,600000 +123,240,28,-1304,40,0,2,600000 +124,114,36,-2143,25,1,2,400000 +125,46,36,19171,38,1,0,500000 +126,44,49,319,19,1,6,400000 +127,44,28,3211,6,0,1,500000 +128,94,1,7887,33,1,6,400000 +129,125,40,1914,39,0,5,400000 +130,50,21,3613,40,1,2,410000 +131,52,28,-3226,21,2,4,500000 +132,3,63,5806,27,1,2,500000 +133,349,24,-2906,47,2,1,500000 +134,150,54,-3241,33,1,0,410000 +135,345,32,15428,40,1,6,600000 +136,72,2,652,1,1,0,410000 +137,472,19,-825,45,1,4,400000 +138,302,5,14639,30,1,1,400000 +139,358,33,14990,35,1,1,600000 +140,305,9,-1613,7,2,0,410000 +141,295,46,-615,35,0,7,500000 +142,297,36,16977,48,1,0,400000 +143,282,10,-3387,33,1,1,410000 +144,236,54,17751,8,0,4,410000 +145,79,8,838,5,2,3,410000 +146,415,24,1506,11,1,1,400000 +147,303,30,12872,34,1,6,400000 +148,138,25,14609,22,1,1,500000 +149,491,18,-4327,26,1,0,500000 +150,37,63,11439,33,1,5,600000 +151,184,10,8925,1,0,7,400000 +152,459,39,15612,26,1,0,400000 +153,138,17,8760,31,1,7,400000 +154,1,33,433,38,1,0,600000 +155,317,25,19347,14,0,2,500000 +156,359,26,2963,30,1,7,600000 +157,91,4,12073,20,0,5,600000 +158,255,56,16483,21,1,5,400000 +159,63,15,1545,26,1,7,600000 +160,117,63,-4434,7,0,6,400000 +161,209,43,-2025,24,1,5,500000 +162,24,57,4856,29,2,1,600000 +163,427,29,-2952,19,1,3,400000 +164,31,34,-1693,11,1,5,410000 +165,271,15,-989,24,1,3,410000 +166,52,49,17068,18,1,4,400000 +167,293,61,-1753,25,0,6,500000 +168,286,6,-1,48,1,4,600000 +169,44,30,15128,22,1,0,500000 +170,65,50,13262,17,0,5,400000 +171,82,43,8183,21,1,2,600000 +172,381,36,2116,29,1,3,410000 +173,214,43,5636,44,1,1,410000 +174,164,62,13665,46,1,0,600000 +175,90,7,11069,34,1,3,400000 +176,242,34,18883,23,1,3,500000 +177,147,51,5120,38,1,4,600000 +178,39,33,548,19,1,2,410000 +179,347,40,-2791,14,1,7,410000 +180,88,52,5083,27,1,0,400000 +181,289,30,9679,45,1,6,410000 +182,498,59,-1600,47,1,3,400000 +183,319,32,6084,35,0,6,600000 +184,21,22,13047,11,1,5,400000 +185,239,29,13097,14,1,5,500000 +186,210,16,10557,38,0,4,500000 +187,258,60,1559,2,1,5,400000 +188,0,56,6754,2,1,5,500000 +189,228,41,15222,37,0,1,400000 +190,243,18,-677,17,2,6,500000 +191,389,1,10713,41,1,2,410000 +192,138,48,-2638,6,1,0,410000 +193,363,25,9704,9,0,2,400000 +194,180,32,16267,10,1,7,400000 +195,148,10,4057,16,1,3,400000 +196,193,42,6969,38,1,5,410000 +197,481,52,4726,15,1,6,400000 +198,442,53,-737,15,1,3,400000 +199,423,35,16493,42,1,1,500000 +200,162,4,5191,41,1,4,410000 +201,356,17,16832,29,0,4,600000 +202,31,11,11010,21,0,6,600000 +203,239,58,-4548,33,1,5,600000 +204,317,0,3738,37,2,7,500000 +205,75,54,2327,5,1,6,400000 +206,307,56,2595,22,0,6,600000 +207,269,40,10851,47,1,3,500000 +208,227,2,1832,44,1,2,410000 +209,493,40,-3138,25,1,3,400000 +210,420,35,6390,2,0,3,400000 +211,93,10,16550,26,1,3,410000 +212,358,34,10391,41,1,2,500000 +213,278,61,-179,39,0,5,500000 +214,61,20,19382,37,1,4,400000 +215,222,52,18205,39,1,2,410000 +216,442,31,19756,35,1,3,400000 +217,459,48,3177,49,0,7,500000 +218,345,34,1233,10,0,6,500000 +219,288,32,7795,32,1,2,600000 +220,487,59,18623,27,1,1,500000 +221,291,56,4268,47,1,7,400000 +222,156,46,-4950,45,0,1,600000 +223,504,28,7483,42,0,4,410000 +224,179,56,14125,22,1,3,410000 +225,507,0,1690,11,1,0,500000 +226,65,55,9146,41,1,1,600000 +227,115,39,19523,47,1,4,600000 +228,351,55,-3451,44,1,5,600000 +229,137,27,16956,30,1,7,500000 +230,66,21,16569,19,0,3,400000 +231,252,46,4806,11,0,0,500000 +232,195,29,17911,23,1,6,500000 +233,260,35,1001,14,1,4,410000 +234,303,21,17590,41,1,1,410000 +235,256,23,12505,1,1,1,400000 +236,112,25,-559,1,0,5,600000 +237,385,24,-1408,16,1,1,500000 +238,412,10,-3509,49,1,7,600000 +239,443,31,-1352,49,1,1,410000 +240,265,31,17501,30,1,5,600000 +241,428,26,8349,23,1,0,410000 +242,34,35,14755,1,1,2,410000 +243,156,21,1667,16,1,4,400000 +244,323,29,-3610,35,0,2,410000 +245,9,39,-3965,11,1,1,410000 +246,448,26,9806,29,1,3,600000 +247,137,27,-3458,32,0,1,500000 +248,49,18,-4184,40,1,0,500000 +249,400,39,12144,5,1,2,600000 +250,50,27,-436,19,1,6,410000 +251,173,56,7942,32,0,0,500000 +252,502,34,18631,40,1,1,410000 +253,87,54,1697,30,2,2,400000 +254,362,56,1763,41,0,2,400000 +255,90,51,-4620,37,1,1,400000 +256,163,7,12029,12,1,1,400000 +257,270,17,17357,17,1,3,400000 +258,341,45,7193,44,1,2,410000 +259,124,7,1152,3,0,2,410000 +260,17,4,3081,33,1,0,600000 +261,240,9,449,11,1,1,400000 +262,303,22,17040,31,0,7,600000 +263,126,29,17749,38,1,3,500000 +264,95,20,18617,35,2,3,500000 +265,266,17,12097,3,1,5,500000 +266,132,32,19618,41,1,0,600000 +267,245,43,468,32,1,7,410000 +268,171,33,17832,10,0,4,500000 +269,345,34,619,47,0,1,500000 +270,242,15,5770,21,1,1,410000 +271,368,5,-3722,38,0,1,600000 +272,264,52,19264,5,1,0,410000 +273,507,46,17001,26,1,1,500000 +274,221,13,3772,9,0,3,410000 +275,419,33,18970,4,1,4,500000 +276,66,22,-1398,15,1,5,500000 +277,144,41,918,26,1,7,500000 +278,354,60,4107,18,1,4,410000 +279,249,2,16518,20,2,6,500000 +280,374,7,555,15,2,7,400000 +281,36,31,6563,22,0,0,600000 +282,38,43,4419,23,1,7,600000 +283,132,35,-2130,46,1,5,600000 +284,119,49,3682,11,0,5,600000 +285,416,59,14981,19,1,5,500000 +286,8,52,12159,25,1,3,600000 +287,283,31,16334,29,1,6,410000 +288,68,61,9494,47,1,6,410000 +289,384,24,10317,25,1,5,400000 +290,142,60,11437,2,1,4,410000 +291,16,37,13676,6,1,5,410000 +292,308,36,5545,6,1,6,410000 +293,406,62,4302,26,1,5,500000 +294,367,31,6556,22,2,6,600000 +295,163,25,-122,14,1,4,400000 +296,233,37,2443,39,0,3,410000 +297,259,34,16119,37,0,5,410000 +298,219,18,14962,1,1,5,500000 +299,281,9,9073,14,1,5,500000 +300,427,18,-1545,5,0,1,400000 +301,358,17,-4476,2,2,1,400000 +302,474,58,17912,49,0,1,500000 +303,250,47,-3408,49,1,4,400000 +304,498,48,-1103,3,1,4,600000 +305,199,57,13755,12,0,0,500000 +306,62,14,7915,3,1,3,500000 +307,473,23,13133,6,1,4,500000 +308,467,48,6457,30,1,2,400000 +309,486,16,5384,16,1,1,410000 +310,499,47,9293,35,1,3,600000 +311,496,13,9712,45,2,4,410000 +312,196,36,6407,35,1,5,500000 +313,384,9,14477,32,1,2,400000 +314,239,26,18792,3,1,1,400000 +315,376,18,10798,47,1,1,400000 +316,201,40,5118,2,0,5,410000 +317,7,36,10938,44,2,0,600000 +318,18,35,4876,33,1,4,410000 +319,504,8,2392,43,0,0,600000 +320,386,53,3442,2,1,5,410000 +321,510,55,11107,47,1,1,410000 +322,267,35,15410,3,0,2,400000 +323,332,44,9388,31,1,3,400000 +324,504,27,10232,29,0,6,400000 +325,504,22,-1770,43,2,1,400000 +326,490,22,561,41,0,3,600000 +327,310,34,10567,2,0,1,600000 +328,403,13,-1714,43,1,7,400000 +329,70,6,-3687,38,1,1,500000 +330,492,9,-797,21,2,5,410000 +331,6,47,-3322,27,1,2,410000 +332,355,39,13891,39,2,7,600000 +333,198,36,4829,28,1,3,410000 +334,401,2,10720,29,1,7,500000 +335,31,2,17613,37,1,5,410000 +336,102,22,7758,36,1,0,500000 +337,31,30,11306,46,1,5,400000 +338,376,24,3298,28,0,2,400000 +339,468,6,-3813,45,1,4,410000 +340,86,47,12235,2,0,4,410000 +341,308,32,8115,46,1,6,600000 +342,189,0,6329,27,2,7,410000 +343,402,6,542,31,1,3,400000 +344,377,23,-1814,10,1,3,600000 +345,163,10,3611,35,0,2,400000 +346,168,61,10029,10,1,2,500000 +347,374,47,4325,8,0,2,400000 +348,471,16,-3447,8,1,7,500000 +349,33,52,673,11,0,2,410000 +350,448,39,7482,15,0,6,600000 +351,344,21,13151,7,1,7,410000 +352,237,51,2943,36,1,1,400000 +353,215,7,7261,3,0,2,600000 +354,139,40,7122,32,1,2,600000 +355,27,30,16898,47,0,1,500000 +356,137,40,3592,38,0,5,400000 +357,391,4,15446,20,0,4,400000 +358,330,2,17743,26,0,4,600000 +359,423,10,4754,34,1,5,400000 +360,124,21,11825,49,1,1,410000 +361,208,17,835,22,1,6,600000 +362,447,38,-394,49,1,2,410000 +363,71,36,8868,49,1,4,600000 +364,267,52,5606,30,1,4,410000 +365,235,1,10211,8,0,6,600000 +366,470,41,13823,25,1,7,600000 +367,410,15,8901,7,1,7,600000 +368,451,8,19880,44,1,2,400000 +369,69,59,7422,36,2,2,410000 +370,348,40,18611,14,1,5,410000 +371,193,56,3147,15,1,1,600000 +372,319,14,16308,2,2,4,600000 +373,398,23,10605,28,1,1,500000 +374,376,60,16991,32,0,3,600000 +375,460,6,1336,8,1,3,410000 +376,106,8,3725,35,1,3,410000 +377,257,16,7036,47,1,6,500000 +378,219,22,-1367,46,0,6,500000 +379,191,34,12956,22,1,3,410000 +380,493,18,-3543,35,1,6,500000 +381,338,30,-3521,47,0,6,400000 +382,291,30,6038,2,1,5,410000 +383,466,48,8317,37,1,7,600000 +384,74,61,14314,21,1,2,410000 +385,141,22,3607,12,2,5,500000 +386,181,61,4987,19,0,5,410000 +387,45,41,19790,26,0,5,410000 +388,217,24,7625,10,2,0,500000 +389,32,5,10233,18,1,4,600000 +390,65,3,-1316,4,2,7,410000 +391,379,23,5186,34,1,2,410000 +392,421,17,3575,37,1,5,500000 +393,134,13,-3920,29,1,7,400000 +394,158,36,14307,42,1,0,600000 +395,406,33,6250,14,1,5,400000 +396,44,15,18467,35,1,0,410000 +397,485,16,19231,35,1,5,410000 +398,287,57,11571,8,1,2,500000 +399,149,10,5742,30,1,5,500000 +400,399,13,-4292,28,1,0,600000 +401,157,63,-3383,25,1,3,410000 +402,89,55,6057,13,1,1,400000 +403,295,63,2626,23,0,1,500000 +404,404,0,-35,43,1,3,500000 +405,23,11,-343,25,1,5,600000 +406,328,56,10763,41,0,1,600000 +407,394,48,15458,21,1,4,400000 +408,120,58,15657,24,1,0,600000 +409,62,41,9066,44,1,6,500000 +410,81,56,-4624,45,1,5,600000 +411,41,11,15423,28,0,7,410000 +412,146,63,-4094,14,1,4,600000 +413,132,26,12252,42,1,0,400000 +414,306,62,1995,47,1,6,410000 +415,149,62,10791,22,0,5,600000 +416,160,61,6509,2,0,3,500000 +417,265,12,7146,20,1,3,600000 +418,319,1,14662,41,1,5,500000 +419,54,8,12926,34,1,6,400000 +420,22,8,2694,26,1,4,500000 +421,160,36,8476,29,2,1,400000 +422,363,53,10092,45,1,2,400000 +423,218,34,13676,14,1,2,500000 +424,452,62,-3773,34,1,5,500000 +425,274,55,2152,45,1,3,600000 +426,88,2,19713,16,1,7,400000 +427,282,61,5810,34,0,1,410000 +428,341,16,5469,8,2,2,410000 +429,229,3,4061,49,1,4,500000 +430,29,39,10504,34,1,3,600000 +431,409,53,9167,39,0,7,400000 +432,402,52,4918,47,1,2,410000 +433,242,14,-1135,25,1,6,500000 +434,451,50,19234,42,1,2,500000 +435,297,28,14381,31,1,2,600000 +436,450,2,12932,20,1,2,400000 +437,22,58,9567,30,0,7,400000 +438,79,13,2372,36,0,5,500000 +439,17,44,-3347,17,2,3,410000 +440,294,63,12285,22,1,6,500000 +441,32,43,-3612,1,1,1,600000 +442,71,60,-705,24,0,2,410000 +443,338,4,7989,7,1,4,410000 +444,183,53,14385,20,1,1,600000 +445,109,9,12282,34,1,2,400000 +446,409,16,5543,17,0,6,600000 +447,132,43,116,7,1,1,400000 +448,427,8,-3229,43,2,5,600000 +449,193,30,11310,48,0,5,400000 +450,316,54,10183,15,0,2,410000 +451,52,24,-177,23,1,6,600000 +452,65,13,19401,31,1,4,410000 +453,401,60,2262,21,1,5,600000 +454,204,23,11682,46,0,6,400000 +455,79,47,7822,18,2,0,400000 +456,183,8,-3705,47,1,7,400000 +457,135,56,19768,39,1,7,400000 +458,482,44,1394,40,0,3,400000 +459,77,6,11870,20,1,6,500000 +460,46,26,15882,37,0,7,400000 +461,208,26,-3012,27,0,0,600000 +462,435,47,7251,38,1,1,500000 +463,199,22,1450,39,1,1,600000 +464,505,15,-2021,19,2,4,410000 +465,367,26,13890,40,1,5,400000 +466,401,23,9703,11,1,6,600000 +467,378,5,17604,43,1,2,410000 +468,251,14,4702,31,1,5,400000 +469,234,12,92,31,0,6,600000 +470,109,54,14858,35,1,0,400000 +471,297,18,2159,31,0,7,400000 +472,189,48,12697,47,1,1,410000 +473,318,28,9762,8,1,2,410000 +474,4,12,-3946,46,0,3,410000 +475,391,60,185,49,1,2,400000 +476,509,35,6820,41,1,6,500000 +477,170,26,-2665,49,1,3,400000 +478,30,35,13736,18,2,1,410000 +479,19,40,16000,28,1,6,410000 +480,333,50,17598,47,1,6,500000 +481,458,36,11686,18,2,7,500000 +482,260,14,-1651,32,0,5,410000 +483,418,58,3899,43,0,3,410000 +484,475,14,18228,14,0,0,400000 +485,164,23,9268,15,1,2,600000 +486,336,7,12388,41,1,3,400000 +487,328,4,12104,41,2,0,400000 +488,472,20,12254,42,0,2,500000 +489,184,15,10183,34,1,1,500000 +490,185,19,13578,11,1,5,600000 +491,498,37,11290,14,0,2,410000 +492,487,43,8044,45,1,0,500000 +493,325,12,9831,34,1,2,600000 +494,230,10,16789,19,1,6,600000 +495,183,5,16716,37,1,2,600000 +496,126,19,12313,20,1,2,400000 +497,449,54,12220,12,1,5,410000 +498,431,11,2366,28,2,5,500000 +499,414,4,17357,42,1,3,400000 +500,291,7,9300,35,1,0,410000 +501,64,24,2960,32,1,1,600000 +502,138,40,-2685,41,1,2,600000 +503,481,42,-3280,21,1,7,500000 +504,4,25,4097,25,1,1,410000 +505,404,26,12148,48,1,4,600000 +506,319,6,-3255,10,0,4,400000 +507,130,51,18339,44,1,6,600000 +508,442,27,16847,20,1,3,500000 +509,146,23,17115,29,2,3,600000 +510,459,41,3162,28,1,6,410000 +511,140,18,5767,24,1,7,400000 +512,14,47,-3851,32,0,4,600000 +513,445,11,14500,39,2,3,410000 +514,317,22,11053,36,1,1,410000 +515,115,13,15444,13,2,4,410000 +516,294,9,19133,31,1,1,500000 +517,263,45,14756,35,1,2,400000 +518,376,32,4597,28,1,0,600000 +519,293,1,3605,21,1,1,600000 +520,509,39,-2864,27,1,3,600000 +521,99,45,-1808,14,2,1,410000 +522,227,39,12849,26,1,1,410000 +523,305,34,14791,2,1,7,600000 +524,40,33,-1146,18,1,2,400000 +525,44,22,8173,43,1,6,410000 +526,155,28,-1128,49,1,2,600000 +527,287,7,9375,40,2,4,500000 +528,123,27,13744,36,1,0,400000 +529,197,30,-2279,29,2,2,600000 +530,316,7,6444,30,1,6,410000 +531,417,56,3302,12,1,5,410000 +532,109,30,-3345,26,1,6,600000 +533,467,55,-4494,3,1,7,500000 +534,36,57,1373,8,1,5,410000 +535,30,44,13865,36,0,3,410000 +536,451,42,16420,16,1,0,600000 +537,100,57,7408,23,1,0,400000 +538,474,56,-882,26,1,4,500000 +539,278,13,15741,29,1,5,500000 +540,84,37,12526,2,1,2,400000 +541,209,63,-2426,12,0,2,600000 +542,474,44,15815,7,1,0,500000 +543,153,40,4771,40,1,1,500000 +544,90,8,9223,42,2,7,500000 +545,248,40,-1568,24,2,3,400000 +546,435,19,8711,7,2,6,410000 +547,469,13,10054,15,1,0,500000 +548,409,33,17383,28,1,7,500000 +549,299,40,3257,2,1,5,600000 +550,287,11,-1098,30,1,1,410000 +551,492,2,15439,41,2,0,500000 +552,132,45,3422,2,2,2,400000 +553,49,21,19188,10,0,2,500000 +554,207,45,583,15,1,3,500000 +555,389,54,7387,3,0,3,500000 +556,398,11,3128,47,1,0,500000 +557,5,63,4037,2,0,4,600000 +558,275,40,7849,34,1,2,600000 +559,326,0,-1484,4,1,6,500000 +560,322,50,5288,8,1,7,600000 +561,204,8,19855,45,1,5,410000 +562,325,15,15186,8,1,2,410000 +563,130,45,6676,44,1,7,500000 +564,401,45,19006,27,0,1,400000 +565,177,54,9291,2,1,7,400000 +566,349,61,12351,44,1,6,500000 +567,504,24,-3057,45,0,4,500000 +568,349,45,6110,14,1,1,500000 +569,342,1,9558,32,1,3,400000 +570,236,41,17805,3,1,3,400000 +571,254,49,-3635,45,1,6,600000 +572,348,58,18054,40,1,5,400000 +573,280,61,16458,19,1,6,410000 +574,67,45,-2473,25,1,1,400000 +575,149,19,-4941,22,1,6,400000 +576,462,20,2939,33,1,6,500000 +577,401,18,16836,32,1,6,400000 +578,482,23,14884,16,0,4,400000 +579,310,13,-3662,33,1,0,600000 +580,31,42,6897,3,1,2,400000 +581,115,38,9441,22,1,1,410000 +582,435,36,16807,15,0,7,500000 +583,490,37,382,24,1,2,500000 +584,492,44,18551,10,1,4,600000 +585,120,19,10647,47,1,0,600000 +586,486,54,2166,36,2,2,410000 +587,57,3,6074,22,1,0,400000 +588,505,1,4556,17,1,4,410000 +589,382,37,11094,27,2,6,400000 +590,464,46,12388,33,1,5,400000 +591,312,41,-741,6,0,1,600000 +592,308,27,-1478,15,1,1,500000 +593,135,41,2444,46,1,3,400000 +594,138,58,19143,47,1,3,400000 +595,147,8,18027,17,2,0,400000 +596,479,8,-3531,43,2,2,600000 +597,473,31,17758,6,1,1,500000 +598,314,42,19077,20,1,3,410000 +599,340,6,19064,18,1,3,500000 +600,299,14,13264,11,1,7,600000 +601,63,34,19130,25,1,0,400000 +602,290,29,16848,3,1,6,400000 +603,259,52,3348,6,0,2,600000 +604,30,6,13653,48,1,4,410000 +605,360,33,7376,33,1,5,400000 +606,208,24,8893,7,0,2,400000 +607,79,0,-1654,17,1,4,600000 +608,26,15,6812,2,0,2,500000 +609,272,45,19581,37,1,2,500000 +610,352,63,7862,33,1,3,500000 +611,75,50,17032,12,2,2,410000 +612,290,56,13622,41,1,7,400000 +613,254,27,-3746,42,1,5,400000 +614,288,49,4854,2,2,1,410000 +615,112,27,18908,46,1,3,400000 +616,251,15,16681,49,1,0,400000 +617,123,8,3003,2,1,5,500000 +618,24,62,-4140,49,1,3,500000 +619,129,29,16221,48,1,7,410000 +620,278,0,-3954,5,1,0,410000 +621,491,58,14723,45,1,0,500000 +622,395,6,-2765,11,0,1,410000 +623,402,19,5574,38,2,5,500000 +624,448,56,-1462,10,1,5,500000 +625,376,13,11578,40,1,1,400000 +626,416,38,11933,43,2,0,500000 +627,332,63,9573,30,1,2,410000 +628,281,24,-2873,41,1,3,600000 +629,321,47,11089,39,1,3,400000 +630,320,31,405,36,2,5,410000 +631,376,35,2173,11,0,4,400000 +632,15,16,-4118,18,1,7,400000 +633,123,42,5965,10,1,2,500000 +634,386,37,9862,34,1,4,500000 +635,91,28,11226,13,2,5,410000 +636,126,13,9720,35,1,5,600000 +637,12,44,-4148,18,1,6,600000 +638,26,25,9177,10,1,3,410000 +639,462,27,3569,8,1,3,410000 +640,299,21,-1897,12,1,4,400000 +641,377,22,6064,22,1,0,410000 +642,310,60,8535,9,0,7,400000 +643,249,22,-2597,4,1,1,500000 +644,168,27,14376,12,1,7,600000 +645,63,62,748,43,0,0,410000 +646,407,36,-4598,36,1,4,410000 +647,136,18,17554,46,0,6,410000 +648,342,21,3368,16,2,3,410000 +649,176,7,-4679,3,1,1,500000 +650,336,18,14841,46,1,3,500000 +651,458,35,12395,36,1,1,400000 +652,501,7,-2412,6,1,4,600000 +653,458,60,-1309,13,2,3,600000 +654,376,55,9252,8,1,7,500000 +655,296,32,11272,17,1,5,500000 +656,333,39,8019,28,2,7,410000 +657,481,17,19281,36,2,6,500000 +658,263,10,11870,24,1,5,400000 +659,432,59,-1480,45,0,0,500000 +660,482,15,16492,14,0,6,500000 +661,290,18,7769,28,1,2,400000 +662,364,5,-186,34,2,0,600000 +663,284,11,15052,30,1,0,500000 +664,323,56,8402,45,2,0,410000 +665,381,40,-1015,46,2,1,410000 +666,54,48,10408,32,1,2,500000 +667,145,40,2128,36,1,7,410000 +668,17,48,-4314,12,2,6,400000 +669,18,36,2489,47,0,5,410000 +670,178,26,6170,41,1,2,410000 +671,355,49,5919,7,1,1,410000 +672,128,56,2596,42,1,3,400000 +673,363,31,15621,45,0,6,600000 +674,409,23,18989,5,0,6,410000 +675,375,12,-2481,13,1,3,600000 +676,269,7,6381,12,0,2,500000 +677,112,19,10812,24,1,0,500000 +678,132,7,8312,23,1,7,410000 +679,359,43,12407,35,1,0,500000 +680,119,34,3492,15,1,6,600000 +681,215,23,8305,20,1,7,500000 +682,210,26,19614,18,0,3,500000 +683,33,59,5863,12,1,0,400000 +684,133,19,19109,30,1,1,500000 +685,313,60,17725,21,1,2,410000 +686,460,61,601,15,2,3,410000 +687,479,35,5681,15,0,6,410000 +688,175,52,-1548,24,1,4,600000 +689,231,41,16593,23,0,0,500000 +690,359,21,2829,49,1,1,400000 +691,295,4,10076,22,1,1,410000 +692,96,13,15509,34,1,4,410000 +693,53,18,3864,1,1,1,500000 +694,210,56,381,36,1,1,410000 +695,125,30,8130,12,1,6,600000 +696,102,13,8885,21,2,5,410000 +697,181,35,19032,10,0,6,500000 +698,297,32,17352,10,1,1,600000 +699,109,47,13329,48,2,1,410000 +700,356,41,1197,20,1,4,500000 +701,417,33,19641,39,2,4,500000 +702,62,37,3873,24,2,3,400000 +703,383,36,16991,1,1,1,410000 +704,467,56,10365,34,1,3,410000 +705,209,51,17072,32,1,3,600000 +706,298,29,4824,33,2,4,600000 +707,447,20,12209,47,1,4,600000 +708,258,48,4492,21,1,6,400000 +709,432,31,17607,46,1,6,410000 +710,359,55,5665,8,1,0,410000 +711,324,4,9240,38,2,6,400000 +712,449,11,19058,33,0,6,410000 +713,363,10,9410,18,2,1,600000 +714,285,37,3766,9,1,2,500000 +715,26,48,8228,6,1,2,600000 +716,75,63,5597,26,0,2,410000 +717,129,13,13688,44,1,5,410000 +718,391,25,11087,4,1,6,600000 +719,227,49,13345,16,0,6,410000 +720,384,46,-1953,24,1,5,500000 +721,412,8,14178,49,1,6,500000 +722,254,55,14602,15,1,7,600000 +723,481,17,-4997,3,1,3,500000 +724,358,1,-4391,34,1,5,600000 +725,67,30,7260,42,1,0,410000 +726,287,52,8170,19,1,2,500000 +727,149,26,1296,41,1,1,410000 +728,483,10,14030,33,1,3,410000 +729,49,30,5115,11,0,7,500000 +730,21,43,5450,11,2,0,400000 +731,24,3,8201,19,1,0,600000 +732,483,30,19897,42,1,5,500000 +733,105,59,9427,48,1,1,400000 +734,19,36,7489,38,1,4,400000 +735,166,60,-2883,11,1,6,410000 +736,193,27,10705,14,1,3,400000 +737,189,31,16774,36,1,5,600000 +738,428,60,1307,9,1,1,410000 +739,121,46,-4919,20,1,5,400000 +740,317,0,5594,7,0,1,410000 +741,351,7,7378,7,1,4,500000 +742,117,20,-4937,34,1,7,410000 +743,381,35,12334,38,1,4,500000 +744,69,17,1603,40,1,1,400000 +745,138,33,10861,32,0,4,400000 +746,10,61,17535,36,1,4,400000 +747,428,43,-2195,44,1,3,600000 +748,369,43,8657,2,2,0,600000 +749,431,6,4391,47,0,7,600000 +750,221,41,17141,26,1,0,600000 +751,76,46,-4850,12,1,7,400000 +752,236,47,14349,35,1,0,410000 +753,61,54,15073,27,1,4,410000 +754,160,49,9036,12,1,3,600000 +755,194,29,12040,24,1,3,500000 +756,236,7,7657,38,1,3,500000 +757,360,29,11600,23,1,6,400000 +758,482,15,12613,8,0,5,600000 +759,345,17,-1357,15,1,6,600000 +760,378,25,3237,11,1,4,400000 +761,238,4,10727,45,1,7,400000 +762,380,47,69,41,0,7,500000 +763,319,58,3879,45,2,1,600000 +764,192,51,12092,47,2,5,410000 +765,210,5,14492,37,1,7,410000 +766,234,27,17755,26,1,1,410000 +767,288,30,17474,35,0,6,500000 +768,338,25,9393,16,1,4,400000 +769,97,58,11319,31,1,1,600000 +770,366,53,13253,47,2,2,500000 +771,98,18,2588,8,1,0,500000 +772,89,0,14754,49,1,0,410000 +773,352,30,1747,23,1,1,400000 +774,246,63,-2628,15,1,7,410000 +775,231,43,16974,38,2,1,600000 +776,228,15,16808,34,2,6,410000 +777,172,43,14643,28,2,1,400000 +778,381,14,9271,34,1,1,410000 +779,296,16,-2539,36,0,2,410000 +780,167,2,8769,41,1,6,400000 +781,0,3,12279,2,1,4,400000 +782,444,12,9305,13,2,6,410000 +783,123,17,-834,31,1,0,600000 +784,201,58,2068,46,0,1,400000 +785,55,35,9295,29,0,2,500000 +786,235,1,7458,47,1,0,410000 +787,275,15,-4964,16,1,3,400000 +788,216,42,-350,40,1,5,400000 +789,326,47,4423,28,1,0,500000 +790,261,41,-3306,27,1,5,400000 +791,489,19,4911,19,1,7,600000 +792,438,17,-1052,10,1,7,600000 +793,472,21,6778,45,2,0,400000 +794,335,0,19051,8,0,2,400000 +795,203,38,2336,3,0,1,410000 +796,309,31,-2351,21,1,0,410000 +797,12,7,-1477,7,1,6,500000 +798,187,0,-3661,6,1,4,500000 +799,226,12,14380,5,1,5,600000 +800,508,44,11317,22,1,5,500000 +801,123,50,13253,37,1,7,410000 +802,499,9,9491,48,1,7,600000 +803,128,2,-4744,7,0,7,500000 +804,215,43,10698,19,1,6,500000 +805,127,15,8269,1,2,1,600000 +806,497,38,7375,24,1,0,500000 +807,418,24,17740,47,1,0,400000 +808,146,58,6736,6,1,0,500000 +809,203,63,1640,7,1,7,600000 +810,63,36,5150,31,1,1,410000 +811,473,18,6917,32,1,3,410000 +812,368,23,19351,41,1,4,400000 +813,451,42,13734,47,1,7,410000 +814,413,2,18517,12,1,7,400000 +815,472,63,-1125,12,1,3,400000 +816,256,31,13845,32,1,1,410000 +817,51,17,-1236,21,1,2,600000 +818,175,14,-4911,20,1,5,410000 +819,66,28,19692,12,0,4,410000 +820,35,18,9665,31,0,2,410000 +821,165,6,8718,35,1,0,600000 +822,144,39,14097,49,0,1,410000 +823,448,5,-1663,29,0,6,400000 +824,142,56,-2790,13,0,7,410000 +825,300,20,-2399,25,0,2,600000 +826,87,26,19762,17,0,4,500000 +827,79,44,17882,34,1,7,500000 +828,407,25,10267,14,2,0,500000 +829,490,15,8668,6,1,7,400000 +830,208,20,10854,47,1,6,500000 +831,410,34,12712,25,0,4,500000 +832,270,54,13615,36,2,1,400000 +833,345,52,7355,14,2,3,410000 +834,130,16,-155,1,1,7,500000 +835,129,44,11298,38,0,1,410000 +836,12,32,10913,29,1,6,400000 +837,391,19,13122,32,0,7,400000 +838,210,33,18994,1,1,5,400000 +839,274,28,-234,30,1,7,500000 +840,475,63,-2767,37,1,5,500000 +841,173,0,-2409,23,2,0,500000 +842,345,25,13836,41,1,1,500000 +843,262,30,-2618,15,1,0,410000 +844,285,4,10740,43,1,4,600000 +845,467,35,11349,27,2,5,400000 +846,73,10,3805,13,1,7,500000 +847,406,26,9751,26,1,3,400000 +848,480,28,19665,24,1,1,500000 +849,312,16,13778,33,0,2,400000 +850,245,41,3666,23,0,2,600000 +851,326,8,14718,32,0,7,410000 +852,236,4,770,34,2,2,400000 +853,123,7,3583,49,0,2,500000 +854,79,53,-1483,36,1,5,410000 +855,399,51,16370,4,1,5,400000 +856,73,38,1870,40,1,5,600000 +857,31,49,-1296,41,0,6,400000 +858,144,21,19070,20,1,6,600000 +859,320,22,10398,8,1,2,410000 +860,344,32,-2654,19,1,6,600000 +861,237,51,19980,16,2,5,410000 +862,405,11,3142,21,1,1,500000 +863,342,15,9334,11,1,6,400000 +864,16,2,15373,26,1,3,600000 +865,68,62,4802,18,1,0,600000 +866,426,44,4372,41,1,6,500000 +867,392,14,3649,44,1,4,600000 +868,142,26,8636,48,0,7,400000 +869,11,5,-1169,9,1,0,600000 +870,232,6,4528,48,1,0,500000 +871,466,40,19980,1,1,2,400000 +872,47,25,9285,44,1,4,600000 +873,127,48,7843,24,1,0,410000 +874,73,57,6492,14,1,3,600000 +875,455,39,4671,37,1,4,410000 +876,293,22,-4320,40,1,4,600000 +877,69,58,12957,42,0,7,400000 +878,157,10,7509,13,2,6,400000 +879,110,34,-4066,41,1,7,600000 +880,427,17,-392,21,1,7,410000 +881,173,56,11618,45,0,2,600000 +882,400,28,2688,1,1,3,400000 +883,480,9,5443,16,1,2,400000 +884,314,14,6063,38,1,0,500000 +885,259,43,14837,20,1,5,410000 +886,345,54,13408,4,0,7,410000 +887,126,17,-1644,28,2,0,500000 +888,288,1,53,18,1,5,410000 +889,395,25,2366,28,2,7,410000 +890,462,18,-3804,27,1,3,500000 +891,273,11,6678,48,2,6,500000 +892,353,4,-3688,1,1,7,410000 +893,217,15,13021,13,0,7,400000 +894,365,7,1986,5,1,1,500000 +895,466,62,7035,42,1,1,600000 +896,138,52,2510,1,1,1,400000 +897,257,49,8814,23,1,3,410000 +898,113,20,18624,36,2,2,400000 +899,197,21,2968,43,1,5,600000 +900,295,35,16625,4,0,7,600000 +901,359,56,6079,24,1,0,410000 +902,237,47,10652,30,0,3,500000 +903,440,48,16301,25,1,4,600000 +904,366,11,4971,26,1,0,400000 +905,151,21,951,16,1,7,400000 +906,127,9,10906,40,1,6,500000 +907,415,54,-2988,12,1,5,600000 +908,80,36,12702,24,0,4,600000 +909,410,23,9378,1,1,2,600000 +910,7,18,13250,44,0,5,410000 +911,59,1,-1989,5,1,7,500000 +912,31,15,14438,12,1,4,600000 +913,67,31,13899,43,2,4,500000 +914,353,24,14673,38,0,4,500000 +915,228,38,6988,17,1,6,400000 +916,179,17,-4359,39,1,5,500000 +917,213,55,2817,40,0,3,600000 +918,389,9,12633,27,1,3,410000 +919,118,5,11619,3,1,0,600000 +920,47,41,1542,25,1,6,400000 +921,93,43,17646,46,1,5,600000 +922,160,36,12389,36,0,2,400000 +923,38,59,-2838,9,1,0,500000 +924,421,47,18849,17,1,4,410000 +925,483,55,16001,43,1,2,500000 +926,460,56,10709,34,1,6,400000 +927,407,48,3367,23,1,0,400000 +928,262,27,-1808,44,2,5,410000 +929,292,13,-291,18,1,0,600000 +930,142,4,19080,33,1,2,400000 +931,181,54,6690,29,1,5,600000 +932,414,25,4800,22,1,1,410000 +933,140,28,15041,8,1,5,600000 +934,462,63,-4538,27,1,4,400000 +935,433,20,1003,23,1,5,400000 +936,291,31,2978,23,1,5,410000 +937,292,57,-1051,26,1,5,400000 +938,206,29,10333,47,1,5,400000 +939,158,33,6652,47,1,2,600000 +940,138,40,12412,30,1,5,500000 +941,301,56,-1507,18,1,0,400000 +942,193,22,-4821,40,1,3,410000 +943,313,51,7369,48,1,7,410000 +944,107,52,11296,38,2,1,500000 +945,43,51,17143,27,1,5,600000 +946,218,41,6472,17,1,4,410000 +947,35,0,8713,20,0,3,500000 +948,205,14,15112,29,0,4,410000 +949,206,2,19035,32,1,2,500000 +950,21,26,11240,26,1,0,400000 +951,34,31,9131,11,1,7,400000 +952,51,14,9075,14,1,1,500000 +953,438,51,-2771,29,2,3,400000 +954,362,6,14203,49,2,3,410000 +955,277,14,420,20,2,1,500000 +956,161,34,16998,13,1,5,400000 +957,18,58,-787,28,1,0,410000 +958,369,42,19998,5,1,6,410000 +959,413,16,260,24,1,1,400000 +960,205,26,9200,5,1,2,500000 +961,463,31,3440,42,1,2,500000 +962,69,31,17960,13,1,0,600000 +963,190,36,9976,44,2,1,400000 +964,487,29,3738,39,1,7,410000 +965,111,36,9666,18,1,7,600000 +966,485,1,-3112,32,2,2,600000 +967,109,34,10780,35,2,5,600000 +968,483,39,8820,1,2,2,600000 +969,325,24,-4534,48,2,5,400000 +970,227,11,9611,22,1,7,410000 +971,250,42,11619,34,1,0,600000 +972,223,20,-2500,7,1,1,600000 +973,44,49,15749,26,1,2,410000 +974,366,40,19044,41,1,1,400000 +975,344,54,6207,7,0,4,500000 +976,160,16,6631,6,1,0,600000 +977,353,52,13840,15,1,1,410000 +978,488,12,4571,29,0,0,400000 +979,212,62,1783,4,1,5,600000 +980,305,23,12365,8,1,0,600000 +981,228,10,10891,8,1,0,410000 +982,125,36,-4289,30,1,0,410000 +983,27,11,19935,35,1,6,500000 +984,227,41,5637,22,1,1,500000 +985,283,30,17402,19,1,2,400000 +986,309,51,444,20,1,1,600000 +987,454,35,8666,21,1,7,500000 +988,429,37,2396,18,1,1,600000 +989,430,24,-472,42,1,5,410000 +990,329,8,8164,29,1,4,400000 +991,129,20,19953,17,1,2,410000 +992,152,59,4136,36,0,1,500000 +993,315,12,-1255,31,1,0,600000 +994,138,18,17662,32,1,3,410000 +995,104,59,-578,12,1,5,500000 +996,142,51,12943,22,0,1,500000 +997,391,21,-3547,35,0,3,600000 +998,496,35,-4779,14,1,2,500000 +999,194,20,10208,2,1,7,400000 +1000,373,3,15918,10,2,7,500000 +1001,496,30,-4555,35,1,3,410000 +1002,247,31,13156,44,1,2,600000 +1003,313,36,16124,41,1,2,500000 +1004,276,47,7541,43,1,2,400000 +1005,113,47,490,2,2,4,600000 +1006,27,10,17833,42,1,2,600000 +1007,428,58,18688,26,1,1,400000 +1008,356,53,9092,41,1,6,400000 +1009,358,43,-1643,1,1,1,410000 +1010,337,37,7166,35,1,7,600000 +1011,462,62,16786,2,1,5,600000 +1012,438,35,6739,40,1,7,500000 +1013,377,1,-563,45,0,6,600000 +1014,357,21,11931,36,1,0,400000 +1015,439,6,15203,29,1,5,400000 +1016,405,35,-4965,48,1,2,500000 +1017,28,15,11063,44,2,4,410000 +1018,341,38,9718,11,0,7,410000 +1019,407,54,14654,36,1,6,600000 +1020,43,15,18003,24,1,5,600000 +1021,295,7,4942,47,1,5,400000 +1022,176,58,18476,3,0,5,600000 +1023,392,33,7406,4,1,0,400000 +1024,234,7,8090,6,0,3,500000 +1025,423,44,12895,5,0,2,500000 +1026,428,6,15513,33,1,7,410000 +1027,389,55,8882,29,1,6,600000 +1028,421,46,19444,35,1,2,410000 +1029,241,20,18585,2,0,4,600000 +1030,315,22,9441,42,1,2,600000 +1031,249,18,10522,45,0,4,400000 +1032,239,13,16438,31,0,7,410000 +1033,251,49,5950,19,0,7,600000 +1034,371,26,14211,49,1,4,600000 +1035,168,38,8170,8,1,4,400000 +1036,371,46,16240,6,0,4,600000 +1037,335,12,3291,35,0,5,600000 +1038,149,15,16003,13,0,0,410000 +1039,341,49,4932,46,1,3,410000 +1040,230,26,8428,35,1,4,600000 +1041,295,59,18418,44,1,5,410000 +1042,6,33,3665,6,1,6,410000 +1043,425,12,17830,28,1,0,400000 +1044,442,43,7100,38,1,5,600000 +1045,454,21,2868,2,1,2,500000 +1046,396,53,8339,40,0,7,600000 +1047,208,28,12317,15,1,0,410000 +1048,177,19,3814,22,1,2,410000 +1049,434,10,12603,49,1,2,600000 +1050,242,62,678,11,1,1,410000 +1051,394,38,882,4,1,4,400000 +1052,40,61,8924,34,2,6,410000 +1053,304,61,1000,45,0,5,600000 +1054,266,16,-3438,37,2,4,500000 +1055,75,6,14315,30,1,5,410000 +1056,387,12,2901,10,0,5,410000 +1057,33,27,14551,48,0,1,410000 +1058,87,14,10545,20,1,6,400000 +1059,456,22,18712,1,1,0,410000 +1060,382,42,11939,24,1,0,600000 +1061,401,63,14898,3,0,7,400000 +1062,243,47,13668,40,2,3,500000 +1063,198,36,12310,6,1,6,600000 +1064,452,35,17168,25,0,4,400000 +1065,495,44,-3054,33,0,4,600000 +1066,183,60,14814,39,0,7,600000 +1067,13,43,-4295,12,1,0,600000 +1068,333,30,13588,12,1,0,500000 +1069,414,32,5309,28,0,0,400000 +1070,144,59,19135,2,0,2,410000 +1071,222,33,3448,42,1,0,400000 +1072,381,63,19061,47,1,5,600000 +1073,7,11,14300,15,1,2,500000 +1074,353,44,16971,3,1,1,500000 +1075,157,51,8091,11,1,3,400000 +1076,263,58,-4617,11,1,2,410000 +1077,479,28,18364,45,1,7,600000 +1078,124,12,-4027,18,1,5,500000 +1079,174,61,18660,29,1,0,400000 +1080,8,11,3315,43,1,5,410000 +1081,208,10,6310,16,1,6,500000 +1082,505,48,6929,28,1,7,500000 +1083,407,29,-2842,12,1,6,600000 +1084,207,1,6947,3,1,6,500000 +1085,410,8,-3963,29,1,3,600000 +1086,96,47,7266,20,1,5,500000 +1087,357,48,15093,24,1,5,600000 +1088,382,54,-842,17,0,3,600000 +1089,200,26,8756,37,1,5,410000 +1090,421,56,10094,1,2,0,600000 +1091,315,21,12157,1,0,4,400000 +1092,56,33,18439,33,2,6,500000 +1093,460,60,-4773,36,1,2,600000 +1094,484,61,1282,47,1,7,400000 +1095,310,23,14041,21,1,4,400000 +1096,106,41,14758,32,1,1,500000 +1097,285,32,80,36,0,6,600000 +1098,31,8,3263,30,1,3,410000 +1099,271,45,-2235,32,1,2,600000 +1100,244,23,7757,18,1,1,500000 +1101,126,27,5623,13,1,6,400000 +1102,103,55,12787,14,1,1,400000 +1103,321,34,-4645,25,1,7,500000 +1104,249,1,7515,2,1,5,500000 +1105,3,42,15458,28,0,1,400000 +1106,490,11,-2163,7,0,2,400000 +1107,146,42,11727,41,2,0,410000 +1108,256,24,-1886,13,0,5,400000 +1109,482,21,10489,26,1,4,400000 +1110,233,37,16505,41,1,2,500000 +1111,373,46,17688,7,1,3,500000 +1112,178,56,15295,44,1,0,410000 +1113,169,62,-3113,42,0,5,600000 +1114,102,23,-2418,25,1,5,410000 +1115,352,31,7322,40,1,6,410000 +1116,122,20,14157,28,1,0,600000 +1117,229,47,8504,6,1,0,500000 +1118,309,32,-2884,41,1,4,410000 +1119,266,50,17486,35,1,4,400000 +1120,29,47,19469,18,1,3,400000 +1121,499,38,4964,32,1,0,400000 +1122,407,38,13756,1,0,1,410000 +1123,327,1,8535,8,1,6,500000 +1124,415,28,1866,46,0,5,410000 +1125,273,26,-1206,19,1,4,400000 +1126,49,21,17884,34,1,6,400000 +1127,62,25,11493,5,1,7,500000 +1128,317,18,11938,5,1,1,500000 +1129,107,9,-2813,34,2,3,600000 +1130,9,10,8948,11,1,3,410000 +1131,438,11,11885,6,1,7,410000 +1132,203,25,19292,38,1,5,500000 +1133,184,4,15976,33,1,2,400000 +1134,423,56,11489,36,1,4,400000 +1135,267,22,-1228,12,1,5,410000 +1136,449,0,14418,37,1,6,500000 +1137,267,13,15202,3,0,7,600000 +1138,468,25,12500,3,0,0,600000 +1139,508,13,17692,29,1,6,410000 +1140,509,48,3796,44,0,3,600000 +1141,294,61,4934,48,1,1,410000 +1142,499,36,-428,9,1,6,400000 +1143,394,13,7605,36,1,5,410000 +1144,23,16,13477,41,1,7,400000 +1145,348,22,-4686,22,1,6,410000 +1146,153,63,8077,26,1,5,500000 +1147,369,3,-1766,46,0,7,400000 +1148,242,58,4031,34,1,0,400000 +1149,338,22,3322,46,0,4,500000 +1150,214,31,6205,44,0,0,600000 +1151,36,13,11766,44,1,6,400000 +1152,338,42,-593,44,0,2,400000 +1153,268,38,10803,24,1,2,500000 +1154,509,59,11085,11,1,0,600000 +1155,2,23,-1894,22,1,2,500000 +1156,436,8,3824,12,1,0,600000 +1157,320,55,-4904,38,1,4,400000 +1158,452,9,-550,18,0,6,410000 +1159,279,25,-3555,11,0,6,400000 +1160,171,58,-3058,13,1,2,500000 +1161,147,61,-1350,29,1,3,500000 +1162,506,59,2543,49,0,2,600000 +1163,391,9,4798,41,1,6,410000 +1164,225,52,9959,13,1,1,410000 +1165,3,2,9603,35,1,2,400000 +1166,96,20,17857,42,1,3,410000 +1167,333,22,11544,21,1,2,410000 +1168,123,9,17110,40,0,5,410000 +1169,238,42,-4284,33,1,4,400000 +1170,335,22,16722,4,2,3,410000 +1171,353,52,2899,17,0,7,600000 +1172,427,53,-4294,27,1,6,500000 +1173,455,10,11588,26,1,5,400000 +1174,323,58,3438,49,2,4,410000 +1175,110,39,14207,48,0,6,600000 +1176,6,41,13765,44,1,3,600000 +1177,281,4,13779,23,1,1,500000 +1178,128,4,-3289,32,1,7,600000 +1179,103,2,-4157,47,1,6,600000 +1180,494,16,-150,25,1,6,600000 +1181,477,33,7109,33,1,1,500000 +1182,359,52,3828,15,1,7,500000 +1183,74,26,-2706,34,1,4,500000 +1184,226,47,12652,29,1,3,410000 +1185,277,14,15864,14,1,7,400000 +1186,201,50,9561,1,1,6,400000 +1187,381,38,1847,11,1,3,410000 +1188,284,51,15327,2,1,2,500000 +1189,344,49,15938,23,0,3,400000 +1190,360,7,15117,41,2,4,600000 +1191,260,0,18122,34,2,3,600000 +1192,362,41,1743,35,1,7,500000 +1193,210,3,19549,9,0,3,410000 +1194,24,57,12898,31,0,1,410000 +1195,224,59,16157,44,1,6,500000 +1196,249,1,17636,21,1,0,410000 +1197,279,59,10028,14,1,1,600000 +1198,258,56,19197,6,0,7,500000 +1199,295,17,7488,29,2,1,600000 +1200,474,18,-4711,14,1,3,410000 +1201,439,16,6246,41,0,2,410000 +1202,323,34,14161,3,1,5,600000 +1203,464,2,14085,9,1,2,400000 +1204,47,38,496,32,1,4,600000 +1205,291,57,6936,46,1,3,600000 +1206,367,19,17323,16,1,3,500000 +1207,133,57,104,40,1,4,410000 +1208,163,31,9066,33,1,5,410000 +1209,368,9,-4813,5,1,2,400000 +1210,460,39,-1434,5,1,3,500000 +1211,301,52,1227,27,1,0,410000 +1212,500,13,5447,41,1,4,410000 +1213,202,16,3980,23,1,6,410000 +1214,392,5,15337,37,1,6,400000 +1215,90,58,8650,45,1,7,500000 +1216,397,53,14456,23,2,7,410000 +1217,492,14,-4828,30,1,2,410000 +1218,306,34,9213,45,0,4,400000 +1219,222,48,-4090,3,1,4,410000 +1220,281,47,16174,46,1,3,500000 +1221,362,39,4803,11,1,5,400000 +1222,81,1,16042,15,0,5,500000 +1223,364,54,2813,44,1,4,410000 +1224,331,20,16714,7,1,2,600000 +1225,84,60,801,49,1,3,500000 +1226,61,63,4577,25,1,3,410000 +1227,0,14,14288,14,0,0,500000 +1228,358,12,-3006,44,1,4,410000 +1229,190,62,8119,34,2,3,500000 +1230,9,8,11437,29,1,4,500000 +1231,119,51,13873,20,1,0,500000 +1232,467,28,19355,21,1,1,400000 +1233,165,17,14845,24,1,5,500000 +1234,258,41,11068,29,0,2,600000 +1235,435,11,11803,14,1,7,410000 +1236,266,18,9628,45,1,4,400000 +1237,460,1,-1269,42,1,3,600000 +1238,418,56,87,29,1,0,500000 +1239,28,40,15065,3,1,5,410000 +1240,317,39,11053,12,1,3,410000 +1241,380,28,7505,38,1,6,410000 +1242,152,6,15575,31,0,7,600000 +1243,340,21,13986,21,2,2,600000 +1244,342,54,6656,22,1,3,400000 +1245,481,20,12417,14,1,6,410000 +1246,223,5,12636,1,1,7,500000 +1247,88,11,7390,43,0,4,400000 +1248,334,18,1597,42,1,4,500000 +1249,74,0,15179,14,1,0,410000 +1250,249,52,15297,32,1,2,600000 +1251,357,0,-2318,41,0,0,500000 +1252,199,21,14761,28,1,3,600000 +1253,384,53,19586,24,1,2,500000 +1254,63,56,11566,5,1,6,410000 +1255,204,55,14715,7,0,0,400000 +1256,334,28,804,47,1,0,600000 +1257,99,22,6967,41,1,7,410000 +1258,10,15,11348,7,1,7,500000 +1259,300,45,5823,28,0,0,410000 +1260,8,0,14859,23,1,2,500000 +1261,455,2,17663,34,1,6,500000 +1262,262,41,655,34,1,5,600000 +1263,8,30,7196,44,2,3,410000 +1264,247,52,-3545,18,0,6,410000 +1265,297,14,-2376,44,1,7,400000 +1266,393,47,6853,2,0,1,400000 +1267,265,39,12526,7,1,0,500000 +1268,196,32,1362,17,0,3,400000 +1269,449,47,-3768,31,1,0,410000 +1270,216,44,16275,42,0,1,600000 +1271,49,28,3080,12,0,0,600000 +1272,256,63,14535,46,2,1,410000 +1273,90,25,-3347,8,1,7,600000 +1274,1,25,-1378,30,1,0,600000 +1275,341,4,11090,34,1,2,600000 +1276,502,40,2795,13,1,0,500000 +1277,266,15,149,36,1,7,400000 +1278,449,49,5118,41,1,2,400000 +1279,180,44,4523,11,0,0,400000 +1280,17,5,-376,28,0,3,600000 +1281,165,48,6744,21,1,5,410000 +1282,211,19,10467,22,2,7,400000 +1283,112,8,-4332,34,1,0,500000 +1284,387,61,9959,4,1,5,500000 +1285,133,62,19459,17,1,2,500000 +1286,430,39,11467,8,1,4,600000 +1287,419,4,4376,9,1,1,400000 +1288,285,15,12996,9,1,1,400000 +1289,397,22,5223,13,1,6,500000 +1290,489,40,12005,25,1,3,410000 +1291,380,61,-2666,35,0,7,500000 +1292,347,22,-4167,2,0,7,500000 +1293,309,2,13862,10,1,5,410000 +1294,339,41,17494,12,0,0,500000 +1295,8,44,979,24,1,5,500000 +1296,460,39,2488,46,1,5,500000 +1297,328,60,-897,4,1,4,500000 +1298,64,1,19736,37,1,5,400000 +1299,258,14,9889,30,1,6,500000 +1300,356,48,2081,39,1,3,500000 +1301,200,42,18498,28,1,6,500000 +1302,271,19,2870,45,2,1,500000 +1303,399,63,2392,37,1,1,410000 +1304,128,48,2396,27,0,6,600000 +1305,388,11,14500,21,0,0,500000 +1306,444,26,11070,24,1,3,410000 +1307,91,33,-922,2,0,5,600000 +1308,420,30,5649,36,1,5,500000 +1309,298,44,10080,10,1,4,410000 +1310,32,54,14900,9,2,1,500000 +1311,8,41,19970,6,0,5,500000 +1312,128,23,490,33,2,7,410000 +1313,225,34,18124,37,1,4,410000 +1314,3,7,16905,28,2,5,410000 +1315,343,59,15501,43,1,2,410000 +1316,350,63,15483,34,1,5,500000 +1317,259,35,8245,48,1,2,500000 +1318,498,37,3087,24,1,7,410000 +1319,479,35,-4118,4,1,6,600000 +1320,190,5,17115,7,2,0,410000 +1321,492,4,15594,27,1,3,410000 +1322,148,30,9891,32,1,6,410000 +1323,243,25,7836,29,0,3,600000 +1324,326,4,3437,49,1,5,410000 +1325,72,44,18981,15,1,7,400000 +1326,446,26,14803,35,1,1,410000 +1327,336,2,-2627,40,1,1,410000 +1328,51,26,17930,13,1,4,500000 +1329,385,46,-3919,26,0,1,400000 +1330,90,44,3554,32,1,0,500000 +1331,90,32,8510,39,1,0,600000 +1332,107,43,764,18,1,1,410000 +1333,263,23,17838,10,2,5,600000 +1334,21,42,-2698,17,0,6,600000 +1335,214,35,786,10,1,4,400000 +1336,157,33,13791,38,1,4,600000 +1337,259,50,18881,4,1,5,600000 +1338,289,29,15607,11,1,7,600000 +1339,366,31,12755,5,1,2,600000 +1340,131,7,17902,24,1,6,500000 +1341,17,30,18125,49,1,3,500000 +1342,135,58,14272,34,1,5,500000 +1343,420,46,-1247,21,0,1,600000 +1344,364,3,10350,42,0,0,410000 +1345,328,4,19527,24,1,4,400000 +1346,113,10,1893,1,1,4,600000 +1347,314,47,-4866,14,0,3,500000 +1348,129,46,6330,9,1,0,500000 +1349,302,62,6000,8,1,3,400000 +1350,99,24,12250,17,0,2,400000 +1351,221,20,11328,42,1,5,600000 +1352,78,27,12287,1,0,6,600000 +1353,165,48,828,44,1,3,400000 +1354,123,1,7390,45,1,6,600000 +1355,88,8,7400,10,1,3,500000 +1356,322,37,5238,47,1,1,400000 +1357,261,41,1572,18,2,4,500000 +1358,282,36,-1394,48,1,0,500000 +1359,163,16,-449,35,1,0,600000 +1360,126,45,13904,47,0,2,600000 +1361,308,32,13497,48,0,3,400000 +1362,455,29,14567,41,1,2,600000 +1363,269,4,14784,9,2,1,500000 +1364,150,39,9280,16,1,0,400000 +1365,9,56,14137,28,1,1,400000 +1366,44,21,5105,14,1,1,600000 +1367,214,52,-1217,34,0,3,400000 +1368,404,58,9118,29,1,2,500000 +1369,404,32,19848,8,1,7,410000 +1370,87,59,10383,49,1,0,600000 +1371,12,49,13159,25,1,3,600000 +1372,46,59,3933,42,1,1,500000 +1373,245,28,2523,39,2,2,500000 +1374,85,30,65,21,1,0,600000 +1375,214,29,8579,26,1,5,400000 +1376,217,37,19834,43,1,6,410000 +1377,423,44,4681,41,1,5,600000 +1378,92,41,4341,43,1,7,410000 +1379,495,48,-626,7,1,1,410000 +1380,423,26,-4677,18,1,3,500000 +1381,457,44,14508,34,1,6,410000 +1382,174,33,-530,5,1,0,400000 +1383,254,56,-937,19,1,6,600000 +1384,69,1,-4358,1,1,3,410000 +1385,110,59,14057,20,1,2,500000 +1386,64,63,9697,38,1,1,500000 +1387,319,26,7,34,1,1,500000 +1388,245,11,23,5,1,1,500000 +1389,480,58,9963,11,1,7,600000 +1390,461,22,14528,26,2,6,410000 +1391,381,51,-1412,47,1,0,400000 +1392,384,39,11761,19,0,2,500000 +1393,158,29,8533,25,1,2,400000 +1394,313,33,9417,15,2,3,600000 +1395,376,55,2960,19,0,6,500000 +1396,142,34,15352,17,1,0,600000 +1397,451,29,10091,10,1,3,500000 +1398,77,54,3856,16,1,4,600000 +1399,358,31,-350,8,1,5,600000 +1400,219,30,15936,3,1,6,500000 +1401,196,25,14339,3,1,2,410000 +1402,451,28,18590,40,0,3,400000 +1403,452,50,12317,33,0,6,410000 +1404,310,48,2195,44,1,6,600000 +1405,328,12,-1559,14,0,6,400000 +1406,206,0,19055,38,1,0,410000 +1407,178,17,16473,37,1,1,410000 +1408,185,18,2617,40,1,0,600000 +1409,315,47,9126,4,1,0,400000 +1410,276,36,9453,4,1,3,400000 +1411,303,10,16271,15,1,6,400000 +1412,217,2,19023,21,1,3,410000 +1413,187,51,-4054,26,2,1,600000 +1414,486,58,-1277,17,1,3,500000 +1415,434,20,243,9,1,5,410000 +1416,195,40,-1834,4,2,3,400000 +1417,133,21,670,4,1,6,400000 +1418,492,25,17816,41,1,7,400000 +1419,321,24,7041,32,0,1,410000 +1420,251,22,18894,25,1,6,400000 +1421,170,14,12413,7,0,0,400000 +1422,100,53,-1509,13,2,1,410000 +1423,0,56,-2492,20,1,1,600000 +1424,479,49,12192,47,0,7,410000 +1425,511,49,-2848,18,1,3,400000 +1426,321,30,-3312,40,1,4,600000 +1427,54,6,-1946,44,0,6,600000 +1428,160,58,13380,36,1,4,500000 +1429,102,33,19285,34,1,5,600000 +1430,333,55,2016,25,1,5,600000 +1431,59,25,-1011,28,1,1,410000 +1432,422,22,1460,38,0,4,400000 +1433,1,29,17757,39,1,4,400000 +1434,160,58,1208,13,1,0,600000 +1435,82,46,-1924,41,1,6,400000 +1436,370,1,8707,9,1,4,500000 +1437,267,42,3074,42,1,0,500000 +1438,273,20,5527,20,0,5,400000 +1439,281,30,-2221,47,0,7,600000 +1440,511,34,4574,12,0,2,600000 +1441,275,47,17878,20,1,4,500000 +1442,227,33,6954,9,1,0,410000 +1443,501,29,13289,15,0,7,410000 +1444,166,16,12357,39,1,2,600000 +1445,141,52,8068,28,1,7,400000 +1446,76,38,12594,21,1,1,500000 +1447,79,6,18221,24,1,5,410000 +1448,86,1,14330,22,0,0,600000 +1449,407,18,16975,28,1,5,600000 +1450,302,62,9187,2,0,1,400000 +1451,76,6,-4360,46,0,5,410000 +1452,283,52,-4788,31,1,4,600000 +1453,446,27,6899,7,1,0,500000 +1454,346,4,6653,4,1,6,410000 +1455,353,49,13413,47,2,5,400000 +1456,505,20,19536,2,1,0,410000 +1457,27,56,10054,27,1,2,400000 +1458,261,39,16001,10,1,4,500000 +1459,26,28,-4210,15,1,7,600000 +1460,305,54,13243,18,1,3,410000 +1461,271,23,15018,34,1,0,410000 +1462,488,31,19751,1,1,5,500000 +1463,42,39,19229,31,1,7,410000 +1464,156,23,6983,22,1,5,600000 +1465,228,53,1835,36,1,7,400000 +1466,360,15,1946,45,0,0,600000 +1467,247,63,7238,29,1,7,500000 +1468,0,38,-1959,6,1,7,400000 +1469,308,16,326,22,1,7,600000 +1470,451,52,17336,39,1,1,600000 +1471,446,42,16932,21,0,1,410000 +1472,256,3,6365,37,1,4,600000 +1473,473,35,19884,35,1,4,600000 +1474,269,9,10536,4,1,3,600000 +1475,253,13,6153,16,1,0,500000 +1476,294,41,13794,4,2,4,500000 +1477,339,35,12004,36,2,0,410000 +1478,165,5,2847,41,1,5,600000 +1479,63,32,11522,31,1,0,500000 +1480,321,50,6874,4,2,2,600000 +1481,471,5,17123,14,2,0,500000 +1482,199,33,16552,9,1,3,400000 +1483,137,22,11627,23,0,6,410000 +1484,269,38,9049,1,1,4,500000 +1485,505,10,9744,4,1,7,400000 +1486,448,5,17175,35,1,1,600000 +1487,394,47,6128,13,0,7,600000 +1488,452,3,12451,4,2,5,500000 +1489,408,4,-2687,46,0,6,400000 +1490,23,8,-2090,24,1,3,600000 +1491,141,44,7784,41,0,1,500000 +1492,174,28,-2607,5,1,7,410000 +1493,155,27,4273,15,0,5,600000 +1494,60,13,7030,30,1,0,400000 +1495,365,41,17857,49,1,4,600000 +1496,408,3,-4276,8,0,2,500000 +1497,301,43,5819,11,1,5,400000 +1498,302,59,7630,28,0,5,410000 +1499,460,40,2184,33,1,6,500000 +1500,492,14,9136,1,2,1,500000 +1501,62,9,-2801,25,1,5,600000 +1502,453,27,1812,24,1,3,500000 +1503,455,46,-3071,48,1,2,400000 +1504,434,57,-804,47,1,3,500000 +1505,95,29,5078,19,2,3,500000 +1506,344,48,18074,6,1,6,600000 +1507,254,39,5643,39,0,7,600000 +1508,129,59,5975,20,0,2,400000 +1509,9,49,13102,19,1,4,410000 +1510,287,24,18423,4,1,0,400000 +1511,460,53,13936,25,1,7,600000 +1512,257,30,14472,11,1,4,410000 +1513,33,21,3759,26,0,0,410000 +1514,368,60,18850,21,1,3,500000 +1515,245,30,14516,8,1,1,600000 +1516,468,38,-3031,31,1,5,410000 +1517,269,20,12599,14,1,0,600000 +1518,300,12,-748,5,1,2,410000 +1519,368,53,755,13,0,2,410000 +1520,240,3,-453,3,1,0,500000 +1521,408,45,19227,49,0,6,600000 +1522,447,54,12644,10,1,6,400000 +1523,403,38,-2032,3,1,7,500000 +1524,234,61,9645,42,1,4,400000 +1525,65,19,11668,40,1,4,600000 +1526,303,52,9864,22,1,1,400000 +1527,133,54,-1637,33,1,4,600000 +1528,77,53,11571,39,1,7,400000 +1529,349,60,8069,38,1,2,600000 +1530,304,15,18479,42,1,7,410000 +1531,286,3,19872,30,1,1,500000 +1532,313,10,2139,25,2,3,400000 +1533,254,24,4311,8,1,0,410000 +1534,247,3,1631,23,1,4,410000 +1535,110,26,18285,5,1,0,500000 +1536,131,1,14556,43,1,0,600000 +1537,124,24,3031,34,0,2,500000 +1538,312,24,19678,22,1,1,500000 +1539,369,32,4829,33,1,0,600000 +1540,493,16,8530,26,0,5,410000 +1541,311,26,376,24,1,0,500000 +1542,116,27,5011,2,1,6,410000 +1543,88,24,15120,41,1,2,500000 +1544,420,25,7626,21,1,2,400000 +1545,20,62,18808,16,0,4,600000 +1546,158,28,16353,17,1,3,600000 +1547,214,15,17749,17,1,2,410000 +1548,358,20,17512,6,0,6,400000 +1549,359,59,2676,26,2,5,500000 +1550,285,15,18476,36,1,6,400000 +1551,380,30,18624,20,1,6,410000 +1552,310,24,14434,23,1,2,500000 +1553,120,24,-3177,31,1,1,600000 +1554,192,17,9572,24,0,1,410000 +1555,73,12,11586,10,1,0,400000 +1556,174,1,-2779,32,1,7,600000 +1557,288,55,4019,11,1,1,600000 +1558,459,41,1190,27,1,6,600000 +1559,200,32,15132,7,2,4,400000 +1560,485,4,-1156,13,0,0,400000 +1561,484,62,8400,17,1,6,500000 +1562,259,9,17936,8,0,1,410000 +1563,200,42,-4806,1,1,2,400000 +1564,383,22,-2074,31,0,1,600000 +1565,189,46,6765,25,1,0,410000 +1566,176,0,2334,18,0,4,410000 +1567,422,48,9231,12,1,2,600000 +1568,445,50,10235,40,1,1,600000 +1569,425,3,-1112,27,1,6,410000 +1570,506,31,3920,10,1,3,400000 +1571,433,53,-3634,31,1,2,410000 +1572,389,49,-3381,32,1,1,500000 +1573,456,9,11731,28,1,1,500000 +1574,424,5,2352,35,0,4,410000 +1575,250,12,-1957,30,1,6,500000 +1576,432,13,14119,36,1,6,500000 +1577,163,42,7363,35,2,2,410000 +1578,480,41,11706,11,2,7,600000 +1579,225,51,13753,27,1,1,600000 +1580,288,37,12223,15,1,4,500000 +1581,487,49,15535,30,1,5,500000 +1582,242,50,6492,31,1,1,410000 +1583,362,12,14234,28,1,1,600000 +1584,289,47,13508,39,1,3,500000 +1585,116,9,19227,38,1,5,500000 +1586,3,30,7844,12,1,0,600000 +1587,137,44,19428,43,1,7,600000 +1588,453,32,9635,23,2,7,500000 +1589,495,40,9276,3,1,7,600000 +1590,163,63,5330,19,1,0,600000 +1591,106,39,8807,21,1,1,600000 +1592,68,28,13069,14,0,3,400000 +1593,102,25,7095,40,0,3,410000 +1594,17,3,18188,38,1,6,600000 +1595,358,17,13986,10,1,1,400000 +1596,97,49,-2554,49,0,7,410000 +1597,40,39,8141,28,1,2,500000 +1598,201,42,-3039,1,1,4,500000 +1599,507,29,2085,47,0,5,410000 +1600,210,9,18282,4,1,4,400000 +1601,32,36,-4053,33,0,2,500000 +1602,120,62,13351,44,1,7,600000 +1603,464,18,1667,27,1,6,410000 +1604,317,22,-3816,38,0,3,410000 +1605,140,24,14049,34,0,1,500000 +1606,129,5,16018,38,1,2,600000 +1607,307,33,16116,29,1,3,500000 +1608,8,10,7241,6,1,0,400000 +1609,371,18,9799,49,1,6,410000 +1610,282,13,4994,2,1,0,500000 +1611,137,20,8871,5,1,1,600000 +1612,14,62,8362,16,1,0,400000 +1613,450,13,-2417,48,1,7,600000 +1614,227,10,11493,32,1,2,400000 +1615,479,25,16489,46,1,2,500000 +1616,455,50,1137,34,0,0,410000 +1617,130,56,-2801,32,1,7,500000 +1618,459,27,7325,33,2,7,410000 +1619,342,19,7722,20,0,1,500000 +1620,222,18,2733,3,1,2,400000 +1621,500,40,2369,14,1,2,500000 +1622,280,10,4967,32,0,1,400000 +1623,43,24,12810,9,1,0,600000 +1624,227,59,17643,6,1,0,410000 +1625,487,21,2220,36,0,6,410000 +1626,95,50,7581,13,1,5,410000 +1627,145,6,14531,21,1,7,410000 +1628,33,44,5221,17,1,5,410000 +1629,215,38,4712,43,1,7,500000 +1630,147,63,16514,46,1,5,500000 +1631,123,36,7357,2,1,2,500000 +1632,434,31,8028,10,1,5,600000 +1633,408,55,6659,1,1,4,600000 +1634,131,59,17503,11,1,1,500000 +1635,54,59,-771,40,1,1,400000 +1636,313,8,8896,14,1,6,410000 +1637,136,45,14583,25,0,3,400000 +1638,86,62,8492,17,1,3,600000 +1639,11,57,3104,49,1,2,410000 +1640,488,22,3928,30,0,7,400000 +1641,154,0,17493,36,0,5,500000 +1642,322,42,-1274,43,1,7,410000 +1643,285,27,627,28,2,4,500000 +1644,34,57,15848,16,1,5,500000 +1645,448,21,1350,8,1,1,600000 +1646,127,11,5130,18,1,1,600000 +1647,340,41,8142,28,0,1,600000 +1648,168,50,19171,39,0,5,400000 +1649,164,23,3314,44,0,7,410000 +1650,381,16,4863,39,1,4,410000 +1651,271,31,9646,30,1,6,600000 +1652,34,22,-1162,43,1,4,410000 +1653,28,61,-1439,42,1,1,400000 +1654,167,57,-3392,8,1,1,600000 +1655,129,7,19586,48,0,1,400000 +1656,268,49,2952,12,0,0,400000 +1657,37,58,-501,2,1,4,600000 +1658,44,4,17779,20,1,1,500000 +1659,8,55,3512,22,1,4,410000 +1660,150,63,-4747,30,0,7,500000 +1661,292,2,9512,47,1,2,400000 +1662,465,3,-4488,12,1,4,600000 +1663,44,40,7152,2,1,5,400000 +1664,104,32,11323,25,1,1,410000 +1665,25,50,13463,46,1,5,600000 +1666,138,40,2648,19,1,7,400000 +1667,24,20,7276,23,1,3,400000 +1668,42,40,-4326,43,1,0,500000 +1669,10,14,7200,34,1,2,400000 +1670,448,9,-817,13,0,7,400000 +1671,379,40,28,44,1,6,600000 +1672,483,52,13569,6,1,7,410000 +1673,137,38,-2049,49,2,1,500000 +1674,66,2,-4881,43,0,0,500000 +1675,217,33,2944,39,1,1,410000 +1676,247,60,-419,31,1,3,400000 +1677,200,28,11389,14,1,7,500000 +1678,337,15,-243,22,1,4,400000 +1679,343,60,7165,24,1,3,500000 +1680,156,46,8365,49,2,0,400000 +1681,354,55,18231,32,1,3,500000 +1682,460,55,18218,36,1,5,400000 +1683,303,13,5457,35,1,2,500000 +1684,86,39,8199,18,1,5,400000 +1685,425,8,-4398,41,0,0,600000 +1686,285,46,13434,6,0,4,500000 +1687,142,34,14022,42,1,0,400000 +1688,338,21,8733,41,1,3,600000 +1689,480,22,13274,21,1,1,500000 +1690,407,25,-954,37,2,7,500000 +1691,169,26,11024,7,0,3,600000 +1692,366,26,12378,41,1,1,400000 +1693,62,55,4678,40,0,7,500000 +1694,34,56,1280,20,1,0,410000 +1695,254,50,13789,18,1,1,500000 +1696,360,5,38,16,1,3,400000 +1697,382,3,-3024,3,1,6,410000 +1698,288,24,12457,15,1,2,500000 +1699,376,54,12961,5,1,3,410000 +1700,486,17,6000,49,1,7,400000 +1701,387,1,-469,39,1,1,600000 +1702,62,5,-1310,4,1,6,500000 +1703,171,60,14787,38,1,0,600000 +1704,352,15,8380,8,1,2,600000 +1705,240,16,11402,15,2,4,600000 +1706,18,18,-5,30,1,7,410000 +1707,309,32,4792,34,1,4,410000 +1708,132,10,-2767,21,1,2,400000 +1709,82,6,8597,24,1,0,500000 +1710,142,47,15820,7,1,0,410000 +1711,175,30,8041,37,1,5,600000 +1712,225,38,17191,44,1,4,500000 +1713,276,29,3602,29,1,4,400000 +1714,281,35,-4809,5,2,6,400000 +1715,393,13,14344,4,1,5,600000 +1716,137,43,-4273,17,1,4,410000 +1717,243,16,2052,18,1,3,600000 +1718,402,52,19849,20,1,3,400000 +1719,1,19,13477,17,2,6,600000 +1720,167,55,-2118,7,1,1,600000 +1721,182,10,6295,20,1,0,400000 +1722,226,41,19829,49,0,3,400000 +1723,178,0,12754,12,1,5,400000 +1724,463,49,18527,38,1,5,400000 +1725,197,47,-2886,2,1,0,600000 +1726,192,29,3737,33,1,4,600000 +1727,154,52,-1173,48,1,5,400000 +1728,125,13,17423,49,0,3,400000 +1729,154,10,-3023,24,1,7,500000 +1730,70,43,17499,31,1,4,600000 +1731,165,17,12963,31,1,5,500000 +1732,236,24,-2614,8,2,6,410000 +1733,44,3,15596,12,1,0,400000 +1734,146,12,15117,15,1,0,500000 +1735,240,57,14235,38,1,3,400000 +1736,499,0,-2109,26,0,6,410000 +1737,212,26,17563,11,1,5,500000 +1738,15,23,7685,2,1,7,500000 +1739,209,15,17125,12,1,0,410000 +1740,410,49,1753,5,1,4,410000 +1741,141,27,-4108,37,1,4,410000 +1742,386,12,16664,1,0,2,600000 +1743,387,54,1804,4,2,6,500000 +1744,267,29,-1635,33,1,1,400000 +1745,331,47,439,35,1,0,600000 +1746,388,8,10207,34,2,2,410000 +1747,236,50,7959,49,2,1,400000 +1748,305,13,14948,8,0,3,400000 +1749,55,50,8280,6,1,3,410000 +1750,96,27,10551,25,0,4,400000 +1751,471,28,7290,18,1,3,410000 +1752,304,31,12939,11,2,5,400000 +1753,444,6,12742,44,1,2,400000 +1754,241,12,9651,34,1,6,400000 +1755,369,4,6,28,1,5,600000 +1756,364,30,19376,3,0,0,400000 +1757,324,40,1451,12,1,2,600000 +1758,257,53,-1288,22,0,2,400000 +1759,73,55,-4570,3,1,7,410000 +1760,54,45,18508,38,1,2,600000 +1761,431,17,10473,5,1,5,600000 +1762,94,34,15665,34,1,7,410000 +1763,360,28,11021,17,2,2,500000 +1764,251,48,-2475,17,1,6,410000 +1765,473,13,6440,45,1,3,410000 +1766,278,5,9553,19,2,1,400000 +1767,90,45,19107,33,0,1,500000 +1768,93,1,7639,1,0,4,500000 +1769,467,18,15183,40,1,1,600000 +1770,131,32,19772,48,1,6,410000 +1771,475,54,11578,39,1,0,600000 +1772,135,47,3163,19,1,5,400000 +1773,483,16,9678,6,1,1,410000 +1774,72,9,12696,28,1,3,410000 +1775,92,51,7265,49,1,7,600000 +1776,345,8,14971,22,1,3,500000 +1777,234,2,9083,11,1,0,400000 +1778,408,44,7004,7,1,6,500000 +1779,66,49,-2758,37,2,6,500000 +1780,456,17,-4479,4,0,5,500000 +1781,286,23,10228,42,0,3,410000 +1782,140,42,-2506,7,1,3,600000 +1783,304,31,14960,39,0,1,500000 +1784,265,12,-257,12,1,1,410000 +1785,432,59,9279,25,2,6,500000 +1786,386,52,1176,22,1,5,600000 +1787,251,60,-2672,36,2,2,400000 +1788,331,38,18786,12,2,2,600000 +1789,74,7,18302,30,1,7,500000 +1790,206,30,18169,24,0,1,410000 +1791,82,63,16782,14,1,1,400000 +1792,408,48,-3274,40,1,5,410000 +1793,509,59,-2946,34,1,6,400000 +1794,502,30,9234,6,1,3,500000 +1795,381,32,-4525,41,1,6,410000 +1796,200,19,4717,5,1,4,400000 +1797,300,35,19969,26,0,6,500000 +1798,209,29,-690,19,1,3,600000 +1799,329,6,7630,31,0,0,400000 +1800,400,55,-961,7,2,4,410000 +1801,418,1,6931,18,1,6,600000 +1802,302,50,14738,20,2,2,400000 +1803,25,31,4316,28,1,6,400000 +1804,507,10,7607,15,1,1,600000 +1805,331,7,11929,44,1,7,500000 +1806,207,8,8068,22,1,6,600000 +1807,415,14,1145,38,1,3,410000 +1808,441,26,18494,17,1,2,600000 +1809,134,53,10872,25,0,2,600000 +1810,476,9,809,25,2,7,400000 +1811,101,0,18845,16,1,7,600000 +1812,390,51,12621,22,1,7,400000 +1813,298,52,13142,28,1,1,400000 +1814,83,52,7843,10,1,2,600000 +1815,69,48,11146,15,0,7,600000 +1816,369,38,11081,21,1,2,400000 +1817,218,44,16285,19,1,4,600000 +1818,379,46,8970,48,1,4,410000 +1819,175,51,-4274,30,1,7,410000 +1820,333,4,5609,6,1,0,400000 +1821,60,58,3571,44,1,1,600000 +1822,196,33,6023,31,1,0,400000 +1823,431,36,10634,47,1,2,600000 +1824,436,51,15488,43,1,3,600000 +1825,22,0,4618,39,1,6,400000 +1826,501,24,17359,24,1,0,400000 +1827,130,13,14711,42,1,6,500000 +1828,194,44,-2160,1,1,6,500000 +1829,328,10,19458,46,1,1,400000 +1830,252,59,19056,43,1,7,600000 +1831,364,57,-4966,24,1,5,500000 +1832,172,42,17312,21,1,3,400000 +1833,171,61,17482,24,1,6,410000 +1834,119,54,1929,6,1,6,500000 +1835,77,30,3665,43,0,3,410000 +1836,18,35,14505,28,0,0,410000 +1837,272,47,5200,13,0,5,410000 +1838,250,39,10674,36,1,3,410000 +1839,59,43,381,48,1,6,500000 +1840,361,0,2611,14,1,6,400000 +1841,116,56,17870,20,1,0,500000 +1842,274,28,4225,10,1,6,410000 +1843,36,28,15136,37,0,1,400000 +1844,38,26,6227,31,1,1,500000 +1845,12,52,-1805,36,1,7,410000 +1846,312,41,19953,12,0,6,600000 +1847,254,33,18390,34,0,1,500000 +1848,70,17,4806,40,1,1,500000 +1849,278,59,437,39,2,3,410000 +1850,230,7,3896,42,1,4,500000 +1851,337,49,6366,23,0,6,400000 +1852,223,26,17518,6,1,0,410000 +1853,390,2,-3241,19,1,3,600000 +1854,352,52,11387,19,1,0,500000 +1855,115,58,13037,1,1,2,500000 +1856,279,20,3748,39,1,7,600000 +1857,301,39,4425,49,1,5,410000 +1858,316,19,1258,34,0,0,600000 +1859,494,17,5027,24,0,5,410000 +1860,510,2,17436,8,1,5,410000 +1861,502,4,-4179,34,1,7,400000 +1862,260,10,18827,14,1,2,410000 +1863,133,40,17487,1,1,6,500000 +1864,327,37,-238,47,1,7,410000 +1865,135,37,14627,24,1,4,600000 +1866,360,7,16733,47,2,6,500000 +1867,80,46,1304,9,1,0,410000 +1868,467,55,13057,23,1,7,400000 +1869,238,21,3008,47,1,6,400000 +1870,372,1,9429,47,1,2,410000 +1871,371,4,11393,15,0,7,410000 +1872,240,38,3456,1,1,4,600000 +1873,259,41,2527,4,1,3,400000 +1874,444,27,3930,12,0,7,400000 +1875,510,44,11303,41,2,4,400000 +1876,358,44,869,5,1,1,600000 +1877,202,51,16344,45,1,4,600000 +1878,241,18,8043,48,1,6,400000 +1879,428,33,1670,18,0,7,400000 +1880,490,47,14737,8,1,4,600000 +1881,63,45,7102,49,2,6,400000 +1882,69,17,17050,18,0,3,410000 +1883,180,5,6861,18,1,1,500000 +1884,123,48,13364,19,1,1,500000 +1885,482,58,-471,1,1,2,600000 +1886,45,7,19321,12,1,1,400000 +1887,258,20,5765,36,0,2,600000 +1888,406,49,7501,34,1,2,500000 +1889,270,55,13736,3,0,7,400000 +1890,358,30,16225,30,1,4,500000 +1891,329,21,16385,29,0,3,410000 +1892,85,3,7196,27,1,0,600000 +1893,110,7,11968,4,0,7,500000 +1894,312,15,13617,7,0,2,400000 +1895,34,1,1311,4,1,5,600000 +1896,192,47,9481,35,1,0,600000 +1897,322,15,-3637,32,1,6,600000 +1898,448,51,14480,38,1,2,400000 +1899,258,12,19967,39,1,4,400000 +1900,16,56,5526,5,2,5,500000 +1901,318,40,290,4,1,1,410000 +1902,467,57,8109,11,1,4,500000 +1903,365,30,9294,37,1,6,600000 +1904,488,8,-2300,49,1,2,400000 +1905,300,3,441,5,1,5,410000 +1906,508,1,11662,42,1,0,410000 +1907,367,44,-2202,44,1,5,400000 +1908,442,12,14631,31,1,4,600000 +1909,460,23,2275,16,1,5,500000 +1910,504,1,-1666,30,1,6,600000 +1911,472,51,15385,37,1,6,500000 +1912,90,51,-2679,6,1,3,400000 +1913,65,33,4688,30,1,3,600000 +1914,303,28,7847,15,0,4,410000 +1915,18,36,3196,24,1,6,500000 +1916,127,26,17811,1,0,6,400000 +1917,256,37,323,9,1,4,500000 +1918,466,55,16397,44,2,5,600000 +1919,443,54,-1262,45,2,1,400000 +1920,365,10,10357,49,1,5,410000 +1921,476,12,17931,14,1,3,400000 +1922,306,29,1078,24,2,3,400000 +1923,159,37,7027,23,1,1,600000 +1924,503,5,12752,41,0,7,410000 +1925,0,47,16492,43,1,4,600000 +1926,106,41,-652,45,1,6,600000 +1927,250,45,14479,37,1,0,400000 +1928,224,59,11218,11,1,3,500000 +1929,480,50,-4360,18,1,6,410000 +1930,262,57,9909,34,2,4,400000 +1931,323,6,1352,46,1,7,410000 +1932,419,25,222,34,0,7,600000 +1933,414,19,-4116,10,1,3,410000 +1934,346,49,11860,17,1,5,500000 +1935,511,15,13758,48,1,1,600000 +1936,30,12,14267,3,1,4,600000 +1937,49,5,-1587,49,1,6,410000 +1938,410,40,11725,8,1,6,500000 +1939,271,42,19937,21,0,4,600000 +1940,395,38,11862,24,1,7,600000 +1941,377,27,6173,29,0,7,410000 +1942,145,23,13911,32,1,7,600000 +1943,53,15,11793,40,0,0,410000 +1944,467,2,183,46,1,5,410000 +1945,312,21,-2609,22,1,3,400000 +1946,11,30,7228,47,2,6,600000 +1947,510,22,16705,7,1,1,600000 +1948,355,47,7127,47,1,2,410000 +1949,23,43,8084,25,1,1,600000 +1950,388,52,6327,11,1,4,600000 +1951,198,35,16506,31,0,4,500000 +1952,138,47,17923,22,0,7,400000 +1953,224,57,6472,2,1,5,400000 +1954,274,48,10774,28,1,2,500000 +1955,510,37,7443,49,1,6,400000 +1956,358,1,8328,29,1,6,500000 +1957,38,10,17209,26,1,2,400000 +1958,269,9,9434,34,1,1,410000 +1959,268,46,6010,32,2,1,600000 +1960,154,27,18733,42,1,4,500000 +1961,93,8,9758,48,1,1,600000 +1962,94,26,8167,45,1,5,400000 +1963,306,38,11905,45,1,6,400000 +1964,461,13,810,13,1,3,410000 +1965,130,41,12792,16,1,4,410000 +1966,416,62,73,29,1,3,410000 +1967,202,24,-4213,20,1,2,400000 +1968,119,7,7804,48,1,1,600000 +1969,468,60,3398,11,0,2,600000 +1970,453,21,-3556,32,1,4,400000 +1971,175,60,-2571,2,1,0,600000 +1972,316,44,-3757,23,1,5,400000 +1973,351,52,13292,34,0,3,500000 +1974,303,10,16600,23,2,7,400000 +1975,329,62,2046,36,1,0,410000 +1976,487,34,-3623,38,1,5,600000 +1977,146,1,11346,8,1,2,500000 +1978,395,7,6935,45,1,5,410000 +1979,486,30,17534,44,1,3,600000 +1980,329,19,13417,19,1,4,600000 +1981,156,16,242,42,1,2,600000 +1982,343,14,2183,5,1,3,500000 +1983,149,24,3666,23,0,2,400000 +1984,316,27,19396,47,1,7,600000 +1985,385,7,6616,11,1,1,410000 +1986,102,18,9114,5,1,0,500000 +1987,85,45,1776,7,1,1,600000 +1988,444,1,4595,31,1,0,600000 +1989,428,22,3203,1,1,6,500000 +1990,416,21,17017,38,0,4,400000 +1991,456,34,197,44,0,2,400000 +1992,298,58,9394,24,1,4,410000 +1993,119,29,6669,40,1,6,600000 +1994,250,4,11720,5,1,0,410000 +1995,125,15,-831,22,0,2,600000 +1996,411,63,612,34,1,1,400000 +1997,183,26,14654,21,0,4,410000 +1998,389,13,5818,1,1,2,500000 +1999,371,41,4644,25,1,5,600000 +2000,339,33,-1187,38,1,3,410000 +2001,112,3,14341,36,1,6,410000 +2002,172,28,9058,11,1,2,400000 +2003,16,30,7714,41,0,2,500000 +2004,240,21,15810,36,0,2,400000 +2005,447,36,18810,17,1,5,500000 +2006,3,51,15412,10,1,6,410000 +2007,162,14,-2402,31,1,3,400000 +2008,20,35,-903,48,0,6,600000 +2009,39,57,10117,6,2,0,600000 +2010,190,30,13980,13,1,4,410000 +2011,457,42,17669,13,1,2,600000 +2012,315,54,7052,25,1,3,500000 +2013,219,48,9413,44,2,3,500000 +2014,58,29,6032,25,1,5,500000 +2015,268,51,19133,39,1,7,410000 +2016,243,17,16405,47,1,7,600000 +2017,311,15,6462,46,2,2,500000 +2018,97,6,3650,37,1,6,400000 +2019,81,50,505,39,1,6,400000 +2020,440,60,13991,28,2,4,500000 +2021,323,18,2276,22,0,6,400000 +2022,471,58,14526,4,1,1,600000 +2023,328,9,11347,33,1,3,500000 +2024,204,57,19764,1,0,6,400000 +2025,326,58,5415,49,1,2,410000 +2026,414,48,-666,21,0,0,400000 +2027,137,33,8647,47,0,1,600000 +2028,244,40,8152,8,0,7,410000 +2029,133,36,1684,41,1,3,600000 +2030,463,20,1413,26,1,7,500000 +2031,342,29,10359,30,2,2,400000 +2032,109,59,-3397,7,0,4,400000 +2033,410,47,13555,45,1,3,500000 +2034,239,0,688,18,1,5,600000 +2035,36,16,14435,28,1,1,400000 +2036,259,47,3021,3,2,1,600000 +2037,321,43,12023,15,1,3,500000 +2038,28,48,16566,17,1,7,400000 +2039,125,19,6136,4,1,0,410000 +2040,41,31,2646,4,1,1,400000 +2041,429,13,-1171,16,0,7,410000 +2042,391,46,-2433,33,1,4,600000 +2043,445,19,11752,33,2,2,400000 +2044,464,50,4331,36,0,3,410000 +2045,324,56,14740,37,1,1,400000 +2046,509,22,13630,37,0,3,600000 +2047,138,38,19694,6,1,0,600000 +2048,233,44,8537,24,1,2,410000 +2049,278,49,16574,2,1,7,500000 +2050,73,27,1448,35,1,6,500000 +2051,154,46,8025,24,2,3,410000 +2052,15,5,4979,38,0,5,400000 +2053,134,34,-4634,5,1,4,600000 +2054,363,47,8390,22,0,2,410000 +2055,232,60,-4902,37,1,2,500000 +2056,359,37,13556,19,1,7,410000 +2057,20,4,-500,23,0,2,410000 +2058,493,11,1062,32,1,0,400000 +2059,167,9,17054,29,1,3,600000 +2060,2,8,6419,30,0,2,600000 +2061,253,8,4937,3,1,5,600000 +2062,440,6,2772,47,1,5,400000 +2063,200,54,-4075,47,1,6,410000 +2064,20,49,6588,29,0,0,500000 +2065,71,58,6515,16,1,5,600000 +2066,398,23,-3408,35,1,1,410000 +2067,449,3,17917,38,1,0,600000 +2068,175,26,8878,28,0,0,600000 +2069,25,2,-4296,41,0,0,600000 +2070,266,53,-4571,9,0,3,500000 +2071,128,41,14079,18,2,3,400000 +2072,361,1,11855,18,0,1,410000 +2073,46,15,2940,36,2,1,500000 +2074,491,58,4443,44,2,2,410000 +2075,123,47,13739,36,0,7,600000 +2076,285,57,6023,32,1,3,600000 +2077,370,16,14629,8,1,6,600000 +2078,490,34,5427,29,1,6,410000 +2079,301,31,12180,11,0,7,400000 +2080,445,58,10158,41,1,5,400000 +2081,79,55,12327,23,1,4,400000 +2082,284,3,1160,46,2,6,500000 +2083,141,18,-11,35,1,0,410000 +2084,29,45,13316,43,2,0,410000 +2085,478,52,8106,21,1,6,600000 +2086,29,10,5172,2,0,0,500000 +2087,465,19,-3960,42,1,1,600000 +2088,46,61,-4801,2,0,3,500000 +2089,392,18,-3804,30,1,5,400000 +2090,320,2,4479,28,1,3,400000 +2091,341,17,-1006,17,1,6,410000 +2092,428,35,1471,15,1,5,410000 +2093,229,20,15908,12,2,5,600000 +2094,240,52,3688,10,2,1,600000 +2095,453,59,3412,49,1,3,400000 +2096,300,43,8855,27,1,5,400000 +2097,282,46,16132,9,1,1,500000 +2098,275,48,10905,45,0,2,400000 +2099,72,41,5411,44,1,0,500000 +2100,300,61,7543,38,0,5,400000 +2101,467,6,7225,3,1,5,600000 +2102,326,33,2641,18,0,5,400000 +2103,153,50,18612,28,1,3,400000 +2104,156,5,-3710,23,1,2,410000 +2105,466,26,19338,12,1,3,500000 +2106,322,36,16217,4,1,3,400000 +2107,373,45,12628,41,1,2,600000 +2108,466,60,1653,8,1,7,410000 +2109,109,43,17054,28,1,4,500000 +2110,193,26,10923,26,1,2,500000 +2111,459,2,4250,40,1,3,410000 +2112,453,2,19531,24,2,2,600000 +2113,150,56,-1845,25,0,7,410000 +2114,123,62,6538,4,1,2,400000 +2115,311,41,12139,24,1,0,410000 +2116,90,19,12676,21,1,2,400000 +2117,481,16,-2847,38,0,2,400000 +2118,12,49,-1224,21,1,6,400000 +2119,487,60,3439,43,1,5,410000 +2120,22,51,5836,30,1,4,400000 +2121,95,10,14564,45,0,3,400000 +2122,179,10,17771,47,0,0,410000 +2123,186,25,4338,30,1,1,410000 +2124,57,8,4025,36,0,7,410000 +2125,344,7,17278,22,1,0,410000 +2126,435,61,350,20,0,6,500000 +2127,98,55,3903,41,1,2,500000 +2128,247,27,229,27,1,2,500000 +2129,299,35,18666,37,1,2,410000 +2130,184,10,14352,5,2,1,600000 +2131,182,60,13448,42,0,0,500000 +2132,440,56,16729,24,1,6,500000 +2133,20,12,10689,23,1,4,400000 +2134,164,59,1800,47,1,2,400000 +2135,45,58,15645,35,1,2,410000 +2136,58,49,12145,20,0,1,410000 +2137,414,22,19041,47,0,7,410000 +2138,89,1,-805,30,2,7,410000 +2139,305,15,11859,48,1,6,500000 +2140,17,26,-2976,3,0,7,410000 +2141,65,58,1971,42,1,0,400000 +2142,84,57,-2868,15,0,2,500000 +2143,496,27,371,4,0,1,410000 +2144,77,60,17778,34,1,5,500000 +2145,142,59,5622,11,1,4,600000 +2146,165,33,8646,40,1,0,500000 +2147,307,37,2719,24,1,2,600000 +2148,223,35,9515,27,1,3,600000 +2149,308,15,13674,48,2,3,600000 +2150,210,40,-1428,8,1,7,500000 +2151,341,9,15601,37,1,7,410000 +2152,503,22,490,8,1,2,500000 +2153,308,43,16049,23,1,3,400000 +2154,228,7,-3741,37,0,3,400000 +2155,367,5,13216,8,1,0,400000 +2156,109,53,15730,35,1,4,500000 +2157,484,14,-3625,18,2,0,410000 +2158,165,48,5208,34,1,2,500000 +2159,215,18,18329,31,0,7,600000 +2160,57,7,7071,1,0,4,400000 +2161,402,29,1151,40,1,5,600000 +2162,306,59,18033,26,2,3,410000 +2163,264,41,15088,45,0,7,500000 +2164,174,53,3167,10,1,0,600000 +2165,222,8,8264,7,1,1,600000 +2166,274,54,9955,8,1,2,400000 +2167,327,43,7080,1,0,3,500000 +2168,487,12,-1327,42,0,5,600000 +2169,225,46,-4015,43,1,1,600000 +2170,209,10,11587,32,1,5,500000 +2171,178,48,19785,39,0,5,410000 +2172,335,31,15270,27,1,1,500000 +2173,108,2,4966,39,2,2,500000 +2174,165,37,10907,12,1,3,410000 +2175,158,26,13176,25,1,6,410000 +2176,63,0,16271,25,0,7,410000 +2177,131,44,12825,30,1,0,410000 +2178,194,47,11125,9,1,3,500000 +2179,374,57,6969,35,1,1,500000 +2180,261,39,8540,25,0,1,410000 +2181,270,44,4268,4,0,5,500000 +2182,360,49,1565,20,1,4,410000 +2183,207,45,-1123,45,0,6,400000 +2184,342,53,-1265,16,1,3,400000 +2185,457,1,4374,41,1,2,500000 +2186,29,10,-278,46,1,7,410000 +2187,122,54,8399,24,0,7,600000 +2188,167,22,8273,33,0,4,410000 +2189,452,21,10240,2,0,2,600000 +2190,233,47,6373,40,1,5,400000 +2191,162,57,19630,15,1,2,410000 +2192,108,11,10868,10,0,3,410000 +2193,328,46,-4407,1,0,6,400000 +2194,230,10,-1746,36,1,1,600000 +2195,480,16,1398,26,2,0,600000 +2196,34,54,9944,35,1,2,410000 +2197,492,57,1951,31,0,5,600000 +2198,489,50,4158,32,1,2,500000 +2199,114,42,-54,5,0,2,500000 +2200,83,2,-1525,7,1,5,500000 +2201,246,5,7729,6,1,0,600000 +2202,302,32,-2255,39,1,7,400000 +2203,251,36,12660,39,1,5,400000 +2204,27,57,8046,41,0,4,600000 +2205,252,46,10537,48,1,7,600000 +2206,435,58,13719,35,1,1,410000 +2207,198,29,-3738,42,1,3,400000 +2208,328,0,16723,28,1,1,600000 +2209,475,58,4311,16,1,2,500000 +2210,506,36,16273,23,1,5,400000 +2211,312,33,16691,30,1,7,410000 +2212,497,2,4598,8,0,4,600000 +2213,269,36,14685,15,1,2,500000 +2214,228,16,6137,38,1,3,500000 +2215,47,39,-4806,8,2,4,600000 +2216,317,37,14189,6,0,1,500000 +2217,294,58,19233,14,1,2,410000 +2218,326,33,710,19,1,3,410000 +2219,287,47,8175,36,0,7,600000 +2220,115,0,-476,44,1,6,410000 +2221,26,25,7928,34,1,2,400000 +2222,145,18,6604,4,1,0,600000 +2223,329,14,-1875,13,1,0,600000 +2224,172,60,-3930,40,0,0,600000 +2225,41,13,-589,44,0,4,600000 +2226,237,34,18696,22,0,1,400000 +2227,129,61,1562,44,1,4,410000 +2228,475,14,13716,28,1,5,600000 +2229,6,35,5596,7,1,1,600000 +2230,19,6,4467,30,1,6,410000 +2231,233,53,4794,34,1,5,500000 +2232,39,24,17586,20,1,0,500000 +2233,338,2,168,32,1,4,410000 +2234,188,22,892,38,0,6,410000 +2235,508,34,8600,48,0,5,410000 +2236,225,40,11022,4,0,4,600000 +2237,151,26,-2089,45,1,5,400000 +2238,213,52,15665,6,1,1,600000 +2239,144,34,-2007,35,2,3,400000 +2240,76,50,-4067,7,1,0,410000 +2241,136,21,17353,26,1,0,500000 +2242,253,42,-270,20,0,2,600000 +2243,156,30,8597,19,1,1,410000 +2244,459,52,45,48,1,6,500000 +2245,402,24,5358,18,1,7,400000 +2246,155,43,19552,10,1,2,600000 +2247,34,35,155,2,1,7,410000 +2248,144,37,12837,20,1,4,600000 +2249,395,45,-2071,30,0,0,410000 +2250,332,56,1501,10,0,0,400000 +2251,79,10,7797,43,1,6,400000 +2252,133,0,-1528,28,0,0,500000 +2253,239,43,-3779,4,1,3,400000 +2254,189,60,15153,30,1,1,400000 +2255,78,35,11356,37,1,6,600000 +2256,54,43,-2005,29,0,7,400000 +2257,413,3,-1052,25,1,1,410000 +2258,44,24,-393,32,1,3,410000 +2259,318,36,1748,45,1,5,600000 +2260,326,15,-522,49,1,3,600000 +2261,165,13,541,8,1,6,500000 +2262,360,49,1706,14,1,2,410000 +2263,462,60,15788,12,1,4,400000 +2264,329,13,-1096,15,0,3,600000 +2265,106,15,12227,31,0,2,600000 +2266,341,60,-794,3,1,1,410000 +2267,396,33,17779,40,1,5,500000 +2268,408,27,-893,8,1,7,400000 +2269,65,45,13103,4,1,1,500000 +2270,435,54,2021,48,1,3,600000 +2271,69,20,-2658,2,1,2,400000 +2272,289,61,10855,30,0,6,500000 +2273,336,27,17394,18,2,7,600000 +2274,115,8,-1637,45,1,2,410000 +2275,466,55,14224,40,2,7,600000 +2276,293,3,13760,29,0,2,600000 +2277,275,58,16206,32,0,7,500000 +2278,378,57,-3682,16,1,0,500000 +2279,391,14,371,34,1,6,500000 +2280,343,32,2219,30,2,4,600000 +2281,380,49,3481,13,1,3,410000 +2282,47,12,18018,23,1,1,410000 +2283,458,22,-1351,17,0,6,500000 +2284,423,39,5787,24,1,2,410000 +2285,251,40,845,25,2,0,410000 +2286,440,30,5471,28,2,2,400000 +2287,403,56,-1683,12,1,2,600000 +2288,33,0,-3269,23,0,3,600000 +2289,154,36,-495,19,0,5,500000 +2290,454,0,8914,6,1,7,600000 +2291,135,21,14111,12,1,3,400000 +2292,385,46,-1352,39,1,7,400000 +2293,370,28,2110,38,1,5,600000 +2294,162,13,16756,39,1,1,500000 +2295,324,7,2747,32,1,7,500000 +2296,464,56,-1082,36,1,2,410000 +2297,385,40,2058,27,2,2,410000 +2298,299,55,3410,21,1,3,500000 +2299,306,0,-2518,28,0,7,400000 +2300,229,14,14201,27,1,0,600000 +2301,346,13,376,47,1,5,500000 +2302,279,49,10106,48,1,7,500000 +2303,33,13,6657,9,0,1,410000 +2304,305,1,16321,40,2,5,400000 +2305,428,62,4638,48,1,4,600000 +2306,54,25,15723,15,0,0,600000 +2307,342,57,18823,29,1,5,500000 +2308,231,27,8859,11,1,5,410000 +2309,295,9,7457,48,2,6,410000 +2310,489,26,12347,31,0,6,600000 +2311,8,21,10569,3,0,6,500000 +2312,487,18,11096,26,1,1,400000 +2313,238,28,-3960,28,1,7,500000 +2314,484,14,8730,40,1,4,500000 +2315,188,61,1236,10,0,4,400000 +2316,271,9,-2528,23,1,4,600000 +2317,397,35,16511,30,2,1,500000 +2318,294,2,730,48,1,1,500000 +2319,22,56,-3830,49,1,7,600000 +2320,331,58,15551,6,1,0,400000 +2321,391,25,3901,7,1,4,500000 +2322,260,4,18622,34,2,4,600000 +2323,109,57,13192,12,0,3,500000 +2324,49,25,18414,20,1,7,500000 +2325,382,48,5023,26,1,0,600000 +2326,443,41,7071,17,2,6,400000 +2327,422,44,4830,9,1,0,410000 +2328,70,24,6003,1,1,7,410000 +2329,349,12,16285,1,1,5,500000 +2330,511,51,3336,10,1,3,600000 +2331,421,38,11645,49,1,6,600000 +2332,208,10,9543,1,1,2,600000 +2333,425,41,-1770,34,1,4,410000 +2334,304,23,19922,12,0,1,600000 +2335,30,19,-2808,41,1,1,400000 +2336,478,11,11513,5,0,6,410000 +2337,97,28,-102,14,1,3,410000 +2338,274,21,14132,33,0,3,410000 +2339,414,10,321,31,1,4,400000 +2340,5,51,2244,6,1,2,600000 +2341,266,44,11414,19,0,6,410000 +2342,481,40,9656,37,1,5,500000 +2343,216,55,3196,26,1,4,410000 +2344,113,1,17736,46,1,6,410000 +2345,133,5,-3644,48,1,3,400000 +2346,137,44,19352,27,0,0,500000 +2347,458,55,4949,23,1,0,400000 +2348,103,17,9902,12,1,3,410000 +2349,222,0,17106,37,1,6,410000 +2350,182,8,196,28,1,3,410000 +2351,510,56,13890,38,0,7,600000 +2352,2,14,14913,18,1,6,600000 +2353,362,63,-1574,44,1,2,400000 +2354,27,39,-743,5,1,3,600000 +2355,202,42,4781,49,1,4,600000 +2356,324,34,5564,36,2,6,410000 +2357,186,13,322,18,0,5,600000 +2358,292,58,9501,23,1,7,500000 +2359,155,24,-3345,29,1,1,600000 +2360,278,18,12916,18,1,6,600000 +2361,61,17,9543,19,1,1,600000 +2362,404,1,8804,11,1,7,400000 +2363,461,18,12704,41,1,6,410000 +2364,93,23,-2264,4,1,5,410000 +2365,121,12,17212,22,1,4,600000 +2366,22,48,13856,4,1,3,410000 +2367,357,21,-1578,43,1,4,500000 +2368,197,12,6233,47,2,4,400000 +2369,144,38,16624,3,0,1,500000 +2370,199,46,-3799,20,1,3,400000 +2371,510,43,12566,13,1,1,400000 +2372,236,3,10194,47,0,6,600000 +2373,272,0,12410,9,0,1,500000 +2374,123,56,-4508,49,0,1,410000 +2375,149,35,13835,37,1,4,500000 +2376,431,29,16145,7,1,7,410000 +2377,24,2,374,38,1,7,400000 +2378,104,20,18206,1,1,1,500000 +2379,91,29,11773,34,1,7,500000 +2380,486,52,16470,29,1,1,400000 +2381,459,51,13817,20,1,1,400000 +2382,185,39,14253,28,1,0,410000 +2383,124,28,17188,46,1,4,500000 +2384,429,39,-2703,43,1,1,600000 +2385,31,30,18059,47,1,4,400000 +2386,257,31,6392,22,0,5,410000 +2387,298,56,13539,33,2,7,410000 +2388,377,49,-1344,14,0,3,600000 +2389,481,31,-3349,49,0,0,600000 +2390,169,0,17464,41,1,0,500000 +2391,430,13,13789,7,1,0,500000 +2392,158,10,12115,25,1,6,400000 +2393,17,20,8853,12,1,5,410000 +2394,193,55,12507,34,1,7,400000 +2395,235,51,18287,40,0,5,410000 +2396,431,4,17101,37,1,1,500000 +2397,420,51,-4092,48,1,5,410000 +2398,375,17,8891,6,1,5,410000 +2399,413,36,14769,48,1,6,410000 +2400,99,21,11531,22,1,4,400000 +2401,108,9,9152,4,2,1,410000 +2402,95,0,8971,12,1,4,400000 +2403,419,4,6761,13,2,6,400000 +2404,442,46,4706,29,0,2,600000 +2405,259,23,11073,9,1,5,400000 +2406,332,60,5208,16,2,3,400000 +2407,481,17,-4952,21,1,1,500000 +2408,8,32,5357,47,1,1,400000 +2409,476,14,14535,12,1,5,600000 +2410,93,62,507,23,0,6,600000 +2411,386,22,5519,49,1,4,410000 +2412,147,14,7278,13,1,6,400000 +2413,124,50,3351,25,0,4,400000 +2414,340,52,17680,2,1,7,600000 +2415,64,6,18494,42,1,4,400000 +2416,59,46,13787,30,1,4,400000 +2417,160,2,11758,38,1,0,600000 +2418,1,39,17958,22,1,2,600000 +2419,148,20,-4209,14,1,0,500000 +2420,138,27,2013,21,1,5,500000 +2421,438,49,13920,49,1,0,500000 +2422,106,53,-4708,8,1,5,600000 +2423,65,51,8776,47,1,6,500000 +2424,257,50,-1075,23,1,1,600000 +2425,267,56,13653,40,1,1,410000 +2426,368,57,6690,17,1,2,500000 +2427,245,27,-2058,29,1,7,410000 +2428,22,17,16634,4,1,0,500000 +2429,4,43,-1141,30,1,5,500000 +2430,324,27,5295,37,1,4,600000 +2431,337,52,-3193,35,2,2,600000 +2432,317,63,10228,14,1,0,600000 +2433,277,63,10658,41,2,7,500000 +2434,405,26,5641,30,1,2,410000 +2435,211,48,2900,17,1,0,400000 +2436,232,34,825,23,0,3,500000 +2437,370,37,10823,43,1,5,410000 +2438,328,22,6291,35,1,1,500000 +2439,282,17,483,14,1,2,600000 +2440,166,4,11992,19,2,2,410000 +2441,248,27,19318,39,1,5,500000 +2442,76,24,-1098,43,1,4,500000 +2443,359,4,3790,23,1,4,410000 +2444,185,20,-1755,15,1,2,410000 +2445,335,38,2833,18,1,3,600000 +2446,134,47,7656,19,1,5,400000 +2447,303,5,16178,27,1,0,410000 +2448,42,59,16916,25,1,6,600000 +2449,196,57,1886,44,0,6,410000 +2450,380,22,-1498,28,0,0,410000 +2451,5,38,15821,6,1,1,410000 +2452,15,29,10988,36,2,6,400000 +2453,449,18,5208,27,2,4,600000 +2454,383,50,2547,32,1,1,400000 +2455,331,7,15058,21,1,5,500000 +2456,406,42,2041,48,0,1,500000 +2457,201,21,6915,6,1,2,400000 +2458,424,44,19693,25,1,0,600000 +2459,369,0,-20,37,1,5,500000 +2460,214,40,9322,18,1,4,500000 +2461,102,10,-2220,16,0,0,500000 +2462,447,58,-3601,25,1,4,400000 +2463,151,39,4590,45,1,4,500000 +2464,153,61,15326,28,1,1,400000 +2465,218,13,-1056,7,1,0,400000 +2466,356,52,12155,49,1,1,500000 +2467,437,7,7516,42,1,5,400000 +2468,478,1,13230,16,1,7,600000 +2469,411,47,7345,12,0,6,600000 +2470,272,30,19763,40,1,4,400000 +2471,498,1,3933,47,1,5,410000 +2472,507,12,14108,11,0,1,410000 +2473,405,40,-4254,20,1,1,500000 +2474,170,55,3319,23,1,7,500000 +2475,68,47,11111,18,1,2,400000 +2476,61,14,1705,37,2,4,410000 +2477,188,58,1307,35,1,6,500000 +2478,334,41,1878,27,0,2,400000 +2479,162,33,-604,44,1,7,500000 +2480,339,35,19594,34,1,3,400000 +2481,156,36,10425,35,1,3,410000 +2482,376,54,12082,26,1,6,600000 +2483,187,19,19396,34,2,4,500000 +2484,335,29,176,8,1,0,400000 +2485,422,19,-1347,35,0,1,600000 +2486,28,30,19897,33,2,4,600000 +2487,168,17,9095,15,1,5,500000 +2488,473,44,7797,1,1,3,500000 +2489,388,20,9578,37,0,6,500000 +2490,41,20,3539,4,1,4,500000 +2491,146,13,9070,34,2,4,410000 +2492,296,42,16159,42,1,1,400000 +2493,416,62,15495,29,1,1,400000 +2494,390,32,-1655,32,0,5,400000 +2495,83,33,10495,32,1,6,400000 +2496,321,44,9150,22,1,7,500000 +2497,265,63,12188,24,1,5,400000 +2498,503,43,8659,38,0,0,410000 +2499,452,2,-2582,28,1,2,400000 +2500,446,0,14558,32,1,7,400000 +2501,342,4,4507,11,1,0,400000 +2502,456,13,4474,17,1,4,600000 +2503,371,7,-672,35,1,6,400000 +2504,444,11,1598,22,1,6,500000 +2505,72,52,-2286,39,1,2,600000 +2506,225,46,14216,27,2,7,400000 +2507,221,47,13978,19,0,0,410000 +2508,8,22,3827,38,1,3,400000 +2509,11,8,12197,17,0,7,500000 +2510,215,8,-4109,22,1,2,410000 +2511,467,7,-3545,12,1,2,600000 +2512,83,56,19053,26,1,3,410000 +2513,196,10,2787,40,1,3,400000 +2514,91,33,917,42,0,1,400000 +2515,140,9,19113,27,0,5,400000 +2516,489,18,13862,33,1,1,410000 +2517,254,45,-2328,5,1,3,600000 +2518,149,3,8876,48,1,6,600000 +2519,493,47,-4326,16,1,2,410000 +2520,277,29,15725,5,1,4,500000 +2521,218,25,2453,41,1,5,400000 +2522,54,5,16857,21,1,3,500000 +2523,400,20,12622,49,0,2,500000 +2524,81,21,-3946,21,1,3,500000 +2525,72,52,9317,22,1,7,410000 +2526,154,23,4505,41,1,5,410000 +2527,413,59,18539,14,1,2,600000 +2528,110,43,16052,40,2,2,600000 +2529,219,5,7512,38,1,5,600000 +2530,477,15,9137,23,1,7,500000 +2531,82,36,5422,12,1,2,600000 +2532,108,63,1040,5,1,7,600000 +2533,110,6,-1850,24,0,3,600000 +2534,314,39,1868,12,1,5,600000 +2535,487,13,2277,36,0,6,410000 +2536,137,14,3231,24,1,0,410000 +2537,421,36,14706,33,0,1,600000 +2538,241,13,16102,49,1,0,400000 +2539,102,11,-4426,13,2,1,410000 +2540,405,5,-2525,11,1,5,600000 +2541,271,43,-4886,49,1,2,410000 +2542,444,18,19734,39,1,6,600000 +2543,100,21,12984,16,1,1,500000 +2544,241,52,1152,9,1,5,500000 +2545,55,1,-1982,5,0,5,410000 +2546,222,49,13984,20,1,0,400000 +2547,38,42,17045,47,0,0,400000 +2548,194,34,11103,23,1,0,400000 +2549,152,46,-4121,40,1,0,600000 +2550,149,23,9311,12,1,3,400000 +2551,308,30,10950,34,0,3,410000 +2552,239,61,15179,10,1,4,410000 +2553,385,57,18975,30,1,0,400000 +2554,435,4,-560,23,1,4,410000 +2555,221,31,-4827,29,2,2,410000 +2556,275,39,10074,26,0,3,500000 +2557,44,6,9025,15,1,5,400000 +2558,39,54,16495,26,1,3,600000 +2559,30,34,18181,2,0,2,410000 +2560,253,22,13056,14,1,6,410000 +2561,273,58,-3285,30,0,0,600000 +2562,392,44,5578,47,1,5,410000 +2563,385,57,-3292,7,1,2,600000 +2564,74,33,4151,40,1,5,500000 +2565,300,26,-2761,32,1,3,500000 +2566,55,59,4219,42,0,0,410000 +2567,363,26,8242,18,1,1,400000 +2568,178,12,8601,19,0,5,400000 +2569,446,30,18906,24,1,6,600000 +2570,132,8,17888,10,2,0,400000 +2571,345,8,1835,48,0,7,600000 +2572,144,32,-4277,27,1,3,410000 +2573,444,32,10683,47,1,3,600000 +2574,338,2,18410,31,1,1,400000 +2575,399,12,6773,44,1,5,410000 +2576,265,55,19906,35,2,1,400000 +2577,63,21,14456,34,1,2,400000 +2578,67,3,17321,1,1,7,600000 +2579,274,41,7885,26,1,1,410000 +2580,112,11,7480,33,1,0,600000 +2581,178,1,13741,15,1,4,410000 +2582,228,37,47,35,1,5,400000 +2583,329,50,42,8,1,4,600000 +2584,6,15,11425,2,1,0,400000 +2585,139,14,14401,48,1,4,400000 +2586,396,26,19337,10,1,2,400000 +2587,83,17,-1399,7,0,2,410000 +2588,427,26,-498,30,0,6,410000 +2589,442,47,-1417,16,0,7,500000 +2590,215,42,4587,26,1,1,500000 +2591,295,33,13698,10,2,0,410000 +2592,363,42,12532,2,1,0,410000 +2593,429,28,-4618,36,1,1,410000 +2594,346,45,11211,7,1,5,400000 +2595,167,60,1681,20,1,6,600000 +2596,401,30,15787,41,0,5,600000 +2597,365,12,17348,24,2,3,600000 +2598,202,19,4595,47,1,6,410000 +2599,268,41,-1618,17,1,4,410000 +2600,392,19,17957,8,1,3,600000 +2601,257,59,-3254,24,0,3,410000 +2602,81,5,2866,8,1,4,400000 +2603,392,23,4359,26,2,1,500000 +2604,436,17,3647,25,1,7,600000 +2605,394,62,9134,39,0,5,500000 +2606,285,59,-768,6,1,5,400000 +2607,406,30,15755,1,1,6,500000 +2608,384,21,12844,1,0,2,500000 +2609,167,23,15217,43,1,1,400000 +2610,338,48,12257,43,1,6,600000 +2611,311,11,-3017,19,0,3,500000 +2612,157,24,19548,35,1,5,600000 +2613,188,57,10276,39,2,4,500000 +2614,70,33,14590,48,1,2,500000 +2615,280,43,12023,42,1,0,500000 +2616,345,36,19228,39,0,6,500000 +2617,135,14,16918,25,1,6,600000 +2618,311,46,-2451,10,2,5,410000 +2619,460,13,10439,19,0,3,410000 +2620,129,8,6912,24,2,0,500000 +2621,300,17,2082,49,1,3,410000 +2622,25,30,10107,49,1,2,410000 +2623,279,61,9535,18,1,5,500000 +2624,293,60,-4297,16,1,5,500000 +2625,457,39,17146,24,1,2,600000 +2626,377,11,-4145,44,1,0,410000 +2627,143,19,17703,9,2,1,600000 +2628,353,40,-2918,40,1,7,400000 +2629,26,30,6692,41,1,4,500000 +2630,426,5,259,45,1,4,600000 +2631,169,27,9253,37,1,2,410000 +2632,152,8,828,3,0,1,400000 +2633,184,36,1796,37,1,6,400000 +2634,391,55,17096,38,1,5,400000 +2635,169,35,2571,9,0,5,400000 +2636,385,16,-4687,48,1,2,400000 +2637,346,29,16123,13,2,0,500000 +2638,78,18,16924,48,0,7,600000 +2639,358,38,5761,2,1,0,410000 +2640,236,33,-3483,26,2,1,400000 +2641,169,1,9549,8,1,5,410000 +2642,288,37,10350,24,1,1,410000 +2643,297,21,5987,9,0,4,410000 +2644,198,14,1731,40,1,3,500000 +2645,243,55,12381,1,0,1,500000 +2646,475,40,-2581,19,1,5,600000 +2647,293,28,3567,33,1,4,500000 +2648,208,37,10173,7,1,7,500000 +2649,194,20,6898,25,1,3,410000 +2650,102,41,-4884,33,1,3,410000 +2651,195,14,9916,22,1,4,500000 +2652,446,11,217,33,2,6,500000 +2653,95,21,-2560,41,1,4,500000 +2654,414,15,6448,35,0,0,410000 +2655,2,1,3817,5,1,0,600000 +2656,14,16,-3898,2,0,7,410000 +2657,358,28,8161,41,2,7,410000 +2658,145,45,-4119,19,2,6,600000 +2659,246,23,14195,12,1,5,400000 +2660,325,6,19467,18,1,7,600000 +2661,27,12,13850,39,1,7,410000 +2662,483,59,10854,18,1,7,400000 +2663,329,40,7185,26,2,7,400000 +2664,330,25,15327,46,0,1,400000 +2665,259,35,17261,48,1,7,500000 +2666,130,8,15566,7,1,7,410000 +2667,510,6,14035,17,1,6,410000 +2668,166,19,-4292,33,1,1,400000 +2669,95,3,12355,2,2,4,410000 +2670,124,25,-4781,46,1,5,400000 +2671,154,38,4180,24,1,5,400000 +2672,471,28,697,25,0,1,400000 +2673,137,32,-3365,24,1,5,500000 +2674,63,46,5240,5,1,0,410000 +2675,27,0,2031,30,0,3,400000 +2676,56,57,-83,37,1,1,600000 +2677,25,20,16859,9,0,6,410000 +2678,322,14,-2387,44,1,0,400000 +2679,157,59,11712,15,0,7,400000 +2680,445,24,-116,5,1,5,400000 +2681,153,47,15314,30,0,3,600000 +2682,14,45,18646,22,2,5,500000 +2683,317,4,12406,9,1,4,500000 +2684,248,0,8504,40,1,0,500000 +2685,362,3,-1209,6,1,3,600000 +2686,60,49,-3061,44,1,5,500000 +2687,128,16,-2578,44,1,6,600000 +2688,115,23,3820,6,1,4,600000 +2689,465,49,-1390,37,1,7,600000 +2690,31,56,284,18,0,4,410000 +2691,281,60,-156,13,1,7,400000 +2692,247,31,4693,6,0,7,600000 +2693,76,1,5749,15,1,6,600000 +2694,406,15,16229,2,0,4,600000 +2695,451,42,12962,21,1,1,400000 +2696,489,35,-1392,7,1,6,400000 +2697,151,14,13522,48,1,0,410000 +2698,267,18,16962,3,2,2,600000 +2699,308,1,-3360,45,1,7,400000 +2700,10,43,-1116,11,1,6,600000 +2701,48,23,-4244,22,1,7,410000 +2702,228,49,-2659,13,1,5,410000 +2703,362,7,17748,44,1,3,600000 +2704,497,63,6307,17,1,6,410000 +2705,483,33,14106,47,2,0,410000 +2706,132,32,4278,3,1,5,410000 +2707,173,10,8177,3,1,4,400000 +2708,174,35,16378,23,2,2,600000 +2709,281,14,2994,21,1,4,410000 +2710,49,28,19533,31,2,7,500000 +2711,484,18,6552,8,1,5,400000 +2712,137,3,8488,31,1,7,410000 +2713,312,36,71,27,1,2,410000 +2714,216,13,2774,2,0,7,410000 +2715,105,55,15940,19,1,5,410000 +2716,312,24,13475,37,1,6,400000 +2717,170,35,5412,7,1,6,410000 +2718,145,33,7368,15,0,2,500000 +2719,51,31,18531,48,1,1,400000 +2720,309,16,15789,24,1,5,400000 +2721,294,34,18951,3,2,1,500000 +2722,242,59,-323,6,1,6,500000 +2723,495,32,-1241,16,2,6,500000 +2724,500,0,3818,11,2,5,500000 +2725,334,8,11137,21,1,1,410000 +2726,31,38,4271,9,1,1,410000 +2727,293,19,16248,35,1,6,600000 +2728,252,54,14618,35,0,6,400000 +2729,127,36,19801,16,1,0,400000 +2730,281,8,2372,6,0,6,600000 +2731,235,10,-1818,9,0,3,410000 +2732,434,42,11439,15,1,0,410000 +2733,311,49,10976,42,1,2,500000 +2734,172,50,18853,45,2,6,400000 +2735,496,41,3159,29,2,5,600000 +2736,415,60,16922,13,1,0,400000 +2737,341,3,11806,2,0,3,400000 +2738,386,49,6003,5,1,5,410000 +2739,186,37,18254,14,0,3,500000 +2740,182,14,-1247,13,1,3,410000 +2741,172,28,9867,16,1,4,500000 +2742,394,31,3366,47,0,3,410000 +2743,123,2,4665,1,0,3,410000 +2744,418,56,-3364,1,0,1,410000 +2745,333,59,11278,17,1,7,400000 +2746,358,44,8334,7,1,3,400000 +2747,219,32,15565,23,1,2,600000 +2748,237,13,8238,5,1,2,410000 +2749,189,9,15478,9,2,3,400000 +2750,89,10,4591,47,0,4,500000 +2751,479,52,12953,13,1,5,400000 +2752,388,5,-582,8,1,0,410000 +2753,491,29,11700,16,1,7,600000 +2754,157,52,2387,5,1,0,410000 +2755,200,30,188,46,1,2,400000 +2756,437,44,-1202,2,1,4,410000 +2757,197,38,6631,26,1,1,410000 +2758,4,36,13275,18,0,1,500000 +2759,392,53,-2981,39,1,5,410000 +2760,397,57,10153,34,0,7,410000 +2761,509,8,4926,30,0,7,400000 +2762,363,49,19966,44,1,6,400000 +2763,78,9,-2234,36,1,5,600000 +2764,441,25,4582,19,2,4,410000 +2765,478,37,-743,45,1,6,410000 +2766,184,4,18556,17,1,6,500000 +2767,177,8,12959,12,1,6,600000 +2768,45,40,6901,18,1,3,500000 +2769,339,59,8266,45,2,0,600000 +2770,301,27,11694,8,0,2,500000 +2771,243,46,-4881,33,1,6,410000 +2772,108,38,3641,30,2,2,600000 +2773,506,38,4590,8,1,1,600000 +2774,415,2,2258,48,1,4,600000 +2775,91,59,1425,47,1,6,410000 +2776,215,58,17027,20,1,2,400000 +2777,378,17,18112,13,1,7,410000 +2778,211,32,-1058,40,1,3,600000 +2779,347,7,-2802,22,1,0,410000 +2780,170,63,19784,38,0,5,400000 +2781,25,39,6523,10,1,0,400000 +2782,58,15,15651,40,2,1,410000 +2783,7,56,-4883,5,0,0,410000 +2784,455,26,19837,35,1,4,600000 +2785,289,50,19866,17,0,6,400000 +2786,407,49,-3859,45,1,5,500000 +2787,90,58,15019,2,2,4,410000 +2788,72,31,4710,23,1,3,410000 +2789,388,4,-1133,19,1,6,410000 +2790,186,15,13725,24,1,7,400000 +2791,118,29,4086,48,0,1,410000 +2792,488,7,1927,13,1,1,500000 +2793,59,0,8660,14,1,3,500000 +2794,176,34,3464,4,1,1,500000 +2795,408,33,-3950,28,0,0,600000 +2796,217,11,8741,48,1,6,600000 +2797,304,34,-2032,38,1,1,400000 +2798,252,41,-3986,20,1,6,600000 +2799,403,16,-2114,28,1,4,500000 +2800,241,16,16856,2,2,6,410000 +2801,509,62,-4384,49,0,5,400000 +2802,46,45,18867,8,2,3,600000 +2803,464,31,12425,22,0,5,500000 +2804,319,23,14630,30,1,0,500000 +2805,380,54,-4549,40,1,4,410000 +2806,422,0,10994,6,1,3,410000 +2807,345,61,18392,40,0,2,410000 +2808,177,29,-1138,6,0,2,600000 +2809,302,20,3576,18,1,2,600000 +2810,205,2,8116,7,1,4,400000 +2811,2,46,17742,48,1,5,400000 +2812,336,28,-3520,28,1,0,410000 +2813,430,24,4226,2,1,5,410000 +2814,313,2,9898,47,2,7,600000 +2815,497,0,1573,13,1,2,500000 +2816,179,7,-1312,25,1,2,400000 +2817,275,9,4178,18,1,3,600000 +2818,62,36,11849,2,0,1,500000 +2819,200,17,18123,28,1,5,410000 +2820,236,40,-814,14,0,4,410000 +2821,371,22,-2473,39,1,4,400000 +2822,352,11,4081,31,1,1,400000 +2823,44,63,13026,36,0,2,600000 +2824,191,36,13564,39,1,7,400000 +2825,73,20,10471,19,1,3,600000 +2826,309,0,8128,23,1,1,500000 +2827,230,2,5787,8,0,5,400000 +2828,328,37,-3336,43,1,0,410000 +2829,118,45,79,38,0,2,410000 +2830,362,21,15347,5,0,5,410000 +2831,1,28,13977,49,0,4,400000 +2832,392,10,-946,12,1,3,600000 +2833,352,11,12803,31,1,3,410000 +2834,315,44,-4453,18,1,0,400000 +2835,286,27,10882,37,1,3,600000 +2836,325,41,15259,35,1,2,500000 +2837,196,11,9805,4,0,4,500000 +2838,173,23,6212,32,0,2,500000 +2839,451,60,2169,37,1,5,410000 +2840,114,39,10621,38,1,6,400000 +2841,255,14,18562,41,1,7,400000 +2842,219,5,16672,43,1,2,410000 +2843,414,38,871,4,1,7,400000 +2844,163,27,57,38,1,4,600000 +2845,397,53,6636,15,1,2,500000 +2846,303,40,11229,36,1,6,600000 +2847,310,30,8760,6,1,3,400000 +2848,343,35,10828,18,0,2,400000 +2849,392,0,3957,37,1,0,500000 +2850,116,8,8014,30,2,1,400000 +2851,346,28,7309,41,1,6,600000 +2852,145,18,16165,48,1,3,400000 +2853,13,62,16047,43,1,0,410000 +2854,58,37,114,31,1,0,500000 +2855,341,48,-3071,14,1,7,500000 +2856,370,39,11801,8,1,6,400000 +2857,179,24,3619,42,1,7,500000 +2858,225,4,10813,32,1,1,410000 +2859,181,35,3961,19,1,7,500000 +2860,32,24,17080,7,1,3,400000 +2861,267,12,11387,10,1,4,400000 +2862,98,39,-3471,32,1,3,600000 +2863,377,46,7766,7,1,7,500000 +2864,55,60,10260,37,1,2,400000 +2865,240,33,13628,26,1,3,600000 +2866,463,37,6861,44,1,6,410000 +2867,188,0,16470,2,2,4,400000 +2868,159,39,18607,38,2,1,410000 +2869,506,5,14852,40,1,2,600000 +2870,487,25,9621,5,1,4,410000 +2871,174,4,2054,1,2,0,600000 +2872,438,26,3691,18,1,7,400000 +2873,5,33,11552,12,1,6,410000 +2874,230,40,4330,3,1,2,500000 +2875,278,40,1570,19,1,0,410000 +2876,397,43,11459,24,1,5,400000 +2877,456,48,-3675,49,0,0,600000 +2878,487,7,-4835,25,1,2,410000 +2879,135,34,4782,6,1,6,500000 +2880,474,57,3906,12,1,4,600000 +2881,167,20,3814,5,1,7,500000 +2882,107,3,19085,12,2,5,500000 +2883,6,48,13629,24,0,7,500000 +2884,302,31,-4639,24,1,2,400000 +2885,301,58,16238,32,1,4,500000 +2886,0,44,8498,9,0,5,600000 +2887,28,53,13254,41,1,1,400000 +2888,238,43,-4777,35,1,6,400000 +2889,188,34,-2481,34,1,3,410000 +2890,135,39,4264,44,1,6,600000 +2891,122,42,-1520,18,2,3,600000 +2892,195,58,1780,36,1,7,600000 +2893,248,19,3295,46,1,6,600000 +2894,127,16,15114,47,1,5,500000 +2895,400,8,-2703,39,1,4,600000 +2896,353,17,15397,32,1,5,400000 +2897,363,28,3784,22,1,0,410000 +2898,496,26,4259,24,0,1,410000 +2899,147,14,-1250,43,0,6,400000 +2900,167,39,-3890,14,1,3,500000 +2901,268,43,5671,26,1,2,410000 +2902,192,24,17425,5,1,0,400000 +2903,511,54,15075,15,0,7,500000 +2904,83,43,18295,7,1,2,600000 +2905,161,55,2951,34,1,1,600000 +2906,439,0,8494,37,1,6,500000 +2907,281,56,-2555,26,0,2,500000 +2908,230,6,11998,10,0,7,500000 +2909,86,44,-1030,13,1,3,500000 +2910,302,37,-4764,32,1,4,600000 +2911,395,52,12941,24,0,2,410000 +2912,205,33,3744,29,1,6,410000 +2913,433,56,15833,23,1,1,500000 +2914,302,40,16486,26,1,3,410000 +2915,499,20,3240,1,0,4,410000 +2916,125,38,-3322,34,1,0,500000 +2917,266,33,10368,42,1,7,400000 +2918,43,5,5801,14,1,0,400000 +2919,148,35,-4177,29,0,0,600000 +2920,307,38,6305,14,1,3,600000 +2921,473,2,4617,48,1,4,600000 +2922,277,7,13044,36,1,4,500000 +2923,389,29,15928,48,0,2,500000 +2924,288,40,4434,5,1,7,500000 +2925,267,41,14723,4,1,6,600000 +2926,207,11,4776,20,0,2,410000 +2927,219,28,-697,32,1,6,600000 +2928,221,51,7330,36,0,0,500000 +2929,29,26,6696,20,1,2,500000 +2930,275,30,-84,48,2,7,500000 +2931,365,56,332,37,1,4,500000 +2932,444,4,17620,30,1,0,600000 +2933,335,38,-1170,32,1,7,400000 +2934,283,7,4728,49,1,4,600000 +2935,365,44,13859,5,1,2,400000 +2936,366,42,262,6,0,7,410000 +2937,481,58,10183,26,1,5,500000 +2938,376,9,18554,30,0,4,410000 +2939,114,24,3058,11,1,3,410000 +2940,128,21,839,3,0,6,500000 +2941,112,50,18230,9,0,5,400000 +2942,65,18,-1068,39,1,6,600000 +2943,287,59,15439,43,2,4,400000 +2944,1,33,12403,32,2,6,500000 +2945,231,56,17741,12,1,2,400000 +2946,150,61,12840,17,1,6,410000 +2947,318,54,9930,1,1,2,500000 +2948,257,41,17364,15,1,4,500000 +2949,167,52,10320,9,1,5,400000 +2950,366,19,19416,4,1,2,400000 +2951,23,1,-3602,24,1,5,410000 +2952,348,58,15384,2,1,3,600000 +2953,99,48,-3743,24,0,2,600000 +2954,48,58,13823,41,0,1,500000 +2955,387,55,18368,42,0,3,500000 +2956,261,20,-1832,10,0,6,600000 +2957,266,7,-380,41,0,0,400000 +2958,33,31,6952,49,1,3,410000 +2959,487,6,19321,32,1,0,500000 +2960,38,2,2264,42,1,5,400000 +2961,10,22,10089,18,1,3,600000 +2962,149,31,3734,11,1,3,500000 +2963,136,34,19851,33,1,0,500000 +2964,509,9,1655,12,0,4,400000 +2965,299,22,1550,29,1,6,500000 +2966,432,50,16237,14,1,2,400000 +2967,188,47,19781,39,0,6,400000 +2968,13,6,6813,26,1,3,500000 +2969,62,50,13546,29,0,7,600000 +2970,72,47,-923,45,1,2,600000 +2971,122,56,-3295,15,1,1,500000 +2972,387,11,5425,44,0,3,600000 +2973,396,11,18681,26,1,0,500000 +2974,506,8,-2786,49,1,5,600000 +2975,360,35,16464,19,1,3,400000 +2976,70,48,3173,45,1,6,410000 +2977,455,55,-2762,19,1,2,500000 +2978,141,9,4134,30,1,5,400000 +2979,41,42,-2464,18,0,4,500000 +2980,297,53,-4543,24,1,5,600000 +2981,490,33,12809,1,1,7,600000 +2982,470,48,-1852,29,1,6,400000 +2983,318,59,10226,48,1,5,600000 +2984,433,4,7566,15,0,4,400000 +2985,353,37,1272,42,1,4,400000 +2986,448,12,1230,13,1,7,500000 +2987,217,38,-4997,38,1,5,500000 +2988,360,28,10699,34,1,1,600000 +2989,130,16,9104,46,1,3,600000 +2990,273,46,2985,23,0,1,410000 +2991,122,63,19197,31,1,3,600000 +2992,413,27,3885,26,1,3,400000 +2993,480,49,17022,13,1,1,600000 +2994,459,58,17594,33,1,4,400000 +2995,329,42,1077,34,1,0,600000 +2996,305,50,19355,34,1,4,410000 +2997,150,35,-1610,44,0,7,400000 +2998,151,57,7525,48,1,4,600000 +2999,202,2,3891,35,0,7,410000 +3000,431,35,-62,45,1,2,500000 +3001,222,9,7443,25,1,2,500000 +3002,398,14,10022,1,0,6,410000 +3003,222,32,6083,17,2,0,410000 +3004,445,23,10477,9,2,4,600000 +3005,178,28,12613,47,1,0,600000 +3006,407,35,-1272,44,0,5,600000 +3007,327,60,11806,47,0,1,600000 +3008,108,20,18813,6,1,5,500000 +3009,302,18,4126,34,1,3,500000 +3010,113,53,10393,1,1,4,600000 +3011,370,14,15002,34,1,6,500000 +3012,410,11,1479,45,0,7,600000 +3013,294,17,10751,31,0,1,600000 +3014,385,11,-1187,32,1,5,410000 +3015,377,32,5095,8,1,0,400000 +3016,142,25,16461,40,2,3,400000 +3017,130,28,-3716,42,0,1,410000 +3018,31,22,-1912,38,1,0,400000 +3019,268,17,-2638,28,1,2,600000 +3020,490,52,8091,15,1,3,400000 +3021,65,26,14591,26,0,5,600000 +3022,319,21,-4784,9,2,0,500000 +3023,325,27,-2221,23,1,1,400000 +3024,359,51,8251,35,1,4,600000 +3025,245,11,-1343,22,1,3,400000 +3026,141,10,16066,9,2,6,410000 +3027,356,18,16139,44,2,0,600000 +3028,203,18,14583,4,2,7,500000 +3029,398,35,-50,10,1,4,400000 +3030,194,22,1510,11,1,4,400000 +3031,359,36,4547,2,2,0,400000 +3032,341,54,10048,17,1,7,410000 +3033,165,48,2597,17,1,5,500000 +3034,391,44,11631,39,1,3,500000 +3035,235,22,5684,38,1,3,500000 +3036,61,6,6864,13,1,6,600000 +3037,367,37,-1491,48,1,2,500000 +3038,349,29,-3235,36,1,3,410000 +3039,234,38,3957,6,1,1,600000 +3040,206,9,185,35,1,0,600000 +3041,196,23,18881,31,1,7,400000 +3042,232,22,1402,38,1,3,400000 +3043,135,17,3793,11,0,3,410000 +3044,58,27,13157,10,1,6,410000 +3045,376,61,-1251,11,1,7,500000 +3046,355,60,8560,34,0,7,500000 +3047,163,33,-1534,34,1,5,400000 +3048,82,36,10068,19,1,5,600000 +3049,500,39,-936,25,0,5,600000 +3050,366,20,-1564,26,0,7,600000 +3051,235,8,12434,45,1,1,410000 +3052,290,33,13602,10,1,2,400000 +3053,313,31,10995,22,1,3,500000 +3054,442,61,19977,8,1,3,600000 +3055,238,24,969,20,1,6,410000 +3056,434,10,442,21,0,0,410000 +3057,206,27,18118,15,1,0,400000 +3058,450,10,4886,34,1,0,600000 +3059,132,48,-30,33,1,3,500000 +3060,338,43,12535,35,1,2,410000 +3061,30,16,-1166,13,1,6,410000 +3062,132,37,17998,30,1,0,400000 +3063,269,18,14938,20,2,3,410000 +3064,395,17,19018,43,1,3,400000 +3065,82,16,10432,8,1,5,410000 +3066,389,19,3367,14,2,6,410000 +3067,219,33,2411,14,1,6,400000 +3068,99,41,-3645,22,1,2,600000 +3069,350,32,-1480,26,0,0,410000 +3070,204,62,10057,13,1,0,410000 +3071,24,1,6684,45,1,3,410000 +3072,112,18,2291,36,1,4,400000 +3073,355,17,-323,40,0,2,500000 +3074,478,52,12685,22,1,3,400000 +3075,94,39,18115,18,0,2,600000 +3076,262,28,12780,21,1,1,500000 +3077,166,42,-1225,29,0,7,400000 +3078,486,2,-4655,25,0,4,410000 +3079,128,1,-1328,49,0,6,410000 +3080,69,47,9401,23,1,4,410000 +3081,273,14,-64,32,0,5,500000 +3082,492,37,12526,33,1,6,400000 +3083,362,59,-2717,42,1,5,410000 +3084,60,2,8777,24,1,7,400000 +3085,285,40,7930,36,1,3,600000 +3086,131,15,18109,1,0,4,400000 +3087,267,2,3399,5,1,5,500000 +3088,459,25,14492,1,1,6,400000 +3089,10,44,17992,35,1,2,400000 +3090,119,44,1592,38,1,3,500000 +3091,290,27,10577,19,1,2,600000 +3092,498,27,9030,15,1,4,400000 +3093,511,1,9819,47,1,2,400000 +3094,464,23,-2151,6,1,2,410000 +3095,326,28,18548,18,1,1,600000 +3096,448,10,6976,18,1,5,410000 +3097,186,38,19951,18,0,7,500000 +3098,109,35,9424,20,2,4,500000 +3099,410,17,6668,5,0,3,500000 +3100,395,12,12239,9,1,6,410000 +3101,469,21,2170,49,1,4,410000 +3102,466,35,8567,25,0,5,410000 +3103,417,55,-2403,8,1,6,500000 +3104,327,9,19066,40,1,6,600000 +3105,187,27,5044,38,0,4,600000 +3106,173,22,5279,21,1,0,600000 +3107,62,34,-815,49,1,4,600000 +3108,23,6,18071,25,1,3,500000 +3109,276,17,12940,11,1,7,400000 +3110,374,2,-971,38,1,3,410000 +3111,147,39,8518,27,1,6,600000 +3112,137,6,-1790,47,1,3,400000 +3113,407,7,7328,10,1,4,600000 +3114,6,49,9083,49,1,4,410000 +3115,496,41,10264,11,1,7,600000 +3116,408,29,4507,29,1,7,500000 +3117,372,24,5791,13,1,2,410000 +3118,99,62,14682,17,1,2,400000 +3119,315,62,-4955,43,1,5,500000 +3120,428,42,-3150,19,2,1,600000 +3121,228,62,17086,23,1,1,400000 +3122,296,27,4057,6,1,4,400000 +3123,120,32,13352,31,1,2,410000 +3124,454,55,1455,18,1,4,500000 +3125,266,38,11700,8,2,2,500000 +3126,218,23,12862,15,1,4,400000 +3127,415,30,17609,21,2,0,410000 +3128,475,34,15591,46,1,6,500000 +3129,396,24,15533,29,1,0,400000 +3130,378,13,4534,22,2,5,410000 +3131,29,36,3685,44,1,5,400000 +3132,282,56,903,22,1,6,600000 +3133,154,26,-4653,12,1,0,410000 +3134,51,7,8277,17,1,4,600000 +3135,106,8,12574,23,1,6,410000 +3136,460,55,16798,28,1,5,500000 +3137,224,14,642,30,1,0,600000 +3138,321,29,17808,1,0,2,500000 +3139,31,34,-4690,10,1,6,500000 +3140,262,20,10728,20,0,1,400000 +3141,325,29,-2902,2,1,3,400000 +3142,160,35,8852,40,0,2,500000 +3143,43,4,9525,25,1,7,600000 +3144,184,19,6851,38,2,5,500000 +3145,464,14,13046,32,1,6,500000 +3146,208,51,7557,45,1,4,400000 +3147,390,8,17957,41,1,5,400000 +3148,154,53,2109,36,1,3,600000 +3149,445,59,-947,16,1,7,600000 +3150,227,52,12719,7,1,3,410000 +3151,144,10,-4903,20,1,5,410000 +3152,165,58,1880,43,1,6,500000 +3153,495,60,15599,6,1,1,500000 +3154,458,53,8302,26,0,4,400000 +3155,392,57,-2245,49,1,5,600000 +3156,64,17,-609,39,1,4,410000 +3157,304,59,-2785,13,1,6,600000 +3158,334,24,11969,41,2,6,500000 +3159,335,44,2366,34,1,6,400000 +3160,275,6,-4000,7,1,2,500000 +3161,272,8,2269,29,1,7,410000 +3162,86,27,9115,15,1,0,600000 +3163,161,19,2622,3,1,4,410000 +3164,333,8,19761,38,2,0,500000 +3165,322,57,17253,34,1,0,500000 +3166,295,50,-4983,11,1,1,600000 +3167,107,40,7853,23,1,0,600000 +3168,472,5,3143,19,1,6,410000 +3169,312,10,-541,44,0,0,410000 +3170,145,18,-529,20,1,0,600000 +3171,500,30,4960,4,1,3,600000 +3172,409,40,6436,28,1,1,400000 +3173,63,33,1765,31,1,2,600000 +3174,324,54,18848,39,1,5,600000 +3175,482,12,12343,6,1,3,500000 +3176,330,7,-4920,4,1,3,410000 +3177,237,29,14576,4,0,5,400000 +3178,129,14,15892,42,2,6,600000 +3179,495,46,9470,22,2,0,600000 +3180,377,5,3472,12,0,4,400000 +3181,270,48,10492,4,1,7,500000 +3182,330,7,17833,41,0,2,400000 +3183,185,38,1597,10,1,1,500000 +3184,108,24,17712,30,1,5,400000 +3185,205,13,2273,27,1,2,400000 +3186,150,7,15668,26,1,4,400000 +3187,479,5,2049,22,2,1,500000 +3188,44,38,2172,43,0,2,500000 +3189,124,51,-3000,3,1,7,500000 +3190,82,3,-1520,42,1,3,410000 +3191,265,39,13948,38,1,0,400000 +3192,470,1,-3767,25,0,4,600000 +3193,105,11,17011,48,1,7,600000 +3194,92,52,1827,19,0,0,400000 +3195,235,54,-700,47,1,5,410000 +3196,255,24,11174,14,1,0,500000 +3197,121,57,15141,31,1,5,500000 +3198,428,42,13387,12,1,6,500000 +3199,329,17,16288,37,2,2,500000 +3200,224,20,18021,38,1,5,600000 +3201,441,27,10117,48,0,2,400000 +3202,7,0,17885,20,2,5,400000 +3203,46,11,-3037,15,1,0,500000 +3204,170,62,-3672,4,1,2,600000 +3205,228,9,16989,15,0,7,500000 +3206,440,34,-3092,5,1,3,400000 +3207,257,5,-3034,15,0,6,400000 +3208,142,0,11780,5,1,2,410000 +3209,403,42,8308,28,1,5,500000 +3210,4,1,12860,32,1,3,410000 +3211,505,7,16417,41,0,2,600000 +3212,474,56,6809,14,0,0,600000 +3213,212,24,-740,12,0,1,600000 +3214,275,14,4365,22,1,1,600000 +3215,347,50,11067,13,1,2,400000 +3216,401,24,-1229,49,2,0,500000 +3217,413,19,7026,4,1,5,410000 +3218,449,40,5905,19,1,7,600000 +3219,312,52,-1703,21,2,0,600000 +3220,408,3,17254,40,2,4,400000 +3221,44,32,2721,20,0,2,400000 +3222,218,16,12391,33,1,1,500000 +3223,22,30,14563,13,1,6,500000 +3224,122,39,18341,45,1,0,500000 +3225,329,39,-432,4,1,2,400000 +3226,352,24,1797,40,1,5,600000 +3227,324,33,-4800,36,0,6,400000 +3228,369,38,14898,39,1,0,500000 +3229,373,18,-421,46,1,4,600000 +3230,42,13,6217,36,1,5,600000 +3231,60,39,11953,48,0,1,410000 +3232,239,59,4098,11,1,4,600000 +3233,295,41,6614,44,1,2,500000 +3234,51,45,19232,12,1,1,600000 +3235,14,37,12382,38,1,2,410000 +3236,369,32,18015,25,2,4,600000 +3237,316,6,565,39,0,0,410000 +3238,398,54,14422,5,1,0,410000 +3239,420,11,2469,29,1,0,500000 +3240,7,25,12817,45,0,4,600000 +3241,459,20,6158,12,2,4,500000 +3242,138,2,13759,44,2,0,410000 +3243,100,8,6271,25,1,0,600000 +3244,463,57,18107,21,1,5,410000 +3245,504,10,7189,23,1,3,600000 +3246,154,29,1630,27,1,2,600000 +3247,422,39,14915,25,1,2,600000 +3248,331,47,5821,35,2,2,500000 +3249,201,18,17368,5,0,1,400000 +3250,245,37,14262,49,1,5,410000 +3251,223,48,-3293,16,1,2,500000 +3252,410,23,-4616,35,1,7,410000 +3253,49,57,542,32,1,2,500000 +3254,89,63,1218,46,2,5,500000 +3255,450,39,264,3,1,7,410000 +3256,466,36,19510,38,0,3,500000 +3257,379,45,6657,19,1,0,410000 +3258,290,49,-611,18,1,5,410000 +3259,427,25,17029,32,1,6,410000 +3260,470,49,9990,41,2,5,400000 +3261,213,17,8545,4,1,6,400000 +3262,194,11,18120,34,0,6,410000 +3263,231,53,-2447,14,0,2,400000 +3264,10,45,1083,48,1,0,500000 +3265,310,62,1775,39,1,5,410000 +3266,136,1,16713,41,2,6,500000 +3267,415,45,18109,7,0,6,410000 +3268,258,30,-2293,28,1,1,410000 +3269,96,8,12165,27,0,6,500000 +3270,303,63,12484,49,1,6,400000 +3271,410,14,-4860,4,1,7,410000 +3272,54,35,12842,39,2,4,600000 +3273,489,8,17510,3,1,6,410000 +3274,242,26,3577,27,1,1,410000 +3275,328,4,-1159,4,1,1,410000 +3276,329,26,9067,46,1,4,400000 +3277,416,47,17141,40,1,5,400000 +3278,413,36,8069,5,1,5,400000 +3279,226,26,-100,20,1,1,500000 +3280,273,31,361,49,1,0,600000 +3281,409,51,13696,3,1,4,600000 +3282,174,19,5725,1,1,5,600000 +3283,242,46,-4515,15,0,7,410000 +3284,32,25,8966,5,1,2,500000 +3285,184,50,17889,31,1,7,500000 +3286,333,63,19301,27,1,0,410000 +3287,127,21,9698,19,1,5,500000 +3288,184,26,3715,31,1,7,400000 +3289,61,37,6277,27,0,4,500000 +3290,186,11,8242,1,1,4,600000 +3291,226,17,17895,33,1,5,500000 +3292,172,59,14316,30,1,0,400000 +3293,102,40,-2761,43,1,1,500000 +3294,352,36,-4650,22,1,2,410000 +3295,145,2,-121,26,1,1,400000 +3296,184,13,-4470,36,1,7,410000 +3297,99,32,16279,18,1,6,500000 +3298,270,10,-574,2,2,4,400000 +3299,17,35,7151,41,1,7,600000 +3300,329,33,-426,36,1,5,400000 +3301,329,11,11703,27,1,1,400000 +3302,260,22,11222,28,0,6,400000 +3303,274,50,3948,33,1,0,500000 +3304,113,58,11443,34,1,7,410000 +3305,249,12,5182,23,1,4,400000 +3306,432,2,9477,2,1,5,500000 +3307,84,8,6649,18,0,4,410000 +3308,400,31,-71,2,0,3,600000 +3309,345,53,8693,13,2,6,500000 +3310,243,8,18740,24,1,5,400000 +3311,161,39,11677,38,1,0,600000 +3312,222,32,11568,40,0,0,600000 +3313,331,58,19336,15,1,0,500000 +3314,82,29,16676,19,2,0,500000 +3315,458,4,19342,29,2,5,400000 +3316,469,46,18090,10,1,5,500000 +3317,470,56,6558,3,2,7,410000 +3318,335,6,3281,43,1,1,410000 +3319,58,28,12792,17,1,5,400000 +3320,203,25,-1875,26,1,1,500000 +3321,412,45,3802,21,1,6,410000 +3322,264,55,16637,28,0,4,410000 +3323,403,14,-3061,14,1,7,500000 +3324,302,43,-1089,47,0,5,600000 +3325,430,5,2834,48,0,4,410000 +3326,35,61,-3725,17,2,1,600000 +3327,446,18,16896,19,1,4,600000 +3328,322,42,6567,40,0,0,400000 +3329,437,32,14360,30,2,6,400000 +3330,482,45,16860,16,1,2,400000 +3331,418,2,6476,33,2,6,400000 +3332,309,3,15546,1,1,4,600000 +3333,410,59,9820,45,0,2,500000 +3334,66,47,-4586,46,1,4,410000 +3335,232,60,17292,31,1,3,600000 +3336,251,35,15033,23,1,2,410000 +3337,380,63,19942,13,0,5,400000 +3338,253,62,11247,17,1,1,410000 +3339,483,42,-4507,41,1,5,500000 +3340,457,18,3483,47,1,5,500000 +3341,29,21,16777,38,1,7,400000 +3342,380,17,8544,4,2,5,500000 +3343,287,63,-2621,24,1,1,500000 +3344,157,22,7824,38,0,5,500000 +3345,185,39,17502,20,1,5,600000 +3346,418,30,11770,40,1,3,600000 +3347,406,4,13553,41,0,3,600000 +3348,343,15,8296,48,1,6,500000 +3349,388,36,16898,40,2,1,400000 +3350,219,36,11945,25,1,0,400000 +3351,279,6,-2587,29,0,1,500000 +3352,178,20,-3912,20,1,7,600000 +3353,491,5,-2583,22,2,0,500000 +3354,224,18,18416,7,0,7,500000 +3355,411,52,9610,7,0,4,500000 +3356,453,11,13168,35,1,5,500000 +3357,258,17,12225,7,1,0,500000 +3358,239,10,16406,28,1,0,400000 +3359,315,47,12902,17,0,7,410000 +3360,62,54,9775,29,1,1,600000 +3361,502,17,18334,7,0,7,600000 +3362,314,59,-4277,33,1,0,400000 +3363,5,17,2604,14,2,2,400000 +3364,49,48,19011,48,1,6,400000 +3365,491,26,3880,9,1,2,410000 +3366,218,57,17686,30,1,1,500000 +3367,321,61,14298,37,2,4,600000 +3368,70,42,15581,12,1,4,400000 +3369,469,9,3332,25,2,0,400000 +3370,201,47,16871,14,1,3,500000 +3371,219,44,-501,35,1,2,500000 +3372,39,62,-3878,11,1,5,600000 +3373,37,6,12059,41,0,0,500000 +3374,182,52,2162,26,2,3,600000 +3375,312,40,11413,2,1,6,600000 +3376,297,13,1441,32,0,3,400000 +3377,100,38,10138,37,1,5,410000 +3378,360,34,10632,48,1,6,500000 +3379,258,25,3050,24,1,6,400000 +3380,279,16,10576,24,1,5,600000 +3381,412,10,12940,45,1,6,600000 +3382,185,33,5854,46,1,4,400000 +3383,317,58,17854,45,1,7,500000 +3384,317,56,4189,4,1,4,600000 +3385,420,1,19973,45,0,1,500000 +3386,102,2,10366,27,0,7,410000 +3387,484,23,10821,17,1,1,410000 +3388,285,3,6800,29,1,6,410000 +3389,254,39,-1106,26,1,1,410000 +3390,100,13,12318,27,2,5,400000 +3391,151,62,234,45,1,5,410000 +3392,506,47,1401,45,1,5,600000 +3393,181,52,-2959,16,1,7,600000 +3394,227,12,-2839,17,1,4,400000 +3395,7,17,8236,49,1,3,500000 +3396,135,22,223,18,1,7,400000 +3397,292,50,10243,17,1,7,500000 +3398,481,9,6305,22,1,3,500000 +3399,136,53,4593,16,1,5,400000 +3400,280,62,6092,33,1,6,600000 +3401,83,59,12666,34,0,1,410000 +3402,412,17,15735,39,0,2,400000 +3403,129,10,5555,6,1,6,500000 +3404,125,46,16765,2,2,2,600000 +3405,193,32,-4629,48,2,6,410000 +3406,196,41,10693,1,1,7,600000 +3407,397,8,3264,40,0,2,600000 +3408,32,33,10188,35,1,4,400000 +3409,171,4,14824,9,0,1,500000 +3410,228,24,13590,11,1,0,500000 +3411,454,20,10476,25,1,1,600000 +3412,4,44,15232,43,1,1,600000 +3413,82,19,16480,19,0,6,400000 +3414,93,45,3381,28,0,4,400000 +3415,316,15,4727,42,1,5,410000 +3416,117,13,11830,16,1,4,500000 +3417,272,24,6873,16,1,3,410000 +3418,9,53,-425,29,1,4,410000 +3419,476,28,-247,9,1,6,500000 +3420,438,43,9306,25,0,2,400000 +3421,220,11,-360,9,1,0,410000 +3422,480,18,-4830,26,1,6,600000 +3423,479,32,102,4,1,4,400000 +3424,39,36,11798,25,1,1,600000 +3425,179,46,8398,40,1,7,600000 +3426,172,13,10059,34,2,6,500000 +3427,51,15,4267,24,1,0,600000 +3428,305,29,-4962,49,1,0,410000 +3429,495,50,943,38,1,2,410000 +3430,59,19,15035,15,1,3,410000 +3431,107,34,17491,14,1,5,500000 +3432,65,24,-2076,10,1,2,410000 +3433,57,20,19570,36,2,0,600000 +3434,287,51,17175,24,1,4,410000 +3435,389,0,14982,40,0,7,600000 +3436,128,10,-2790,1,1,3,410000 +3437,459,35,-3128,10,0,2,400000 +3438,21,50,11795,11,1,2,410000 +3439,383,63,5050,39,1,7,410000 +3440,32,57,270,34,2,6,410000 +3441,169,22,-2194,18,1,7,400000 +3442,225,52,18876,32,0,2,500000 +3443,430,27,14804,29,1,2,410000 +3444,233,1,16219,44,1,2,410000 +3445,90,6,8954,4,1,6,400000 +3446,498,42,3789,4,1,7,600000 +3447,476,26,-3547,36,1,2,400000 +3448,231,20,-3985,33,2,1,400000 +3449,157,41,7401,33,0,1,400000 +3450,85,39,6110,2,1,2,400000 +3451,367,13,3333,1,1,4,410000 +3452,240,0,9916,3,1,0,400000 +3453,95,23,12120,10,1,3,400000 +3454,280,44,18411,39,1,1,600000 +3455,428,12,14569,22,1,0,410000 +3456,445,14,8238,42,0,0,600000 +3457,179,29,16846,40,0,4,600000 +3458,332,10,34,37,1,2,400000 +3459,128,10,5407,18,0,1,400000 +3460,456,56,11165,25,0,5,410000 +3461,311,30,5105,1,1,3,600000 +3462,21,58,19831,45,1,5,410000 +3463,184,23,3727,8,1,3,500000 +3464,228,27,-212,4,1,2,410000 +3465,489,36,12975,38,1,1,410000 +3466,8,50,-1045,39,1,6,500000 +3467,507,34,414,18,0,4,500000 +3468,129,51,-2044,44,1,7,600000 +3469,52,30,5270,34,1,5,400000 +3470,499,10,5972,16,1,0,500000 +3471,246,35,-1753,46,0,7,400000 +3472,6,27,19393,48,1,2,600000 +3473,329,49,-2044,32,1,3,600000 +3474,91,15,19500,28,1,6,600000 +3475,174,18,3585,8,1,4,500000 +3476,436,14,-3565,9,1,7,410000 +3477,412,9,2610,9,0,1,500000 +3478,268,13,-4129,27,0,7,400000 +3479,8,8,337,40,1,4,500000 +3480,323,22,6109,30,0,6,500000 +3481,101,50,16620,12,1,3,500000 +3482,98,55,16775,38,1,6,600000 +3483,137,59,9758,20,1,5,500000 +3484,214,3,9405,8,1,5,400000 +3485,302,59,-564,13,1,3,400000 +3486,460,37,10711,43,1,3,400000 +3487,307,16,-3554,25,0,7,400000 +3488,30,20,16441,47,0,7,410000 +3489,294,1,1700,44,1,4,500000 +3490,189,24,5935,6,1,1,410000 +3491,365,55,-586,26,1,1,600000 +3492,455,16,7910,13,0,1,400000 +3493,203,15,13055,36,0,3,400000 +3494,76,63,1144,6,1,2,400000 +3495,136,9,13648,42,2,1,500000 +3496,188,4,-856,3,2,3,410000 +3497,312,49,2539,42,1,1,400000 +3498,166,14,-1887,45,1,5,400000 +3499,465,36,9038,14,1,4,400000 +3500,410,10,17197,42,1,3,400000 +3501,274,19,1510,10,0,7,600000 +3502,306,19,7963,12,1,6,400000 +3503,139,40,-1812,32,0,0,400000 +3504,352,63,6212,19,1,0,400000 +3505,458,30,-3827,49,1,4,410000 +3506,152,49,-1405,35,1,4,600000 +3507,238,13,18768,10,1,2,600000 +3508,64,38,14973,38,1,1,600000 +3509,213,58,-2633,10,1,0,600000 +3510,58,15,-4349,33,1,3,400000 +3511,247,48,9859,37,1,5,410000 +3512,470,33,-950,11,1,6,400000 +3513,472,16,7532,9,0,0,500000 +3514,68,46,153,20,1,1,400000 +3515,278,2,3607,40,1,7,410000 +3516,237,15,14022,38,1,6,600000 +3517,359,17,-578,4,1,4,600000 +3518,139,60,410,9,2,0,600000 +3519,132,22,7076,6,1,4,400000 +3520,214,55,8956,48,1,7,600000 +3521,301,54,10447,47,0,3,400000 +3522,444,5,18191,16,0,7,400000 +3523,30,15,1121,7,1,6,500000 +3524,228,8,964,4,1,2,600000 +3525,255,26,14849,43,1,6,500000 +3526,242,17,-1936,11,1,5,410000 +3527,344,29,11696,26,1,2,400000 +3528,320,40,-3116,28,1,2,500000 +3529,109,59,7764,40,2,1,410000 +3530,454,54,19040,1,0,7,600000 +3531,277,38,1325,48,1,6,410000 +3532,199,1,8283,2,1,4,500000 +3533,51,55,14800,16,0,6,410000 +3534,270,22,8763,49,2,3,500000 +3535,310,10,18687,23,1,5,600000 +3536,347,45,18205,35,1,3,400000 +3537,384,20,8300,16,1,0,400000 +3538,383,55,5869,43,1,6,500000 +3539,146,20,-303,4,0,5,600000 +3540,84,58,5726,46,0,6,400000 +3541,18,60,19787,14,1,5,600000 +3542,234,12,-1164,38,1,5,500000 +3543,243,37,6434,47,1,0,600000 +3544,490,9,-462,25,1,5,600000 +3545,196,54,16210,5,0,5,410000 +3546,82,55,4251,17,1,5,400000 +3547,371,9,13779,13,0,4,410000 +3548,64,58,-3025,30,2,6,500000 +3549,459,55,18370,25,1,0,500000 +3550,125,18,19877,22,1,0,410000 +3551,151,44,10202,29,1,1,410000 +3552,356,23,5327,26,1,2,400000 +3553,459,20,15029,26,1,0,400000 +3554,371,57,9316,33,0,1,400000 +3555,212,41,11171,18,0,1,400000 +3556,243,53,-3589,43,1,7,600000 +3557,198,12,15771,43,1,2,400000 +3558,319,26,3673,3,0,7,500000 +3559,390,31,2937,15,0,3,400000 +3560,324,16,5570,18,1,5,600000 +3561,96,49,4820,9,0,0,410000 +3562,347,21,-3270,16,1,5,400000 +3563,20,40,2529,9,1,4,400000 +3564,89,32,15113,22,2,4,600000 +3565,209,34,5571,23,1,5,500000 +3566,175,31,14260,15,1,5,410000 +3567,84,23,15557,23,1,7,500000 +3568,396,43,18006,7,1,3,410000 +3569,483,21,-4378,34,1,6,410000 +3570,403,61,-1641,10,1,3,500000 +3571,242,27,12840,47,1,4,500000 +3572,295,24,-681,40,1,2,400000 +3573,253,47,16350,18,1,4,500000 +3574,109,5,6699,22,0,3,400000 +3575,162,20,6460,37,0,7,400000 +3576,338,39,242,5,1,6,400000 +3577,494,0,7050,14,1,2,600000 +3578,281,16,8889,46,1,3,410000 +3579,141,15,17394,37,1,3,600000 +3580,39,42,15085,11,1,3,500000 +3581,250,25,5163,44,2,4,410000 +3582,0,57,16028,8,0,0,600000 +3583,187,45,9314,42,2,0,500000 +3584,73,61,-279,3,1,7,600000 +3585,325,42,-2260,40,2,2,410000 +3586,247,47,13404,6,0,4,410000 +3587,117,24,12864,40,1,7,500000 +3588,316,52,18905,13,1,1,410000 +3589,496,38,-3697,22,1,5,600000 +3590,346,48,6799,44,1,4,500000 +3591,32,33,1622,25,1,4,500000 +3592,414,42,16428,5,1,5,600000 +3593,66,7,14419,25,1,3,500000 +3594,314,50,9602,37,1,3,410000 +3595,92,5,6042,41,1,6,500000 +3596,275,14,7856,48,1,7,500000 +3597,355,32,12733,26,1,5,410000 +3598,158,3,-254,16,2,5,600000 +3599,443,57,-191,17,1,2,500000 +3600,347,55,1622,22,1,4,600000 +3601,478,32,19017,23,1,2,500000 +3602,437,21,-2478,10,2,4,400000 +3603,208,46,-1590,5,2,2,500000 +3604,242,49,1624,39,1,2,400000 +3605,285,33,15256,46,1,1,410000 +3606,375,30,15832,47,2,5,410000 +3607,144,29,13288,5,1,7,500000 +3608,396,62,12681,31,0,7,500000 +3609,151,18,15527,26,1,1,600000 +3610,189,41,9683,20,1,4,400000 +3611,131,28,10493,40,2,2,600000 +3612,507,56,6558,41,1,7,500000 +3613,452,60,201,13,1,2,600000 +3614,297,2,5864,28,0,4,410000 +3615,191,5,-4361,40,0,5,600000 +3616,206,15,2690,21,0,5,410000 +3617,471,24,11541,25,1,6,400000 +3618,267,22,9086,30,2,3,500000 +3619,318,46,-3009,41,1,1,410000 +3620,327,62,-353,22,1,7,500000 +3621,337,41,3032,16,1,7,500000 +3622,171,12,6380,13,1,1,600000 +3623,257,25,1670,38,0,6,600000 +3624,116,38,16664,18,1,5,410000 +3625,240,63,13087,2,1,4,600000 +3626,223,27,-3692,3,1,6,410000 +3627,381,38,4547,20,0,4,600000 +3628,510,49,19760,26,1,4,400000 +3629,351,12,2984,9,1,0,600000 +3630,384,60,15675,5,0,2,400000 +3631,219,61,15927,14,1,2,500000 +3632,315,27,16479,12,2,4,600000 +3633,371,28,3141,38,2,2,500000 +3634,451,7,14136,24,1,2,600000 +3635,266,6,19364,13,2,5,500000 +3636,406,2,11716,15,1,1,400000 +3637,180,22,-3422,10,0,0,410000 +3638,137,60,6460,18,1,7,410000 +3639,276,51,10882,46,2,1,410000 +3640,227,10,1362,15,1,2,400000 +3641,488,6,8699,11,1,4,500000 +3642,420,50,12482,26,1,6,400000 +3643,298,26,15610,3,1,4,600000 +3644,195,55,6911,11,1,1,500000 +3645,113,40,3355,26,0,0,600000 +3646,111,42,9239,43,1,4,500000 +3647,1,52,8249,26,1,0,400000 +3648,222,38,9961,48,1,0,600000 +3649,237,16,9374,13,0,0,600000 +3650,236,21,2698,1,1,7,600000 +3651,166,60,17649,17,2,3,400000 +3652,449,22,9531,13,1,4,400000 +3653,34,36,244,13,1,1,400000 +3654,142,11,-590,44,1,6,410000 +3655,215,35,16373,7,1,7,410000 +3656,419,50,6552,18,1,7,500000 +3657,167,0,-1350,44,1,1,410000 +3658,392,11,15261,19,1,4,400000 +3659,81,8,17438,12,1,4,600000 +3660,24,14,-3008,45,0,3,410000 +3661,159,9,5322,35,2,7,500000 +3662,195,63,8748,20,1,5,410000 +3663,76,16,4585,34,1,4,500000 +3664,371,38,5163,26,0,7,400000 +3665,86,15,10280,11,1,0,400000 +3666,453,51,8970,21,1,4,600000 +3667,471,7,9199,1,1,4,410000 +3668,129,16,5161,21,2,3,400000 +3669,70,42,1170,37,1,3,410000 +3670,84,7,-4247,5,1,4,600000 +3671,411,25,1067,8,0,0,600000 +3672,366,53,-573,18,1,3,500000 +3673,38,37,-3015,17,0,1,600000 +3674,36,63,1998,26,1,5,400000 +3675,505,60,1470,1,1,7,400000 +3676,421,54,-968,10,1,2,600000 +3677,247,29,8903,14,1,1,400000 +3678,427,52,13292,46,1,7,600000 +3679,12,5,10425,25,1,3,400000 +3680,453,11,19537,30,0,4,600000 +3681,260,3,-4638,25,1,3,500000 +3682,346,40,-122,12,1,6,400000 +3683,273,35,16800,34,1,7,400000 +3684,263,31,15795,45,0,2,400000 +3685,236,15,13970,49,1,1,410000 +3686,231,36,-2309,39,1,6,400000 +3687,498,53,2772,39,1,4,410000 +3688,363,48,17460,3,1,4,400000 +3689,228,17,15888,3,1,1,500000 +3690,323,54,6892,20,0,3,410000 +3691,469,52,-3734,26,2,3,410000 +3692,280,18,14118,41,1,6,410000 +3693,126,27,3446,33,1,7,400000 +3694,159,18,-2832,4,1,4,400000 +3695,479,25,9445,13,1,3,500000 +3696,243,41,1888,31,1,3,400000 +3697,192,60,12139,28,1,4,400000 +3698,130,17,15546,5,1,7,410000 +3699,109,37,6994,30,1,5,600000 +3700,386,26,2134,13,1,7,400000 +3701,280,19,-1134,9,1,6,400000 +3702,443,15,10759,42,1,1,500000 +3703,66,22,17031,30,1,2,600000 +3704,300,51,8823,24,1,1,400000 +3705,123,61,5821,36,0,7,600000 +3706,10,35,-2308,47,1,3,400000 +3707,171,18,4791,7,1,3,500000 +3708,247,26,9287,17,1,0,410000 +3709,266,16,-1453,33,1,6,410000 +3710,428,27,9181,18,1,6,500000 +3711,49,30,5863,5,2,2,500000 +3712,190,6,15966,32,1,7,410000 +3713,67,12,-4984,41,1,3,500000 +3714,169,23,-3647,27,2,1,410000 +3715,469,46,12592,29,1,5,600000 +3716,456,61,4134,33,0,6,400000 +3717,321,34,-4772,21,0,7,400000 +3718,330,40,18485,45,1,0,600000 +3719,185,19,11692,40,0,7,410000 +3720,246,22,17251,47,0,5,600000 +3721,44,19,15147,8,1,2,600000 +3722,409,47,5904,12,1,0,400000 +3723,464,43,968,30,1,1,600000 +3724,33,15,-1654,28,1,4,500000 +3725,478,37,1528,15,2,2,500000 +3726,119,15,-2705,19,1,5,400000 +3727,116,26,5078,34,1,3,400000 +3728,184,31,-4451,34,1,4,600000 +3729,371,46,14069,37,1,3,600000 +3730,44,11,63,9,0,7,410000 +3731,215,46,16557,24,1,3,600000 +3732,450,35,17005,40,1,4,500000 +3733,442,39,-3816,26,1,6,500000 +3734,417,54,4119,41,1,5,400000 +3735,158,22,12825,44,1,3,500000 +3736,248,13,13089,16,1,7,500000 +3737,388,34,-3133,26,1,5,410000 +3738,87,21,3617,16,1,7,400000 +3739,407,5,2758,36,1,7,500000 +3740,426,2,13359,11,1,3,500000 +3741,75,23,-1323,17,1,7,500000 +3742,172,28,9984,26,1,0,600000 +3743,347,10,1831,9,0,3,500000 +3744,36,46,16396,23,1,1,600000 +3745,481,28,19219,31,0,6,410000 +3746,358,18,11945,24,1,7,600000 +3747,250,15,18977,17,1,7,410000 +3748,199,24,8829,6,1,1,500000 +3749,260,36,-2117,13,0,3,400000 +3750,29,5,2263,39,1,5,500000 +3751,305,21,-190,34,2,3,410000 +3752,220,10,3672,32,1,2,410000 +3753,304,20,956,20,0,3,400000 +3754,461,1,19449,14,0,5,500000 +3755,415,59,-1170,40,0,6,400000 +3756,236,51,-3358,18,0,1,500000 +3757,87,27,13784,18,1,5,600000 +3758,447,3,-2889,44,1,2,400000 +3759,191,42,6466,8,0,5,400000 +3760,130,7,3952,42,1,7,500000 +3761,494,23,-4465,17,1,3,600000 +3762,213,33,8473,22,1,0,400000 +3763,40,28,187,36,2,0,400000 +3764,100,19,-3417,35,1,2,400000 +3765,223,56,11693,41,2,0,500000 +3766,484,8,2664,2,2,1,600000 +3767,141,18,7208,25,0,4,400000 +3768,505,47,13180,30,1,7,500000 +3769,23,5,1228,23,0,5,410000 +3770,83,17,7824,36,1,1,410000 +3771,385,38,11014,12,2,5,600000 +3772,344,20,-1786,4,1,7,400000 +3773,214,19,9371,31,0,1,410000 +3774,1,47,-3938,40,1,4,600000 +3775,89,26,4609,42,1,1,400000 +3776,490,11,-4473,1,1,2,410000 +3777,251,44,18744,27,1,2,410000 +3778,389,47,18612,41,0,3,500000 +3779,209,30,11626,31,1,6,600000 +3780,327,0,14266,39,1,2,400000 +3781,369,24,-4131,28,0,3,500000 +3782,71,34,3493,46,1,0,400000 +3783,128,48,12877,38,1,0,500000 +3784,173,54,8559,34,1,5,500000 +3785,94,56,13041,9,1,1,410000 +3786,206,55,-2665,1,0,0,410000 +3787,176,37,10963,36,2,3,410000 +3788,179,26,3119,25,1,0,500000 +3789,441,33,6308,6,1,6,410000 +3790,427,45,10254,48,1,7,410000 +3791,457,40,10060,34,2,5,410000 +3792,167,5,8739,20,2,7,600000 +3793,475,35,-3091,24,1,2,600000 +3794,285,39,10607,8,1,7,600000 +3795,11,46,9760,13,1,1,600000 +3796,469,61,3137,40,0,6,400000 +3797,388,53,15242,30,1,6,400000 +3798,213,24,19560,28,1,7,410000 +3799,109,63,8048,46,0,0,600000 +3800,193,46,9064,47,1,0,410000 +3801,441,18,16122,8,0,0,500000 +3802,86,7,16150,44,1,5,410000 +3803,233,60,3651,31,1,2,600000 +3804,352,12,18003,49,1,3,600000 +3805,199,51,-2512,6,1,1,600000 +3806,280,48,14638,3,1,5,400000 +3807,77,1,19415,1,1,0,500000 +3808,508,11,1104,13,1,4,500000 +3809,425,56,2823,8,0,7,410000 +3810,310,13,10933,36,2,3,500000 +3811,384,14,6294,8,0,0,400000 +3812,465,62,15187,48,1,6,400000 +3813,130,46,1513,12,1,5,410000 +3814,161,26,5785,21,1,3,600000 +3815,72,8,11179,42,1,4,410000 +3816,308,30,19850,40,2,4,600000 +3817,159,51,13407,23,1,2,400000 +3818,218,34,17628,3,0,5,500000 +3819,16,21,13301,4,0,4,500000 +3820,229,60,12262,21,1,0,410000 +3821,494,62,17621,38,1,1,600000 +3822,192,52,19480,39,0,7,400000 +3823,499,22,-356,37,1,2,600000 +3824,485,24,774,14,1,7,600000 +3825,377,35,2523,4,0,5,410000 +3826,239,6,10471,21,1,5,410000 +3827,269,14,1363,6,0,3,400000 +3828,196,39,10544,26,2,2,410000 +3829,430,31,19037,17,1,0,500000 +3830,334,9,16717,49,1,5,400000 +3831,271,17,7760,34,1,7,400000 +3832,9,46,-4636,40,1,1,600000 +3833,252,53,14707,11,1,6,500000 +3834,124,43,5516,17,0,5,600000 +3835,226,1,5746,25,0,7,500000 +3836,251,47,3083,9,1,5,410000 +3837,254,24,-941,33,2,0,500000 +3838,428,48,-2741,19,1,3,410000 +3839,217,33,18774,45,2,5,410000 +3840,315,30,19692,19,1,4,600000 +3841,128,4,18726,27,1,7,410000 +3842,15,3,11844,42,1,5,410000 +3843,435,58,6701,19,1,1,400000 +3844,369,33,8106,14,1,4,500000 +3845,497,51,5794,26,0,7,600000 +3846,154,41,14093,7,1,4,400000 +3847,95,6,10695,9,1,5,400000 +3848,265,33,-2928,27,1,6,410000 +3849,419,58,16926,7,2,5,500000 +3850,122,54,9664,14,0,2,400000 +3851,10,46,13150,30,1,3,600000 +3852,29,54,18001,32,1,1,410000 +3853,438,45,15282,37,0,3,410000 +3854,387,3,1981,3,0,2,410000 +3855,11,10,13745,11,2,7,500000 +3856,178,1,2314,39,0,1,500000 +3857,202,43,11912,13,1,3,410000 +3858,473,47,4143,37,1,2,500000 +3859,302,49,-3348,25,1,0,410000 +3860,123,55,10786,41,1,1,410000 +3861,355,53,18555,39,0,0,410000 +3862,7,3,11081,17,2,0,600000 +3863,92,18,3436,37,1,4,400000 +3864,72,17,120,26,1,0,600000 +3865,417,9,12883,39,1,1,500000 +3866,471,25,-1166,33,1,3,400000 +3867,276,13,9603,18,1,6,500000 +3868,307,49,15032,27,2,6,410000 +3869,91,38,18806,17,1,2,400000 +3870,344,58,981,20,0,5,400000 +3871,31,25,17951,30,2,5,410000 +3872,431,23,-3257,30,0,1,600000 +3873,283,53,9847,22,0,0,410000 +3874,211,62,7666,33,1,1,500000 +3875,220,9,3221,17,1,3,410000 +3876,494,12,10649,16,1,3,500000 +3877,70,40,15568,17,1,7,500000 +3878,243,42,-2765,21,1,7,500000 +3879,116,18,-1268,32,0,5,400000 +3880,398,3,14745,24,0,7,500000 +3881,107,42,3214,45,1,0,400000 +3882,504,31,5929,3,1,4,500000 +3883,452,45,2623,20,1,1,500000 +3884,471,41,1140,18,2,0,400000 +3885,144,10,18190,36,1,7,400000 +3886,236,36,11575,1,1,4,410000 +3887,112,50,12281,43,1,1,400000 +3888,467,11,16737,11,1,4,600000 +3889,202,15,3844,8,1,6,400000 +3890,50,35,-1041,35,0,3,400000 +3891,409,3,13798,8,0,4,500000 +3892,327,63,19507,28,1,0,600000 +3893,6,21,2975,39,1,7,500000 +3894,275,24,-1490,47,1,7,600000 +3895,475,54,7369,15,1,0,600000 +3896,43,43,-2209,6,1,6,600000 +3897,398,49,7896,44,0,5,400000 +3898,311,6,4085,49,1,4,600000 +3899,119,27,9778,34,0,3,410000 +3900,453,6,-1331,19,2,0,400000 +3901,396,63,11755,46,1,3,410000 +3902,129,29,4148,30,1,6,600000 +3903,341,42,2402,32,1,1,600000 +3904,96,47,2852,31,2,2,400000 +3905,25,62,12555,40,1,6,600000 +3906,259,50,18997,19,0,0,400000 +3907,457,18,6109,30,1,5,400000 +3908,275,34,-2893,40,0,2,600000 +3909,461,16,5403,18,1,6,600000 +3910,473,17,15557,19,2,1,600000 +3911,238,42,12646,29,0,0,410000 +3912,416,20,7174,32,0,1,410000 +3913,24,13,7678,41,1,1,410000 +3914,228,29,13783,5,1,1,600000 +3915,238,35,9507,24,1,1,410000 +3916,318,16,12813,23,1,7,600000 +3917,241,43,-2064,39,1,2,600000 +3918,4,13,7387,11,1,1,400000 +3919,428,44,12241,44,0,3,410000 +3920,318,25,9354,46,1,3,600000 +3921,73,55,16409,30,0,0,500000 +3922,363,50,1474,29,1,2,600000 +3923,136,43,-3683,22,1,2,410000 +3924,154,23,5498,8,2,0,600000 +3925,329,22,14923,13,1,2,500000 +3926,89,22,-3182,7,1,4,410000 +3927,358,17,12928,21,1,2,600000 +3928,444,1,-3501,41,1,3,600000 +3929,506,4,9214,9,1,6,600000 +3930,282,63,4568,35,1,7,600000 +3931,167,41,10202,16,1,2,400000 +3932,403,29,6914,33,1,2,600000 +3933,496,9,4023,35,1,2,600000 +3934,503,52,2798,32,0,2,600000 +3935,5,15,2361,4,1,1,600000 +3936,164,35,194,17,2,0,410000 +3937,153,5,15078,16,1,2,600000 +3938,280,55,11286,21,1,7,410000 +3939,296,0,5743,41,1,5,600000 +3940,438,47,4175,17,1,4,500000 +3941,352,59,4652,5,1,7,410000 +3942,23,59,11699,13,1,3,400000 +3943,271,6,-848,35,0,0,400000 +3944,99,43,7091,22,0,2,400000 +3945,245,28,-1561,24,1,2,400000 +3946,193,7,-3984,36,2,6,410000 +3947,411,43,7711,7,1,0,400000 +3948,261,49,8728,39,0,4,400000 +3949,139,63,12235,12,1,2,500000 +3950,15,62,-1785,44,1,7,410000 +3951,60,41,1613,24,1,3,410000 +3952,365,37,13043,45,1,3,600000 +3953,353,22,-3970,15,1,0,410000 +3954,173,51,-342,11,1,7,500000 +3955,456,42,728,38,1,0,600000 +3956,349,33,10601,26,0,4,400000 +3957,231,17,18125,39,0,6,500000 +3958,356,54,-1182,29,0,0,410000 +3959,351,8,4660,31,1,3,410000 +3960,30,8,2997,49,0,1,600000 +3961,499,59,17719,49,2,0,600000 +3962,2,32,10566,2,1,1,410000 +3963,79,28,-911,29,0,7,400000 +3964,219,32,11639,6,2,2,600000 +3965,340,35,2839,5,1,6,500000 +3966,420,27,-1017,40,1,3,600000 +3967,398,44,-1102,31,0,1,410000 +3968,377,62,-1840,34,1,4,600000 +3969,415,57,-877,30,1,3,400000 +3970,67,24,-3411,39,0,3,500000 +3971,488,42,-1195,11,1,1,410000 +3972,172,38,6243,18,2,3,500000 +3973,475,30,-3506,42,1,2,600000 +3974,80,13,3657,27,0,4,600000 +3975,65,54,2758,26,1,6,400000 +3976,52,55,4389,28,2,0,400000 +3977,164,41,2719,20,1,6,400000 +3978,257,49,19626,44,2,4,500000 +3979,13,1,-3851,4,0,5,400000 +3980,84,37,19967,6,1,5,500000 +3981,345,52,5046,47,1,7,400000 +3982,60,35,-2241,48,1,5,600000 +3983,319,47,10081,31,1,7,600000 +3984,241,3,10761,31,1,1,600000 +3985,10,22,-4402,20,1,1,600000 +3986,99,51,10296,1,1,1,500000 +3987,313,20,914,35,1,7,500000 +3988,27,8,-155,45,1,7,400000 +3989,488,18,8204,22,1,2,600000 +3990,29,15,5176,44,2,3,500000 +3991,218,18,5493,3,1,0,600000 +3992,167,36,9495,13,0,5,600000 +3993,438,15,8547,38,0,0,600000 +3994,414,44,18356,1,2,6,500000 +3995,281,42,-1704,33,0,1,500000 +3996,216,38,16691,41,1,1,600000 +3997,182,63,5996,1,1,5,400000 +3998,396,12,7415,49,1,5,500000 +3999,465,29,13432,2,1,4,500000 +4000,310,5,-2076,42,1,5,400000 +4001,203,21,-1496,38,1,2,600000 +4002,79,12,-321,17,1,7,500000 +4003,106,8,12757,14,1,6,410000 +4004,354,56,16868,38,1,0,500000 +4005,217,50,16414,47,1,5,410000 +4006,260,32,5031,30,1,0,410000 +4007,456,0,-1180,31,1,1,600000 +4008,192,33,-3614,8,1,2,410000 +4009,313,17,-898,48,1,6,400000 +4010,277,42,16156,38,1,6,500000 +4011,279,56,175,24,0,3,400000 +4012,330,11,-2501,33,1,5,500000 +4013,113,51,9404,5,0,6,410000 +4014,94,43,6184,26,1,0,500000 +4015,413,56,-1353,26,1,1,410000 +4016,255,6,8228,19,1,7,600000 +4017,214,57,7567,24,1,2,410000 +4018,371,36,8356,36,1,1,600000 +4019,66,23,-1517,32,1,6,600000 +4020,170,0,14505,14,1,7,400000 +4021,251,62,11617,13,1,7,500000 +4022,467,61,2657,43,1,3,400000 +4023,82,20,14592,37,1,0,400000 +4024,382,45,5404,47,1,3,410000 +4025,269,46,16294,25,1,4,400000 +4026,203,25,2866,48,1,7,600000 +4027,370,40,16833,12,1,1,600000 +4028,383,4,10157,22,1,0,410000 +4029,189,5,1664,6,1,2,600000 +4030,452,52,18448,49,1,5,500000 +4031,32,37,9292,2,0,2,400000 +4032,312,46,15055,46,2,7,400000 +4033,128,45,7200,32,1,4,400000 +4034,418,9,-2629,45,1,2,500000 +4035,294,13,18825,49,0,4,600000 +4036,492,45,10065,9,1,0,600000 +4037,348,26,3199,21,0,3,500000 +4038,504,34,11240,32,1,2,400000 +4039,227,10,14829,17,1,1,500000 +4040,131,29,16413,42,1,5,600000 +4041,473,24,14962,20,0,6,500000 +4042,386,10,14324,18,0,1,410000 +4043,51,42,1450,32,1,1,410000 +4044,406,2,18642,2,1,0,600000 +4045,498,42,-3769,16,0,6,400000 +4046,340,62,5840,6,2,2,600000 +4047,435,59,-29,12,0,5,600000 +4048,241,19,8818,18,1,4,500000 +4049,180,42,6696,39,1,4,400000 +4050,147,34,10676,10,0,1,400000 +4051,218,6,7450,43,2,6,600000 +4052,445,5,6602,36,1,2,400000 +4053,146,58,11391,43,1,6,410000 +4054,323,26,11091,25,1,4,410000 +4055,285,49,2635,34,1,0,410000 +4056,150,3,2653,19,1,6,600000 +4057,71,20,3565,42,1,4,600000 +4058,286,59,8706,28,1,5,410000 +4059,290,58,-801,47,1,5,400000 +4060,202,19,9991,41,0,2,400000 +4061,350,26,2966,1,1,4,400000 +4062,395,29,6688,40,1,7,400000 +4063,134,19,4130,9,1,2,500000 +4064,132,49,-2463,19,0,1,500000 +4065,94,21,17377,39,1,0,410000 +4066,139,18,3493,31,1,1,500000 +4067,270,2,19951,47,1,0,600000 +4068,340,11,13114,27,1,2,400000 +4069,157,61,-1990,17,1,2,410000 +4070,18,16,1446,13,0,0,410000 +4071,35,33,-773,24,1,4,500000 +4072,433,60,1910,10,1,7,600000 +4073,330,53,9558,30,1,0,400000 +4074,367,9,9838,19,0,0,400000 +4075,414,63,2612,48,1,5,600000 +4076,44,42,14241,28,1,1,500000 +4077,249,5,3789,26,1,4,400000 +4078,35,26,19382,30,1,2,500000 +4079,429,16,18046,35,2,1,600000 +4080,171,42,16320,12,1,1,400000 +4081,278,46,19439,26,1,5,500000 +4082,390,48,19320,10,0,3,400000 +4083,104,6,15591,6,1,1,410000 +4084,104,10,13471,8,1,3,400000 +4085,2,36,-953,43,1,4,500000 +4086,290,38,11557,1,0,6,400000 +4087,227,47,2187,25,1,5,410000 +4088,503,15,1761,46,1,7,410000 +4089,429,53,8954,41,0,0,410000 +4090,82,29,7150,8,1,7,400000 +4091,295,1,-1044,21,1,2,400000 +4092,39,21,-750,30,2,2,500000 +4093,482,62,14081,6,1,7,400000 +4094,259,57,17408,30,1,2,410000 +4095,59,3,-262,7,1,0,600000 diff --git a/crates/lance-graph-quack/tests/duckdb/data/partner.csv b/crates/lance-graph-quack/tests/duckdb/data/partner.csv new file mode 100644 index 000000000..b50551ec4 --- /dev/null +++ b/crates/lance-graph-quack/tests/duckdb/data/partner.csv @@ -0,0 +1,65 @@ +rid,country,pgroup +0,7,0 +1,7,3 +2,1,3 +3,0,1 +4,6,1 +5,0,1 +6,4,2 +7,7,0 +8,3,1 +9,6,3 +10,4,2 +11,7,2 +12,0,1 +13,2,3 +14,0,1 +15,0,2 +16,2,3 +17,7,0 +18,3,3 +19,7,2 +20,6,3 +21,6,2 +22,4,3 +23,0,1 +24,0,2 +25,0,3 +26,2,2 +27,2,1 +28,2,0 +29,2,2 +30,2,0 +31,2,1 +32,2,3 +33,1,1 +34,1,0 +35,2,3 +36,0,3 +37,0,0 +38,5,2 +39,2,2 +40,6,3 +41,4,1 +42,2,2 +43,6,1 +44,5,1 +45,5,3 +46,2,1 +47,7,1 +48,2,3 +49,1,3 +50,2,0 +51,2,2 +52,4,3 +53,0,0 +54,4,0 +55,0,1 +56,5,2 +57,6,2 +58,6,2 +59,0,1 +60,3,0 +61,3,3 +62,5,0 +63,1,0 diff --git a/crates/lance-graph-quack/tests/duckdb/fixture.rs b/crates/lance-graph-quack/tests/duckdb/fixture.rs new file mode 100644 index 000000000..7a7251877 --- /dev/null +++ b/crates/lance-graph-quack/tests/duckdb/fixture.rs @@ -0,0 +1,288 @@ +//! The deterministic ERP-shaped fixture the DuckDB differential suite runs +//! against — three tables, generated by a seeded splitmix64 stream so the +//! same bytes come out of every run, in every environment, forever. +//! +//! Foreign keys are ROW INDICES, not a minted address: `line.doc_id` names a +//! `doc` row by its position, `line.partner_id` names a `partner` row by its +//! position, and both tables carry an explicit `rid` column in their CSV so +//! a consumer (DuckDB or this crate) never has to trust an engine's own +//! internal row-numbering to agree with the fixture's. +//! +//! This module is included by `#[path]` from `duckdb_differential.rs`, not +//! compiled as its own test binary — `cargo test` only auto-discovers files +//! directly under `tests/`, so a file one level down (here) is inert until +//! named. + +use std::io::{self, Write}; +use std::path::Path; + +use lance_graph_mask_risc::{LaneRef, Planes}; + +/// Row counts — the shapes named in the spec. +pub const PARTNER_ROWS: usize = 64; +pub const DOC_ROWS: usize = 512; +pub const LINE_ROWS: usize = 4096; + +/// splitmix64: the whole generator. One state word, one step. +fn splitmix64(state: &mut u64) -> u64 { + *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = *state; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) +} + +/// Uniform `0..n`. Modulo bias is not a concern here — the fixture only +/// needs to be FIXED, not statistically pure. +fn next_u32(state: &mut u64, n: u32) -> u32 { + (splitmix64(state) % u64::from(n)) as u32 +} + +/// Uniform `[lo, hi)` as `i32`. +fn next_i32_range(state: &mut u64, lo: i32, hi: i32) -> i32 { + lo + next_u32(state, (hi - lo) as u32) as i32 +} + +/// `status` weighted 20 % open / 70 % posted / 10 % cancelled. +fn next_status(state: &mut u64) -> u32 { + let r = next_u32(state, 100); + if r < 20 { + 0 + } else if r < 90 { + 1 + } else { + 2 + } +} + +const GL_ACCOUNTS: [u32; 4] = [400_000, 410_000, 500_000, 600_000]; + +fn next_gl_account(state: &mut u64) -> u32 { + GL_ACCOUNTS[next_u32(state, GL_ACCOUNTS.len() as u32) as usize] +} + +/// The `partner` table — 64 rows, `country: 0..8`, `pgroup: 0..4`. +pub struct PartnerTable { + pub country: Vec, + pub pgroup: Vec, +} + +/// The `doc` table — 512 rows, `company: 0..3`, `period: 1..13`, +/// `doc_type: 0..4`. +pub struct DocTable { + pub company: Vec, + pub period: Vec, + pub doc_type: Vec, +} + +/// The `line` table — 4096 rows. +/// +/// `doc_id` is stored as `i32`, not `u32`, and that is a deliberate +/// representational choice made HERE, not a fact about the schema: the +/// fixture's `doc_id` domain is `0..DOC_ROWS`, always non-negative and far +/// inside `i32`'s range, so nothing is lost by the cast. It is needed because +/// `lance_graph_mask_risc::Pred` has ordered comparisons (`GtI32`/`LtI32`/…) +/// only over a signed lane — there is no `GtU32`/`LtU32` in the IR today — and +/// `range_docid` is exactly the query that needs one. Every other `line` +/// column that participates in a lowered query is compared by EQUALITY or by +/// [`lance_graph_quack::Cmp::MatchU32`]'s ternary match, neither of which +/// needs an order, so they stay `u32`, matching the schema's own typing. +pub struct LineTable { + pub doc_id: Vec, + pub partner_id: Vec, + pub amount: Vec, + pub qty: Vec, + pub status: Vec, + pub cost_center: Vec, + pub gl_account: Vec, +} + +pub struct Fixture { + pub partner: PartnerTable, + pub doc: DocTable, + pub line: LineTable, +} + +/// The seed, spelled as a valid hex literal (hex digits only: no `O`, no `L` +/// — `FOLD` becomes `F01D`, the ordinary leetspeak substitution). +const SEED: u64 = 0x51AC_F01D_2026; + +/// Generate the fixture. Deterministic: the same seed, the same row-major +/// draw order (every field of row `i` before any field of row `i + 1`, table +/// by table: `partner`, then `doc`, then `line`), every time. +pub fn generate() -> Fixture { + let mut state = SEED; + + let mut country = Vec::with_capacity(PARTNER_ROWS); + let mut pgroup = Vec::with_capacity(PARTNER_ROWS); + for _ in 0..PARTNER_ROWS { + country.push(next_u32(&mut state, 8)); + pgroup.push(next_u32(&mut state, 4)); + } + let partner = PartnerTable { country, pgroup }; + + let mut company = Vec::with_capacity(DOC_ROWS); + let mut period = Vec::with_capacity(DOC_ROWS); + let mut doc_type = Vec::with_capacity(DOC_ROWS); + for _ in 0..DOC_ROWS { + company.push(next_u32(&mut state, 3)); + period.push(next_u32(&mut state, 12) + 1); + doc_type.push(next_u32(&mut state, 4)); + } + let doc = DocTable { + company, + period, + doc_type, + }; + + let mut doc_id = Vec::with_capacity(LINE_ROWS); + let mut partner_id = Vec::with_capacity(LINE_ROWS); + let mut amount = Vec::with_capacity(LINE_ROWS); + let mut qty = Vec::with_capacity(LINE_ROWS); + let mut status = Vec::with_capacity(LINE_ROWS); + let mut cost_center = Vec::with_capacity(LINE_ROWS); + let mut gl_account = Vec::with_capacity(LINE_ROWS); + for _ in 0..LINE_ROWS { + doc_id.push(next_i32_range(&mut state, 0, DOC_ROWS as i32)); + partner_id.push(next_u32(&mut state, PARTNER_ROWS as u32)); + amount.push(next_i32_range(&mut state, -5000, 20_000)); + qty.push(next_i32_range(&mut state, 1, 50)); + status.push(next_status(&mut state)); + cost_center.push(next_u32(&mut state, 8)); + gl_account.push(next_gl_account(&mut state)); + } + let line = LineTable { + doc_id, + partner_id, + amount, + qty, + status, + cost_center, + gl_account, + }; + + Fixture { partner, doc, line } +} + +impl Fixture { + /// `partner.csv`'s bytes, in memory — the single source both the + /// on-disk writer and the drift guard read from, so there is exactly one + /// place the format can drift. + pub fn partner_csv(&self) -> Vec { + let mut buf = Vec::new(); + writeln!(buf, "rid,country,pgroup").expect("write to Vec never fails"); + for i in 0..PARTNER_ROWS { + writeln!( + buf, + "{i},{},{}", + self.partner.country[i], self.partner.pgroup[i] + ) + .expect("write to Vec never fails"); + } + buf + } + + /// `doc.csv`'s bytes. See [`Fixture::partner_csv`]. + pub fn doc_csv(&self) -> Vec { + let mut buf = Vec::new(); + writeln!(buf, "rid,company,period,doc_type").expect("write to Vec never fails"); + for i in 0..DOC_ROWS { + writeln!( + buf, + "{i},{},{},{}", + self.doc.company[i], self.doc.period[i], self.doc.doc_type[i] + ) + .expect("write to Vec never fails"); + } + buf + } + + /// `line.csv`'s bytes. See [`Fixture::partner_csv`]. + pub fn line_csv(&self) -> Vec { + let mut buf = Vec::new(); + writeln!( + buf, + "rid,doc_id,partner_id,amount,qty,status,cost_center,gl_account" + ) + .expect("write to Vec never fails"); + for i in 0..LINE_ROWS { + writeln!( + buf, + "{i},{},{},{},{},{},{},{}", + self.line.doc_id[i], + self.line.partner_id[i], + self.line.amount[i], + self.line.qty[i], + self.line.status[i], + self.line.cost_center[i], + self.line.gl_account[i], + ) + .expect("write to Vec never fails"); + } + buf + } + + /// Write `partner.csv` / `doc.csv` / `line.csv` into `dir` — the + /// generator's on-disk form, built once and committed under + /// `tests/duckdb/data/`. Byte-for-byte deterministic: the drift guard + /// (`committed_fixture_matches_generator`) depends on that. + pub fn write_csv(&self, dir: &Path) -> io::Result<()> { + std::fs::write(dir.join("partner.csv"), self.partner_csv())?; + std::fs::write(dir.join("doc.csv"), self.doc_csv())?; + std::fs::write(dir.join("line.csv"), self.line_csv())?; + Ok(()) + } +} + +/// The `line` table's lanes, in the fixed order the `Col` constants below +/// index into. Only `line` is exposed as [`Planes`] — `partner` and `doc` +/// are never the target of a lowered [`lance_graph_quack::Query`] in this +/// suite (Quack has no join lowering; see the `join_*_is_the_open_seam` +/// tests), so they exist only as CSV rows for the oracle. +pub mod col { + use lance_graph_quack::Col; + + pub const AMOUNT: Col = Col(0); + pub const QTY: Col = Col(1); + pub const DOC_ID: Col = Col(2); + pub const STATUS: Col = Col(3); + pub const COST_CENTER: Col = Col(4); + pub const GL_ACCOUNT: Col = Col(5); +} + +/// The `line` table's lanes, borrowed and held so [`Planes`] can borrow them +/// back in turn. Two steps because `Planes<'a>` needs `&'a [LaneRef<'a>]` — +/// an array built inline in a function cannot outlive that function's +/// return, so this struct is the thing that DOES outlive it, owned by the +/// caller for exactly as long as the [`Planes`] it lends out. +pub struct LineLanes<'a> { + lanes: [LaneRef<'a>; 6], +} + +impl LineTable { + /// Borrow every lane in the fixed order `col` above indexes into. + pub fn lanes(&self) -> LineLanes<'_> { + LineLanes { + lanes: [ + LaneRef::I32(&self.amount), + LaneRef::I32(&self.qty), + LaneRef::I32(&self.doc_id), + LaneRef::U32(&self.status), + LaneRef::U32(&self.cost_center), + LaneRef::U32(&self.gl_account), + ], + } + } +} + +impl<'a> LineLanes<'a> { + /// Borrow `LINE_ROWS` rows over these lanes, no resident mask planes — + /// every case in this suite is a plain `WHERE`, never a plane leaf. + pub fn planes(&self) -> Planes<'_> { + Planes { + n_rows: LINE_ROWS, + masks: &[], + lanes: &self.lanes, + } + } +} diff --git a/crates/lance-graph-quack/tests/duckdb/oracle.py b/crates/lance-graph-quack/tests/duckdb/oracle.py new file mode 100644 index 000000000..350ebc2d7 --- /dev/null +++ b/crates/lance-graph-quack/tests/duckdb/oracle.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""DuckDB oracle for the lance-graph-quack differential suite. + +Loads the three committed fixture CSVs into DuckDB, runs every case's SQL +from `cases.tsv`, and REWRITES the `expected` column in place. DuckDB is the +semantic oracle here — nothing in this file computes an answer by hand. + +Usage: python oracle.py (run from anywhere; paths are script-relative) + +Encoding (must match the Rust side's `run_query`/`run_group` in +`../duckdb_differential.rs` exactly): + - a bare scalar (COUNT/SUM/MIN/MAX) -> its decimal string + - EXISTS(...) -> "0" or "1" + - a grouped result (key, value) -> "k1:v1;k2:v2;..." sorted by key + - a projection of `rid` -> sorted comma-separated row indices +""" + +import csv +import pathlib +import sys + +try: + import duckdb +except ImportError: + sys.exit("duckdb is not importable — run with the venv's python, e.g. " + "/ddv/bin/python tests/duckdb/oracle.py") + +HERE = pathlib.Path(__file__).resolve().parent +DATA = HERE / "data" +CASES = HERE / "cases.tsv" + +# Case ids whose query returns one row of (key, value) pairs to encode as +# "k:v;k:v;...". Every other id is either a bare scalar or `rows_proj`. +GROUPED = {"group_count_cc", "group_sum_cc", "join_group_sum_country"} +BOOLEAN = {"exists_neg"} +ROWS = {"rows_proj"} + + +def encode(case_id: str, rows: list[tuple]) -> str: + if case_id in ROWS: + # `rows_proj`'s query is `SELECT rid ... ORDER BY rid` — already + # sorted; still sort defensively so the encoding's own contract + # ("sorted") does not silently depend on DuckDB's ORDER BY. + return ",".join(str(r[0]) for r in sorted(rows, key=lambda r: r[0])) + if case_id in GROUPED: + pairs = sorted(rows, key=lambda r: r[0]) + return ";".join(f"{k}:{v}" for k, v in pairs) + if case_id in BOOLEAN: + (val,) = rows[0] + return "1" if val else "0" + # A bare scalar: exactly one row, one column. + (val,) = rows[0] + if val is None: + # NULL only happens if a filter admits zero rows (e.g. an empty + # MIN/MAX group) — surfaced as the literal string so a fixture bug + # that makes this happen is visible in cases.tsv rather than + # silently coerced to 0. + return "NULL" + return str(val) + + +def main() -> None: + con = duckdb.connect(database=":memory:") + con.execute( + f"CREATE TABLE partner AS SELECT * FROM read_csv_auto('{DATA / 'partner.csv'}')" + ) + con.execute( + f"CREATE TABLE doc AS SELECT * FROM read_csv_auto('{DATA / 'doc.csv'}')" + ) + con.execute( + f"CREATE TABLE line AS SELECT * FROM read_csv_auto('{DATA / 'line.csv'}')" + ) + + with open(CASES, newline="") as f: + reader = csv.reader(f, delimiter="\t") + header = next(reader) + assert header == ["id", "sql", "expected"], f"unexpected header: {header}" + out_rows = [header] + for row in reader: + if not row: + continue + case_id, sql = row[0], row[1] + rows = con.execute(sql).fetchall() + expected = encode(case_id, rows) + out_rows.append([case_id, sql, expected]) + print(f"{case_id}: {expected}") + + with open(CASES, "w", newline="") as f: + writer = csv.writer(f, delimiter="\t", lineterminator="\n") + writer.writerows(out_rows) + + +if __name__ == "__main__": + main() diff --git a/crates/lance-graph-quack/tests/duckdb_differential.rs b/crates/lance-graph-quack/tests/duckdb_differential.rs new file mode 100644 index 000000000..6c3c0c4ba --- /dev/null +++ b/crates/lance-graph-quack/tests/duckdb_differential.rs @@ -0,0 +1,613 @@ +//! The DuckDB ↔ Quack differential harness. +//! +//! DuckDB is the semantic ORACLE — `tests/duckdb/oracle.py` runs the SQL in +//! `tests/duckdb/cases.tsv` against the committed fixture and writes the +//! `expected` column. Every test below is the same query lowered through +//! [`lance_graph_quack`] and executed by `lance_graph_mask_risc::execute`, +//! encoded the same way, and compared. **Expected values are never +//! hand-edited** — see `tests/duckdb/README.txt`. +//! +//! `join_*_is_the_open_seam` tests are the honest exception: Quack has no +//! join lowering (`src/lib.rs`'s own status section names it — `src_mask → +//! hop → dst_mask` is `lance-graph-mask-risc`'s PR5 and there is no `hop` op +//! to lower to yet). Those three still load their oracle-computed `expected` +//! value — proving the oracle side is real and committed — then `todo!()`. +//! They are `#[should_panic]` by design, not disabled and not deleted. + +#[path = "duckdb/fixture.rs"] +mod fixture; + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::path::Path; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use lance_graph_mask_risc::{ + execute, materialize_rows, scratch_words_for, words_for, Operand, Planes, Scratch, Value, +}; +use lance_graph_quack::{lower, lower_group_by, Agg, Cmp, Col, Filter, GroupBy, Query}; + +use fixture::col::{AMOUNT, COST_CENTER, DOC_ID, GL_ACCOUNT, QTY, STATUS}; + +// --------------------------------------------------------------------- +// The counting allocator — `no_alloc.rs`'s pattern (lance-graph-mask-risc), +// reused rather than reinvented. It counts every byte `alloc` hands out, +// for the WHOLE process; each case reads the counter before and after its +// own `execute` call(s) and reports the delta, never the running total. +// --------------------------------------------------------------------- + +struct Counting; + +static BYTES: AtomicUsize = AtomicUsize::new(0); + +// SAFETY: a pure pass-through to `System`; the counter is the only addition. +unsafe impl GlobalAlloc for Counting { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + BYTES.fetch_add(layout.size(), Ordering::Relaxed); + // SAFETY: same layout, same contract as the caller's. + unsafe { System.alloc(layout) } + } + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + // SAFETY: `ptr` came from `alloc` above with this `layout`. + unsafe { System.dealloc(ptr, layout) } + } +} + +#[global_allocator] +static A: Counting = Counting; + +fn alloc_delta(before: usize) -> usize { + BYTES.load(Ordering::Relaxed) - before +} + +// --------------------------------------------------------------------- +// cases.tsv — hand-rolled TSV, no serde in this crate's dependency graph. +// --------------------------------------------------------------------- + +struct Case { + id: String, + sql: String, + expected: String, +} + +fn load_cases() -> Vec { + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/duckdb/cases.tsv"); + let content = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("cases.tsv unreadable at {}: {e}", path.display())); + let mut lines = content.lines(); + let header = lines.next().expect("cases.tsv has a header row"); + assert_eq!( + header, "id\tsql\texpected", + "cases.tsv header drifted from the id/sql/expected contract" + ); + lines + .filter(|l| !l.is_empty()) + .map(|line| { + let mut parts = line.splitn(3, '\t'); + let id = parts + .next() + .unwrap_or_else(|| panic!("missing id column in row: {line:?}")) + .to_string(); + let sql = parts + .next() + .unwrap_or_else(|| panic!("missing sql column in row: {line:?}")) + .to_string(); + let expected = parts.next().unwrap_or("").to_string(); + Case { id, sql, expected } + }) + .collect() +} + +/// Assert `actual` matches the oracle's `expected` for `id`, with enough +/// context to diagnose a divergence without re-running anything. +fn assert_case(cases: &[Case], id: &str, actual: &str) { + let case = cases + .iter() + .find(|c| c.id == id) + .unwrap_or_else(|| panic!("case {id} is not in cases.tsv")); + assert_eq!( + actual, case.expected, + "case {id} diverged from the DuckDB oracle\n sql: {}\n rust: {actual}\n duckdb: {}", + case.sql, case.expected + ); +} + +/// Load `id`'s expected value without asserting anything — the `join_*` +/// cases use this to prove the oracle side is real before `todo!()`-ing the +/// Rust side. +fn expected_only<'a>(cases: &'a [Case], id: &str) -> &'a str { + cases + .iter() + .find(|c| c.id == id) + .unwrap_or_else(|| panic!("case {id} is not in cases.tsv")) + .expected + .as_str() +} + +// --------------------------------------------------------------------- +// Execution + encoding — one place each, so the differential is actually +// comparing the same shape of answer on both sides. +// --------------------------------------------------------------------- + +struct CaseMetrics { + ops: usize, + scratch_words: usize, + alloc_bytes_exec: usize, + rows_materialized: usize, + index_vec_len: usize, + out_bytes: usize, + /// `Some(K)` for a grouped case — `ops`/`scratch_words`/`alloc_bytes_exec` + /// are then the SUM over the phase-1 filter and all K phase-2 programs, + /// per the spec's "record ops/allocs as the SUM over the K runs". + programs: Option, +} + +fn print_metric(id: &str, m: &CaseMetrics) { + match m.programs { + Some(k) => eprintln!( + "METRIC case={id} ops={} scratch_words={} alloc_bytes_exec={} \ + rows_materialized={} index_vec_len={} out_bytes={} programs={k}", + m.ops, + m.scratch_words, + m.alloc_bytes_exec, + m.rows_materialized, + m.index_vec_len, + m.out_bytes + ), + None => eprintln!( + "METRIC case={id} ops={} scratch_words={} alloc_bytes_exec={} \ + rows_materialized={} index_vec_len={} out_bytes={}", + m.ops, + m.scratch_words, + m.alloc_bytes_exec, + m.rows_materialized, + m.index_vec_len, + m.out_bytes + ), + } +} + +/// Copy a kept mask's bits out of wherever `Value::Mask` says they landed — +/// a scratch slot, or (for a bare-plane filter) the plane itself. +fn mask_bits(planes: &Planes<'_>, scratch: &Scratch<'_>, op: Operand) -> Vec { + match op { + Operand::Scratch(i) => scratch.slot(i).expect("Keep names a written slot").to_vec(), + Operand::Plane(p) => planes.masks[usize::from(p)].to_vec(), + } +} + +/// A plain scalar/projection query: lower, size scratch exactly, execute, +/// encode. The encoding matches `oracle.py`'s exactly — see +/// `tests/duckdb/README.txt`. +fn run_query(id: &str, planes: &Planes<'_>, filter: Filter, agg: Agg) -> (String, CaseMetrics) { + let program = lower(&Query { filter, agg }).expect("lowers"); + let words = words_for(planes.n_rows); + let slots = program.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let scratch_words = buf.len(); + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + + let before = BYTES.load(Ordering::Relaxed); + let value = execute(&program, planes, &mut scratch, None).expect("runs"); + let alloc_bytes_exec = alloc_delta(before); + + let (encoded, rows_materialized, index_vec_len) = match value { + Value::Count(c) => (c.to_string(), 0, 0), + Value::Bool(b) => (u8::from(b).to_string(), 0, 0), + Value::SumI64(s) => (s.to_string(), 0, 0), + Value::OptI32(Some(x)) => (x.to_string(), 0, 0), + Value::OptI32(None) => { + panic!("case {id}: min/max over an empty set — fixture is not what the case assumes") + } + Value::Mask(op) => { + let bits = mask_bits(planes, &scratch, op); + let rows = materialize_rows(&bits, planes.n_rows); + let n = rows.len(); + let s = rows + .iter() + .map(usize::to_string) + .collect::>() + .join(","); + (s, n, n) + } + Value::Blended => panic!("case {id}: no BlendI32 case in this suite"), + }; + let out_bytes = encoded.len(); + ( + encoded, + CaseMetrics { + ops: program.ops.len(), + scratch_words, + alloc_bytes_exec, + rows_materialized, + index_vec_len, + out_bytes, + programs: None, + }, + ) +} + +/// A `GROUP BY`: run the two-phase [`lance_graph_quack::GroupPlan`] by hand +/// (there is no execution helper in `lance_graph_quack` — this crate only +/// LOWERS, per its own crate doc) — phase 1's kept mask becomes plane 0 of a +/// widened [`Planes`], then each of the K group programs runs against it. +fn run_group( + id: &str, + planes: &Planes<'_>, + filter: Filter, + key: Col, + groups: u32, + agg: Agg, +) -> (String, CaseMetrics) { + let g = GroupBy { + filter, + key, + groups, + agg, + }; + // `planes.masks` is empty in this suite, so plane 0 is free — the kept + // mask lands there and nothing else needs to shift. + let plan = lower_group_by(&g, 0).expect("lowers"); + assert_eq!( + plan.filter_plane, 0, + "case {id}: filter_plane drifted from where we place the kept mask" + ); + + let words = words_for(planes.n_rows); + let f_slots = plan.filter.scratch_slots as usize; + let mut f_buf = vec![0u64; scratch_words_for(words, f_slots).expect("sized")]; + let mut scratch_words_total = f_buf.len(); + let mut f_scratch = Scratch::over(&mut f_buf, words, f_slots).expect("carves"); + + let before = BYTES.load(Ordering::Relaxed); + let kept = execute(&plan.filter, planes, &mut f_scratch, None).expect("runs"); + let mut alloc_bytes_exec = alloc_delta(before); + + let kept_bits = match kept { + Value::Mask(op) => mask_bits(planes, &f_scratch, op), + other => panic!("case {id}: group filter did not Keep a mask: {other:?}"), + }; + let widened_masks: Vec<&[u64]> = std::iter::once(kept_bits.as_slice()).collect(); + let widened = Planes { + n_rows: planes.n_rows, + masks: &widened_masks, + lanes: planes.lanes, + }; + + let mut ops = plan.filter.ops.len(); + let mut pairs = Vec::with_capacity(plan.groups.len()); + for (k, prog) in plan.groups.iter().enumerate() { + let g_slots = prog.scratch_slots as usize; + let mut g_buf = vec![0u64; scratch_words_for(words, g_slots).expect("sized")]; + scratch_words_total += g_buf.len(); + let mut g_scratch = Scratch::over(&mut g_buf, words, g_slots).expect("carves"); + + let before = BYTES.load(Ordering::Relaxed); + let v = execute(prog, &widened, &mut g_scratch, None).expect("runs"); + alloc_bytes_exec += alloc_delta(before); + ops += prog.ops.len(); + + let val_str = match v { + Value::Count(c) => c.to_string(), + Value::SumI64(s) => s.to_string(), + other => panic!("case {id}: unexpected group-{k} value: {other:?}"), + }; + pairs.push(format!("{k}:{val_str}")); + } + let encoded = pairs.join(";"); + let out_bytes = encoded.len(); + ( + encoded, + CaseMetrics { + ops, + scratch_words: scratch_words_total, + alloc_bytes_exec, + rows_materialized: 0, + index_vec_len: 0, + out_bytes, + programs: Some(plan.groups.len()), + }, + ) +} + +// --------------------------------------------------------------------- +// The fixture drift guard. +// --------------------------------------------------------------------- + +/// FAILS IF: the generator's bytes and the committed `tests/duckdb/data/` +/// CSVs have drifted apart — a change to `fixture.rs`'s generation order, +/// ranges, or CSV formatting that wasn't followed by regenerating the +/// committed files (and re-running `oracle.py`). +#[test] +fn committed_fixture_matches_generator() { + let fx = fixture::generate(); + let partner_committed = include_bytes!("duckdb/data/partner.csv"); + let doc_committed = include_bytes!("duckdb/data/doc.csv"); + let line_committed = include_bytes!("duckdb/data/line.csv"); + assert_eq!( + fx.partner_csv(), + partner_committed, + "partner.csv drifted from the generator" + ); + assert_eq!( + fx.doc_csv(), + doc_committed, + "doc.csv drifted from the generator" + ); + assert_eq!( + fx.line_csv(), + line_committed, + "line.csv drifted from the generator" + ); +} + +/// Not run by default — regenerates `tests/duckdb/data/*.csv` on disk. Run +/// once with `cargo test -p lance-graph-quack --test duckdb_differential \ +/// write_fixture -- --ignored` after any change to `fixture.rs`'s generation +/// logic, then re-run `oracle.py` (see `tests/duckdb/README.txt`) before +/// committing. +#[test] +#[ignore = "regenerates the committed fixture CSVs on disk; run explicitly"] +fn write_fixture() { + let fx = fixture::generate(); + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/duckdb/data"); + std::fs::create_dir_all(&dir).expect("data dir"); + fx.write_csv(&dir).expect("write fixture CSVs"); +} + +// --------------------------------------------------------------------- +// The 11 scalar/projection cases. +// --------------------------------------------------------------------- + +#[test] +fn sel_count_posted() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let filter = Filter::cmp(STATUS, Cmp::EqU32(1)); + let (actual, m) = run_query("sel_count_posted", &planes, filter, Agg::Count); + print_metric("sel_count_posted", &m); + assert_case(&cases, "sel_count_posted", &actual); +} + +#[test] +fn conj_count() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + // `cost_center < 4` over a lane restricted to `0..8` (3 bits) is exactly + // "bit 2 clear" — `Cmp::MatchU32 { pattern: 0, care: 0b100 }`. There is + // no ordered comparison over a `u32` lane in the IR (see `fixture.rs`'s + // doc on `LineTable::doc_id`), so a ternary match is the honest spelling + // here, not a workaround. + let filter = Filter::and([ + Filter::cmp(STATUS, Cmp::EqU32(1)), + Filter::cmp(AMOUNT, Cmp::GtI32(1000)), + Filter::cmp( + COST_CENTER, + Cmp::MatchU32 { + pattern: 0, + care: 0b100, + }, + ), + ]); + let (actual, m) = run_query("conj_count", &planes, filter, Agg::Count); + print_metric("conj_count", &m); + assert_case(&cases, "conj_count", &actual); +} + +#[test] +fn disj_count() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let filter = Filter::or([ + Filter::cmp(STATUS, Cmp::EqU32(2)), + Filter::cmp(AMOUNT, Cmp::LtI32(0)), + ]); + let (actual, m) = run_query("disj_count", &planes, filter, Agg::Count); + print_metric("disj_count", &m); + assert_case(&cases, "disj_count", &actual); +} + +#[test] +fn not_count() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let filter = Filter::negate(Filter::cmp(STATUS, Cmp::EqU32(1))); + let (actual, m) = run_query("not_count", &planes, filter, Agg::Count); + print_metric("not_count", &m); + assert_case(&cases, "not_count", &actual); +} + +#[test] +fn in_count() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let filter = Filter::in_u32(GL_ACCOUNT, [400_000, 600_000]); + let (actual, m) = run_query("in_count", &planes, filter, Agg::Count); + print_metric("in_count", &m); + assert_case(&cases, "in_count", &actual); +} + +#[test] +fn sum_posted() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let filter = Filter::cmp(STATUS, Cmp::EqU32(1)); + let (actual, m) = run_query("sum_posted", &planes, filter, Agg::SumI32(AMOUNT)); + print_metric("sum_posted", &m); + assert_case(&cases, "sum_posted", &actual); +} + +#[test] +fn min_posted_cc3() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let filter = Filter::and([ + Filter::cmp(STATUS, Cmp::EqU32(1)), + Filter::cmp(COST_CENTER, Cmp::EqU32(3)), + ]); + let (actual, m) = run_query("min_posted_cc3", &planes, filter, Agg::MinI32(AMOUNT)); + print_metric("min_posted_cc3", &m); + assert_case(&cases, "min_posted_cc3", &actual); +} + +#[test] +fn max_posted_cc3() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let filter = Filter::and([ + Filter::cmp(STATUS, Cmp::EqU32(1)), + Filter::cmp(COST_CENTER, Cmp::EqU32(3)), + ]); + let (actual, m) = run_query("max_posted_cc3", &planes, filter, Agg::MaxI32(AMOUNT)); + print_metric("max_posted_cc3", &m); + assert_case(&cases, "max_posted_cc3", &actual); +} + +#[test] +fn exists_neg() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let filter = Filter::cmp(AMOUNT, Cmp::LtI32(-4990)); + let (actual, m) = run_query("exists_neg", &planes, filter, Agg::Any); + print_metric("exists_neg", &m); + assert_case(&cases, "exists_neg", &actual); +} + +#[test] +fn rows_proj() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let filter = Filter::and([ + Filter::cmp(STATUS, Cmp::EqU32(2)), + Filter::cmp(QTY, Cmp::GtI32(45)), + ]); + let (actual, m) = run_query("rows_proj", &planes, filter, Agg::Rows); + print_metric("rows_proj", &m); + assert_case(&cases, "rows_proj", &actual); +} + +#[test] +fn range_docid() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + // Two compares (`GeI32`/`LtI32`), not `Cmp::Range`: `Cmp::Range` is + // minted only by `Filter::prefix_facet` from a validated + // `OrderedLaneWitness` (D-DIAMOND-1 R2) — it is not a general-purpose + // range spelling this suite is licensed to construct by hand, and + // `doc_id` here is a plain generated index, not a sealed ordered lane. + let filter = Filter::and([ + Filter::cmp(DOC_ID, Cmp::GeI32(128)), + Filter::cmp(DOC_ID, Cmp::LtI32(256)), + ]); + let (actual, m) = run_query("range_docid", &planes, filter, Agg::Count); + print_metric("range_docid", &m); + assert_case(&cases, "range_docid", &actual); +} + +// --------------------------------------------------------------------- +// The 2 GROUP BY cases. +// --------------------------------------------------------------------- + +#[test] +fn group_count_cc() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let filter = Filter::cmp(STATUS, Cmp::EqU32(1)); + let (actual, m) = run_group( + "group_count_cc", + &planes, + filter, + COST_CENTER, + 8, + Agg::Count, + ); + print_metric("group_count_cc", &m); + assert_case(&cases, "group_count_cc", &actual); +} + +#[test] +fn group_sum_cc() { + let cases = load_cases(); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let filter = Filter::cmp(STATUS, Cmp::EqU32(1)); + let (actual, m) = run_group( + "group_sum_cc", + &planes, + filter, + COST_CENTER, + 8, + Agg::SumI32(AMOUNT), + ); + print_metric("group_sum_cc", &m); + assert_case(&cases, "group_sum_cc", &actual); +} + +// --------------------------------------------------------------------- +// The 3 join cases — the open seam. Quack lowers filters and one-table +// aggregates; it has no join lowering (`src/lib.rs`'s `# Status` section +// names the gap explicitly). Each of these loads its oracle-computed +// `expected` — proving the DuckDB side is real and committed — then +// `todo!()`s, so the case is red until join lowering exists rather than +// silently absent from the suite. +// --------------------------------------------------------------------- + +#[test] +#[should_panic(expected = "NO JOIN LOWERING")] +fn join_sum_country_is_the_open_seam() { + let cases = load_cases(); + let expected = expected_only(&cases, "join_sum_country"); + assert!( + !expected.is_empty(), + "join_sum_country has no oracle value — run oracle.py first" + ); + todo!("NO JOIN LOWERING: join_sum_country"); +} + +#[test] +#[should_panic(expected = "NO JOIN LOWERING")] +fn join_count_docs_with_posted_is_the_open_seam() { + let cases = load_cases(); + let expected = expected_only(&cases, "join_count_docs_with_posted"); + assert!( + !expected.is_empty(), + "join_count_docs_with_posted has no oracle value — run oracle.py first" + ); + todo!("NO JOIN LOWERING: join_count_docs_with_posted"); +} + +#[test] +#[should_panic(expected = "NO JOIN LOWERING")] +fn join_group_sum_country_is_the_open_seam() { + let cases = load_cases(); + let expected = expected_only(&cases, "join_group_sum_country"); + assert!( + !expected.is_empty(), + "join_group_sum_country has no oracle value — run oracle.py first" + ); + todo!("NO JOIN LOWERING: join_group_sum_country"); +} From 663b87921e8ea1cadea15b31ae5cd79d87485123 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 20:38:43 +0000 Subject: [PATCH 02/15] =?UTF-8?q?mask-risc=20+=20quack:=20JOIN=20as=20a=20?= =?UTF-8?q?fold=20=E2=80=94=20Gather=20/=20ScatterOr=20ops,=20one-terminal?= =?UTF-8?q?=20GROUP=20BY=20SUM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two falsified seams from the harness close without a hash join, a row iterator, or a pair relation. mask-risc (additive; `execute`/`reference_execute` signatures unchanged, so lance-graph-java keeps compiling against them): - `MaskOp::Gather { lane, foreign, dst }` — the fk semijoin: dst[i] = foreign_plane[lane[i]], out-of-range FALSE. A foreign plane is a mask over ANOTHER table's rows, passed through the new `Foreign` argument of `execute_into`, never a `Planes::masks` member. - `Terminal::ScatterOrU32 { mask, lane, out_rows }` — the one-to-many hop, written into the caller's `Out::Mask`. - `Terminal::GroupSumI32 { mask, key, val }` — all groups in one pass into the caller's `Out::I64` (its length is the group universe). - `Out<'_>` (None / I32 / I64 / Mask); `validate` takes the foreign set and an `OutShape`; the row oracle implements all three; `tests/foreign.rs` pins executor == oracle including the refusal parity (wrong lane kind, foreign index out of range, mis-shaped Out). L1 (no allocation in execute) still test-enforced. quack: `Filter::Semijoin { fk, foreign }` is a leaf like `Cmp`, so it composes under And/Or/Not and fuses; `Agg::ScatterOrU32`, `Agg::GroupSumI32`. Harness, measured single-threaded: join_sum_country programs=2 ops=5 alloc_bytes_exec=0 pair_relation_bytes=0 join_count_docs_with_posted programs=2 ops=1 alloc_bytes_exec=0 pair_relation_bytes=0 group_sum_cc programs=1 ops=1 (was programs=8 ops=9; both agree with DuckDB) All values equal the DuckDB oracle. `join_group_sum_country` stays red by design until the fk-keyed group-sum terminal lands (the ndarray primitive `masked_group_sum_i32_via` exists as of ndarray#318). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/lance-graph-mask-risc/src/exec.rs | 95 +++- crates/lance-graph-mask-risc/src/ir.rs | 81 ++- crates/lance-graph-mask-risc/src/lib.rs | 28 +- crates/lance-graph-mask-risc/src/reference.rs | 233 +++++++- crates/lance-graph-mask-risc/src/value.rs | 36 ++ crates/lance-graph-mask-risc/tests/foreign.rs | 501 ++++++++++++++++++ crates/lance-graph-quack/src/lib.rs | 327 ++++++++++++ .../lance-graph-quack/tests/duckdb/fixture.rs | 68 ++- .../tests/duckdb_differential.rs | 311 +++++++++-- 9 files changed, 1583 insertions(+), 97 deletions(-) create mode 100644 crates/lance-graph-mask-risc/tests/foreign.rs diff --git a/crates/lance-graph-mask-risc/src/exec.rs b/crates/lance-graph-mask-risc/src/exec.rs index 374c63b4e..971be61f6 100644 --- a/crates/lance-graph-mask-risc/src/exec.rs +++ b/crates/lance-graph-mask-risc/src/exec.rs @@ -26,17 +26,20 @@ use ndarray::simd::{ blend_i32, eq_i32_to_mask, eq_i32_to_mask_under, eq_u32_to_mask, eq_u32_to_mask_under, ge_i32_to_mask, ge_i32_to_mask_under, gt_i32_to_mask, gt_i32_to_mask_under, le_i32_to_mask, le_i32_to_mask_under, lt_i32_to_mask, lt_i32_to_mask_under, mask_all, mask_and, - mask_and_assign, mask_andnot, mask_andnot_assign, mask_any, mask_not, mask_not_assign, mask_or, - mask_or_assign, mask_set_range, mask_xor, mask_xor_assign, masked_max_i32, masked_min_i32, - masked_sum_i32, ne_i32_to_mask, ne_i32_to_mask_under, ne_u32_to_mask, ne_u32_to_mask_under, - popcount_batch_u64, ternary_match_u32_to_mask, ternary_match_u32_to_mask_under, - ternary_match_u64_to_mask, ternary_match_u64_to_mask_under, + mask_and_assign, mask_andnot, mask_andnot_assign, mask_any, mask_gather_u32, mask_not, + mask_not_assign, mask_or, mask_or_assign, mask_scatter_or_u32, mask_set_range, mask_xor, + mask_xor_assign, masked_group_sum_i32, masked_max_i32, masked_min_i32, masked_sum_i32, + ne_i32_to_mask, ne_i32_to_mask_under, ne_u32_to_mask, ne_u32_to_mask_under, popcount_batch_u64, + ternary_match_u32_to_mask, ternary_match_u32_to_mask_under, ternary_match_u64_to_mask, + ternary_match_u64_to_mask_under, }; -use crate::ir::{LaneRef, MaskOp, Operand, Planes, Pred, Program, Terminal, MAX_SCRATCH_SLOTS}; -use crate::reference::validate; +use crate::ir::{ + Foreign, LaneRef, MaskOp, Operand, Planes, Pred, Program, Terminal, MAX_SCRATCH_SLOTS, +}; +use crate::reference::{out_shape, validate}; use crate::ternlog_dispatch::{ternlog_dispatch, ternlog_dispatch_assign}; -use crate::value::{ExecError, Value}; +use crate::value::{ExecError, Out, Value}; use crate::words_for; /// Where a [`Scratch`]'s words live: owned by the arena, or borrowed from a @@ -549,11 +552,39 @@ fn run_pred<'a>( /// destination a [`Terminal::BlendI32`] writes. Validation is total and /// happens before any result write, so an `Err` leaves the scratch slots and /// `out` untouched. +/// +/// A thin wrapper over [`execute_into`]: no foreign planes, and `out` widened +/// to [`Out::I32`] (or [`Out::None`] for `out: None`) — the shape every +/// caller of this crate already had before [`MaskOp::Gather`] existed. pub fn execute( program: &Program, planes: &Planes<'_>, scratch: &mut Scratch<'_>, out: Option<&mut [i32]>, +) -> Result { + execute_into( + program, + planes, + &Foreign::NONE, + scratch, + out.map_or(Out::None, Out::I32), + ) +} + +/// Run `program` over `planes` with the caller's `scratch`, resolving any +/// [`MaskOp::Gather`] against `foreign`; `out` is the destination a +/// [`Terminal::BlendI32`] / [`Terminal::ScatterOrU32`] / +/// [`Terminal::GroupSumI32`] writes (every other terminal ignores it — an +/// `Out` of the wrong shape for the terminal that IS present is refused by +/// [`validate`], never silently accepted and silently not written). +/// Validation is total and happens before any result write, so an `Err` +/// leaves the scratch slots and `out` untouched. +pub fn execute_into( + program: &Program, + planes: &Planes<'_>, + foreign: &Foreign<'_>, + scratch: &mut Scratch<'_>, + out: Out<'_>, ) -> Result { // BEFORE the capacity check, not after: an over-declared count is a lie // about the PROGRAM, and the caller's buffer is irrelevant to it. Checked @@ -583,7 +614,8 @@ pub fn execute( validate( program, planes, - out.as_deref().map(<[i32]>::len), + foreign, + out_shape(&out), scratch.written_mut(), )?; let n_rows = planes.n_rows; @@ -683,6 +715,19 @@ pub fn execute( clear_tail(x, n_rows); } } + MaskOp::Gather { + lane, + foreign: fidx, + dst, + } => { + // No aliasing shape to consider: unlike every other op, + // `Gather` never reads `dst` as an input — it only writes it + // — so there is no `dst == a` case to route to an in-place + // facade form. + let (d, _rest) = scratch.split(dst); + let fp = &foreign.planes[usize::from(fidx)]; + mask_gather_u32(fp.words, fp.rows, lane_u32(planes, lane), d); + } } } @@ -706,8 +751,8 @@ pub fn execute( read(planes, &all, mask), )), Terminal::BlendI32 { mask, then, els } => { - // `validate` already refused a missing or mis-sized `out`. - if let Some(o) = out { + // `validate` already refused a missing or mis-shaped `out`. + if let Out::I32(o) = out { blend_i32( read(planes, &all, mask), lane_i32(planes, then), @@ -717,6 +762,34 @@ pub fn execute( } Value::Blended } + Terminal::ScatterOrU32 { + mask, + lane, + out_rows, + } => { + // `validate` already refused a missing or mis-sized `out`. + if let Out::Mask(o) = out { + mask_scatter_or_u32( + read(planes, &all, mask), + lane_u32(planes, lane), + o, + out_rows as usize, + ); + } + Value::Scattered + } + Terminal::GroupSumI32 { mask, key, val } => { + // `validate` already refused a missing or too-small `out`. + if let Out::I64(o) = out { + masked_group_sum_i32( + read(planes, &all, mask), + lane_u32(planes, key), + lane_i32(planes, val), + o, + ); + } + Value::GroupSummed + } Terminal::Keep { mask } => Value::Mask(mask), }) } diff --git a/crates/lance-graph-mask-risc/src/ir.rs b/crates/lance-graph-mask-risc/src/ir.rs index 9fd4b0a14..c7c659677 100644 --- a/crates/lance-graph-mask-risc/src/ir.rs +++ b/crates/lance-graph-mask-risc/src/ir.rs @@ -56,6 +56,35 @@ pub struct Planes<'a> { pub lanes: &'a [LaneRef<'a>], } +/// One foreign table's resident validity, as bits over ITS OWN row space — +/// never `n_rows`-checked against the program's own [`Planes`], because it +/// isn't the same population. +#[derive(Debug, Clone, Copy)] +pub struct ForeignPlane<'a> { + /// The plane's packed bits, LSB-first, `words_for(rows)` long. + pub words: &'a [u64], + /// The row count this plane spans — the FOREIGN table's row count, not + /// the executing program's `n_rows`. + pub rows: usize, +} + +/// The foreign-table masks a program's [`MaskOp::Gather`] may address — +/// every entry a mask over ANOTHER table's row space. A foreign plane is +/// never a member of [`Planes::masks`] and its length is never checked +/// against the executing program's `n_rows`: that would be checking a +/// population against a population it is not. +#[derive(Debug, Clone, Copy)] +pub struct Foreign<'a> { + /// Foreign planes, indexed by [`MaskOp::Gather::foreign`]. + pub planes: &'a [ForeignPlane<'a>], +} + +impl Foreign<'_> { + /// The empty foreign set — every program that names no `Gather` runs + /// against this. + pub const NONE: Foreign<'static> = Foreign { planes: &[] }; +} + /// A value-lane predicate that produces a mask — the vector half of a /// columnar filter. `lane` indexes [`Planes::lanes`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -154,6 +183,19 @@ pub enum MaskOp { c: Operand, dst: u16, }, + /// `dst[i] = foreign.planes[foreign].words[lane_u32[i]]` — the fk + /// SEMIJOIN: `dst` is set for row `i` exactly when `lane`'s value names a + /// row that survives on the foreign table. Out-of-range is the + /// zero-fallback ([`crate::exec::Gather`]'s underlying facade primitive + /// contract), never an error: `line WHERE EXISTS partner p ON p.rid = + /// line.partner_id AND ` is `Gather{lane: partner_id, foreign: + /// }` — the fk gather. + /// + /// No `under` field: the facade primitive it lowers to + /// (`ndarray::simd::mask_gather_u32`) takes no gate. Compose a gate with + /// [`MaskOp::And`] instead — the same shape a gated `Ternlog` or a gated + /// `Range` write already uses when the fused kernel doesn't exist. + Gather { lane: u16, foreign: u16, dst: u16 }, } /// What the program produces. Exactly one per program; the mask it reads is @@ -182,6 +224,28 @@ pub enum Terminal { /// The final mask itself stays in `mask` (a scratch slot the caller /// reads back); nothing is reduced. Keep { mask: Operand }, + /// The one-to-many hop: for every row `i` where `mask` holds, sets bit + /// `lane[i]` of the caller's `Out::Mask` buffer — provided it is `< + /// out_rows`. Writes `out[i] |= lane[i]`'s target, union across + /// repeats — [`ndarray::simd::mask_scatter_or_u32`]'s contract, out of + /// range silently dropped. The caller's buffer must be exactly + /// `words_for(out_rows)` long; a follow-on program then reads it back + /// as a [`ForeignPlane`] or an input [`Planes::masks`] entry. + ScatterOrU32 { + mask: Operand, + lane: u16, + out_rows: u32, + }, + /// The one-terminal `GROUP BY … SUM`: for every row `i` where `mask` + /// holds, adds `val[i]` into the caller's `Out::I64` buffer at index + /// `key[i]` — provided `key[i] < out.len()` + /// ([`ndarray::simd::masked_group_sum_i32`]'s contract; a key past the + /// group universe is dropped, not an error). `out.len()` IS the group + /// universe `K`. Same carry bound as [`Terminal::MaskedSumI32`] + /// ([`MASKED_SUM_I32_MAX_ROWS`]) — a per-key sum can never overflow + /// past it either, since it is strictly less work than the one-group + /// sum the bound was derived against. + GroupSumI32 { mask: Operand, key: u16, val: u16 }, } /// The widest plane [`Terminal::MaskedSumI32`] is defined on: `2^32` rows. @@ -275,6 +339,12 @@ impl Program { touch(c); touch(Operand::Scratch(dst)); } + // `foreign` is not a `Scratch`/`Plane` operand — it indexes + // `Foreign::planes`, a wholly separate address space this + // touch-based accounting has no business over. + MaskOp::Gather { dst, .. } => { + touch(Operand::Scratch(dst)); + } } } match terminal { @@ -285,6 +355,8 @@ impl Program { | Terminal::MaskedMinI32 { mask, .. } | Terminal::MaskedMaxI32 { mask, .. } | Terminal::BlendI32 { mask, .. } + | Terminal::ScatterOrU32 { mask, .. } + | Terminal::GroupSumI32 { mask, .. } | Terminal::Keep { mask } => touch(mask), } Self { @@ -331,6 +403,7 @@ impl Program { | MaskOp::AndNot { .. } => h.two_input += 1, MaskOp::Not { .. } => h.not += 1, MaskOp::Ternlog { .. } => h.ternlog += 1, + MaskOp::Gather { .. } => h.gather += 1, } } h @@ -360,12 +433,17 @@ pub struct OpHistogram { /// write with `mask_and_assign`. Unlike every lane predicate, which has a /// fused `*_to_mask_under` kernel, no `mask_set_range_under` exists. pub ranges: usize, + /// `Gather` (the fk semijoin). One pass over the destination mask, the + /// same accounting as a range write — it reads no value LANE (it reads + /// a foreign PLANE at a data-dependent row), so it is counted apart from + /// [`predicates`](Self::predicates) for the same reason `ranges` is. + pub gather: usize, } impl OpHistogram { /// Total mask-word passes the program spends after its predicates. pub fn mask_passes(&self) -> usize { - self.two_input + self.not + self.ternlog + self.ranges + self.two_input + self.not + self.ternlog + self.ranges + self.gather } } @@ -579,6 +657,7 @@ mod tests { not: 1, ternlog: 1, ranges: 0, + gather: 0, } ); assert_eq!(h.mask_passes(), 3); diff --git a/crates/lance-graph-mask-risc/src/lib.rs b/crates/lance-graph-mask-risc/src/lib.rs index 1f3967f4b..ce5b3ab6e 100644 --- a/crates/lance-graph-mask-risc/src/lib.rs +++ b/crates/lance-graph-mask-risc/src/lib.rs @@ -13,12 +13,16 @@ //! word lowers to is its question, answered by its own parity tests, and //! running this suite under another realization would test ndarray through a //! proxy rather than test this crate. `tests/no_alloc.rs` pins the -//! zero-allocation law. Still absent, named: -//! `hop` (PR5); a strided `Operand` — the gap is in THIS IR, not in T1: -//! `ndarray::simd` already ships `ternary_match_strided_to_mask`, -//! `eq_u32_strided_to_mask` and `masked_strided_group_sum`, and nothing here -//! can name a `(base, stride, group)` source; and the Cypher `mask_lower` -//! seam (the Cypher plan's Wave 1 consumes this crate). +//! zero-allocation law. `hop` (PR5) LANDED as [`MaskOp::Gather`] (the fk +//! semijoin, over [`ir::Foreign`]) and [`Terminal::ScatterOrU32`] (the +//! one-to-many hop back); [`Terminal::GroupSumI32`] is the one-terminal +//! `GROUP BY … SUM`. Still absent, named: a strided `Operand` — the gap is +//! in THIS IR, not in T1: `ndarray::simd` already ships +//! `ternary_match_strided_to_mask`, `eq_u32_strided_to_mask` and +//! `masked_strided_group_sum`, and nothing here can name a `(base, stride, +//! group)` source; a via-key group-sum ([`ndarray::simd::masked_group_sum_i32_via`], +//! `SUM(...) GROUP BY partner.country`); and the Cypher `mask_lower` seam +//! (the Cypher plan's Wave 1 consumes this crate). //! //! One duplication is filed rather than resolved here: `lgj-abi` already //! carries its own runtime-immediate → const-generic ternlog bridge @@ -103,15 +107,17 @@ pub mod reference; pub mod ternlog_dispatch; pub mod value; -pub use exec::{execute, materialize_rows, scratch_words_for, Scratch}; +pub use exec::{execute, execute_into, materialize_rows, scratch_words_for, Scratch}; pub use fuse::{fuse, fuse_program, ternlog_imm, BoolExpr, FuseError, Fused}; pub use ir::{ - LaneRef, MaskOp, Operand, Planes, Pred, Program, Terminal, MASKED_SUM_I32_MAX_ROWS, - MAX_SCRATCH_SLOTS, + Foreign, ForeignPlane, LaneRef, MaskOp, Operand, Planes, Pred, Program, Terminal, + MASKED_SUM_I32_MAX_ROWS, MAX_SCRATCH_SLOTS, +}; +pub use reference::{ + reference_execute, reference_execute_into, reference_scratch, reference_scratch_with_foreign, }; -pub use reference::{reference_execute, reference_scratch}; pub use ternlog_dispatch::{ternlog_dispatch, ternlog_dispatch_assign}; -pub use value::{ExecError, LaneKind, Value}; +pub use value::{ExecError, LaneKind, Out, Value}; /// Number of `u64` words a mask over `n_rows` occupies. #[inline] diff --git a/crates/lance-graph-mask-risc/src/reference.rs b/crates/lance-graph-mask-risc/src/reference.rs index 997c4ef31..df5145c41 100644 --- a/crates/lance-graph-mask-risc/src/reference.rs +++ b/crates/lance-graph-mask-risc/src/reference.rs @@ -17,12 +17,36 @@ //! path a consumer should reach for. use crate::ir::{ - LaneRef, MaskOp, Operand, Planes, Pred, Program, Terminal, MASKED_SUM_I32_MAX_ROWS, + Foreign, LaneRef, MaskOp, Operand, Planes, Pred, Program, Terminal, MASKED_SUM_I32_MAX_ROWS, MAX_SCRATCH_SLOTS, }; -use crate::value::{ExecError, LaneKind, Value}; +use crate::value::{ExecError, LaneKind, Out, Value}; use crate::words_for; +/// The caller's terminal-out, described by SHAPE rather than borrowed — what +/// [`validate`] needs to check a terminal's destination without holding the +/// destination itself (which [`execute`](crate::exec::execute) and +/// [`reference_execute_into`] still need afterwards). One spelling for the +/// four `Out` shapes, read off an `&Out<'_>` by [`out_shape`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum OutShape { + None, + I32(usize), + I64(usize), + Mask(usize), +} + +/// [`OutShape`] of a borrowed `out` — the caller keeps `out` itself to write +/// into after validation passes. +pub(crate) fn out_shape(out: &Out<'_>) -> OutShape { + match out { + Out::None => OutShape::None, + Out::I32(v) => OutShape::I32(v.len()), + Out::I64(v) => OutShape::I64(v.len()), + Out::Mask(v) => OutShape::Mask(v.len()), + } +} + fn kind_of(lane: &LaneRef<'_>) -> LaneKind { match lane { LaneRef::I32(_) => LaneKind::I32, @@ -193,26 +217,29 @@ fn dst_of(op: &MaskOp) -> u16 { | MaskOp::Xor { dst, .. } | MaskOp::AndNot { dst, .. } | MaskOp::Not { dst, .. } - | MaskOp::Ternlog { dst, .. } => dst, + | MaskOp::Ternlog { dst, .. } + | MaskOp::Gather { dst, .. } => dst, } } /// The validation rules, in the order both the executor and the oracle apply /// them: (1) addressable scratch and plane counts; (2) plane and lane lengths, /// then a dirty plane tail; (3) every op in program order, operands in field -/// order; (4) the terminal's mask, its lanes, the sum bound, and the blend -/// destination. `out_len` is the caller's `out` slice length, if any. -/// `written_bits` supplies one caller-owned bit per declared scratch slot. -/// Every path that reaches step (3) clears and reuses that prefix first, -/// including paths that then return an error — but step (1) returns ahead of -/// the clear, so an `ScratchSlotsUnaddressable` refusal leaves the caller's -/// bits exactly as they were. That is deliberate: the refusal happens before -/// the declared count is known to be addressable, and sizing anything from it -/// is the failure that check exists to prevent. +/// order (a [`MaskOp::Gather`]'s `foreign` is checked against `foreign`, never +/// against `planes`); (4) the terminal's mask, its lanes, the sum bound, and +/// the destination `out` needs, described by [`OutShape`] rather than +/// borrowed. `written_bits` supplies one caller-owned bit per declared +/// scratch slot. Every path that reaches step (3) clears and reuses that +/// prefix first, including paths that then return an error — but step (1) +/// returns ahead of the clear, so an `ScratchSlotsUnaddressable` refusal +/// leaves the caller's bits exactly as they were. That is deliberate: the +/// refusal happens before the declared count is known to be addressable, and +/// sizing anything from it is the failure that check exists to prevent. pub(crate) fn validate( p: &Program, planes: &Planes<'_>, - out_len: Option, + foreign: &Foreign<'_>, + out: OutShape, written_bits: &mut [u64], ) -> Result<(), ExecError> { // FIRST, before anything walks a plane or an op: a declared slot count @@ -324,6 +351,26 @@ pub(crate) fn validate( written(b)?; written(c)?; } + MaskOp::Gather { + lane, + foreign: fidx, + dst, + } => { + check_lane(planes, lane, LaneKind::U32)?; + let fp = foreign + .planes + .get(usize::from(fidx)) + .ok_or(ExecError::ForeignOutOfRange(fidx))?; + let want = words_for(fp.rows); + if fp.words.len() < want { + return Err(ExecError::LenMismatch { + what: "foreign", + expected: want, + found: fp.words.len(), + }); + } + check_operand(p, planes, Operand::Scratch(dst))?; + } } written_slots.mark(dst_of(op)); } @@ -354,14 +401,55 @@ pub(crate) fn validate( written_slots.readable(mask)?; check_lane(planes, then, LaneKind::I32)?; check_lane(planes, els, LaneKind::I32)?; - match out_len { - None => Err(ExecError::BlendNeedsOut), - Some(len) if len != n => Err(ExecError::LenMismatch { + match out { + OutShape::None => Err(ExecError::BlendNeedsOut), + OutShape::I32(len) if len != n => Err(ExecError::LenMismatch { what: "out", expected: n, found: len, }), - Some(_) => Ok(()), + OutShape::I32(_) => Ok(()), + OutShape::I64(_) | OutShape::Mask(_) => Err(ExecError::BlendNeedsOut), + } + } + Terminal::ScatterOrU32 { + mask, + lane, + out_rows, + } => { + check_operand(p, planes, mask)?; + written_slots.readable(mask)?; + check_lane(planes, lane, LaneKind::U32)?; + let want = words_for(out_rows as usize); + match out { + OutShape::Mask(len) if len == want => Ok(()), + OutShape::Mask(len) => Err(ExecError::LenMismatch { + what: "out", + expected: want, + found: len, + }), + OutShape::None | OutShape::I32(_) | OutShape::I64(_) => { + Err(ExecError::TerminalNeedsOut { + what: "ScatterOrU32", + }) + } + } + } + Terminal::GroupSumI32 { mask, key, val } => { + check_operand(p, planes, mask)?; + written_slots.readable(mask)?; + check_lane(planes, key, LaneKind::U32)?; + check_lane(planes, val, LaneKind::I32)?; + if n > MASKED_SUM_I32_MAX_ROWS { + return Err(ExecError::SumRowBound { n_rows: n }); + } + match out { + OutShape::I64(len) if len >= 1 => Ok(()), + OutShape::None | OutShape::I32(_) | OutShape::I64(_) | OutShape::Mask(_) => { + Err(ExecError::TerminalNeedsOut { + what: "GroupSumI32", + }) + } } } } @@ -378,6 +466,13 @@ fn i32_at(planes: &Planes<'_>, lane: u16, row: usize) -> i32 { } } +fn u32_at(planes: &Planes<'_>, lane: u16, row: usize) -> u32 { + match planes.lanes[usize::from(lane)] { + LaneRef::U32(v) => v[row], + _ => 0, + } +} + /// One predicate, one row — the scalar SPEC of what each `Pred` means. /// /// This is the definition the executor's `ndarray::simd` delegation is diffed @@ -461,7 +556,7 @@ impl Rows { /// slot no earlier op wrote (`ExecError::ScratchReadBeforeWrite`), and it /// refuses a `scratch_slots` count past `MAX_SCRATCH_SLOTS` before this /// allocates anything proportional to it. -fn run(p: &Program, planes: &Planes<'_>) -> Rows { +fn run(p: &Program, planes: &Planes<'_>, foreign: &Foreign<'_>) -> Rows { let n = planes.n_rows; let mut rows = Rows { slots: (0..p.scratch_slots).map(|_| vec![false; n]).collect(), @@ -485,6 +580,19 @@ fn run(p: &Program, planes: &Planes<'_>) -> Rows { | u8::from(rows.bit(planes, c, row)); (imm >> idx) & 1 == 1 } + // `idx` is validated `LaneKind::U32`, so `u32_at` never falls + // back to `0` on a well-typed program. Out-of-range is the + // zero-fallback the facade primitive owns; the oracle mirrors + // it as a plain bounds check rather than reading past `fp`. + MaskOp::Gather { + lane, + foreign: fidx, + .. + } => { + let idx = u32_at(planes, lane, row) as usize; + let fp = &foreign.planes[usize::from(fidx)]; + idx < fp.rows && (fp.words[idx / 64] >> (idx % 64)) & 1 == 1 + } }; let dst = match *op { MaskOp::Pred { dst, .. } @@ -493,7 +601,8 @@ fn run(p: &Program, planes: &Planes<'_>) -> Rows { | MaskOp::Xor { dst, .. } | MaskOp::AndNot { dst, .. } | MaskOp::Not { dst, .. } - | MaskOp::Ternlog { dst, .. } => dst, + | MaskOp::Ternlog { dst, .. } + | MaskOp::Gather { dst, .. } => dst, }; rows.set(dst, row, v); } @@ -525,20 +634,36 @@ fn written_bitmap(p: &Program) -> Result, ExecError> { /// Evaluate `p` over `planes` row by row; `out` is the destination a /// [`Terminal::BlendI32`] writes. Same validation, same [`Value`], same -/// [`ExecError`] as the executor. +/// [`ExecError`] as the executor — the same shape +/// [`execute`](crate::exec::execute) keeps against +/// [`execute_into`](crate::exec::execute_into). pub fn reference_execute( p: &Program, planes: &Planes<'_>, out: Option<&mut [i32]>, +) -> Result { + reference_execute_into(p, planes, &Foreign::NONE, out.map_or(Out::None, Out::I32)) +} + +/// Evaluate `p` over `planes` row by row, resolving any [`MaskOp::Gather`] +/// against `foreign`; `out` is the destination the terminal writes +/// ([`Terminal::BlendI32`]/[`Terminal::ScatterOrU32`]/ +/// [`Terminal::GroupSumI32`]; every other terminal ignores it). Same +/// validation, same [`Value`], same [`ExecError`] as the executor. +pub fn reference_execute_into( + p: &Program, + planes: &Planes<'_>, + foreign: &Foreign<'_>, + out: Out<'_>, ) -> Result { // The oracle allocates by design (its `Rows` arena is this crate's one // named L5 exemption), so a local bitmap costs it nothing it was not // already paying — but it is sized through the guarded helper, not from // the declared count. let mut bits = written_bitmap(p)?; - validate(p, planes, out.as_deref().map(<[i32]>::len), &mut bits)?; + validate(p, planes, foreign, out_shape(&out), &mut bits)?; let n = planes.n_rows; - let rows = run(p, planes); + let rows = run(p, planes, foreign); let rows = &rows; let survivors = |mask: Operand| (0..n).filter(move |&r| rows.bit(planes, mask, r)); Ok(match p.terminal { @@ -557,7 +682,7 @@ pub fn reference_execute( Value::OptI32(survivors(mask).map(|r| i32_at(planes, lane, r)).max()) } Terminal::BlendI32 { mask, then, els } => { - if let Some(o) = out { + if let Out::I32(o) = out { for (r, slot) in o.iter_mut().enumerate() { let pick = if rows.bit(planes, mask, r) { then } else { els }; *slot = i32_at(planes, pick, r); @@ -565,6 +690,39 @@ pub fn reference_execute( } Value::Blended } + Terminal::ScatterOrU32 { + mask, + lane, + out_rows, + } => { + if let Out::Mask(o) = out { + for w in o.iter_mut() { + *w = 0; + } + for r in survivors(mask) { + let idx = u32_at(planes, lane, r) as usize; + if idx < out_rows as usize { + o[idx / 64] |= 1u64 << (idx % 64); + } + } + } + Value::Scattered + } + Terminal::GroupSumI32 { mask, key, val } => { + if let Out::I64(o) = out { + for x in o.iter_mut() { + *x = 0; + } + for r in survivors(mask) { + let k = u32_at(planes, key, r) as usize; + if k < o.len() { + let v = i64::from(i32_at(planes, val, r)); + o[k] = o[k].wrapping_add(v); + } + } + } + Value::GroupSummed + } Terminal::Keep { mask } => Value::Mask(mask), }) } @@ -572,15 +730,32 @@ pub fn reference_execute( /// Every scratch slot's FINAL contents after `p` runs, packed LSB-first with /// tail bits zero — the shape an executor's `Scratch` holds, built here by /// hand so a `Keep` result and every intermediate can be diffed word by word. +/// `p` sees no foreign planes; see [`reference_scratch_with_foreign`] for a +/// program that names a [`MaskOp::Gather`]. pub fn reference_scratch(p: &Program, planes: &Planes<'_>) -> Result>, ExecError> { - // `Some(n_rows)`, not `None`: slot contents are a function of the OPS - // alone, so a `BlendI32` terminal's missing `out` must not make this - // report `BlendNeedsOut` — that silently skipped the differential's - // whole scratch comparison for every blend program. + reference_scratch_with_foreign(p, planes, &Foreign::NONE) +} + +/// [`reference_scratch`], resolving any [`MaskOp::Gather`] against `foreign`. +pub fn reference_scratch_with_foreign( + p: &Program, + planes: &Planes<'_>, + foreign: &Foreign<'_>, +) -> Result>, ExecError> { + // `OutShape::I32(n_rows)`, not `OutShape::None`: slot contents are a + // function of the OPS alone, so a `BlendI32` terminal's missing `out` + // must not make this report `BlendNeedsOut` — that silently skipped the + // differential's whole scratch comparison for every blend program. A + // `ScatterOrU32`/`GroupSumI32` program still validates fine against this + // shape because their own `out` requirement is checked independently of + // `BlendI32`'s — but this helper cannot also satisfy THEIR `out` + // requirement at once, so a caller diffing a scatter or group-sum + // program's scratch reads `Err` and should call [`validate`]'s shape + // directly instead (or simply not call this helper for those programs). let mut bits = written_bitmap(p)?; - validate(p, planes, Some(planes.n_rows), &mut bits)?; + validate(p, planes, foreign, OutShape::I32(planes.n_rows), &mut bits)?; let n = planes.n_rows; - let rows = run(p, planes); + let rows = run(p, planes, foreign); Ok(rows .slots .iter() diff --git a/crates/lance-graph-mask-risc/src/value.rs b/crates/lance-graph-mask-risc/src/value.rs index def20dab8..307567af3 100644 --- a/crates/lance-graph-mask-risc/src/value.rs +++ b/crates/lance-graph-mask-risc/src/value.rs @@ -22,6 +22,29 @@ pub enum Value { OptI32(Option), /// [`crate::Terminal::BlendI32`]: the caller's `out` slice was written. Blended, + /// [`crate::Terminal::ScatterOrU32`]: the caller's `Out::Mask` buffer was + /// written. + Scattered, + /// [`crate::Terminal::GroupSumI32`]: the caller's `Out::I64` buffer was + /// written, one slot per group. + GroupSummed, +} + +/// The caller's terminal-result destination — one variant per shape a +/// [`crate::Terminal`] may write into, plus `None` for a terminal that writes +/// nothing (every reducer that returns its answer as a [`Value`] instead). +#[derive(Debug, PartialEq, Eq)] +pub enum Out<'a> { + /// No destination buffer is needed for this terminal. + None, + /// [`crate::Terminal::BlendI32`]'s destination, `n_rows` long. + I32(&'a mut [i32]), + /// [`crate::Terminal::GroupSumI32`]'s destination — one `i64` per group, + /// its length IS the group universe `K`. + I64(&'a mut [i64]), + /// [`crate::Terminal::ScatterOrU32`]'s destination, `words_for(out_rows)` + /// long. + Mask(&'a mut [u64]), } /// The lane width a predicate or terminal expects, for [`ExecError::LaneKind`]. @@ -108,4 +131,17 @@ pub enum ExecError { /// `mask_set_range` a bound past the scratch words (its own assert would /// panic) and the oracle never indexes a row that does not exist. RangeOutOfBounds { lo: u32, hi: u32, n_rows: usize }, + /// [`crate::MaskOp::Gather`]'s `foreign` names no entry of the caller's + /// [`crate::Foreign::planes`]. + ForeignOutOfRange(u16), + /// A terminal's [`crate::Out`] is missing, or is the wrong SHAPE + /// (`Out::I32` where an `Out::I64` was needed, and so on) — or, for + /// [`crate::Terminal::ScatterOrU32`], sized to the wrong `out_rows`, or + /// for [`crate::Terminal::GroupSumI32`], the empty `Out::I64([])` + /// (`K == 0` names no group at all). `what` names the terminal. + /// [`crate::Terminal::BlendI32`] keeps its own [`Self::BlendNeedsOut`] + /// rather than routing through here — two spellings of "needs an `out`", + /// kept apart because `BlendI32` predates `Out` and every existing + /// caller already matches on the old variant. + TerminalNeedsOut { what: &'static str }, } diff --git a/crates/lance-graph-mask-risc/tests/foreign.rs b/crates/lance-graph-mask-risc/tests/foreign.rs new file mode 100644 index 000000000..b3a5d302d --- /dev/null +++ b/crates/lance-graph-mask-risc/tests/foreign.rs @@ -0,0 +1,501 @@ +//! `MaskOp::Gather` / `Terminal::ScatterOrU32` / `Terminal::GroupSumI32` +//! against the row-at-a-time oracle — the same differential shape +//! `tests/differential.rs` uses, extended over a SECOND, foreign row space. + +use lance_graph_mask_risc::exec::{execute_into, Scratch}; +use lance_graph_mask_risc::reference::{reference_execute_into, reference_scratch_with_foreign}; +use lance_graph_mask_risc::{ + scratch_words_for, words_for, ExecError, Foreign, ForeignPlane, LaneRef, MaskOp, Operand, Out, + Planes, Pred, Program, Terminal, Value, +}; + +fn lcg(seed: &mut u64) -> u64 { + *seed = seed + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + *seed >> 11 +} + +const S0: Operand = Operand::Scratch(0); +const S1: Operand = Operand::Scratch(1); + +/// A primary ("line") table of `n` rows and a foreign ("partner") table of +/// `foreign_rows` rows, deliberately SHORTER than `n` so `Gather`'s +/// out-of-range zero-fallback is real, not vacuous. +struct Fixture { + foreign_rows: usize, + /// The fk lane: values `0..(foreign_rows + slack)`, so some indices land + /// past `foreign_rows` and are genuinely out of range. + fk: Vec, + /// A `u32` value lane in `0..64`, used as a status-shaped predicate. + status: Vec, + /// A signed value lane, including negatives, for the sum/group cases. + amount: Vec, + /// A `u32` key lane for `GroupSumI32`, values `0..(groups + slack)` so + /// some keys are dropped (>= the group universe). + key: Vec, + /// The foreign table's own validity — a random-density mask. + foreign_bits: Vec, +} + +impl Fixture { + fn new(n: usize, foreign_rows: usize, groups: u32, seed: u64) -> Self { + let mut s = seed ^ (n as u64) ^ (foreign_rows as u64).wrapping_shl(17); + let slack = 5u32; + let fk_range = foreign_rows as u32 + slack; + let mut fk: Vec = (0..n) + .map(|_| { + if fk_range == 0 { + 0 + } else { + (lcg(&mut s) % u64::from(fk_range)) as u32 + } + }) + .collect(); + // Plant a deterministic out-of-range fk on the LAST row (mirroring + // `tests/differential.rs`'s own "plant one hit" convention): the + // random draw above makes an out-of-range hit merely LIKELY for a + // small `n`, which is not the same as guaranteed, and the + // can-it-fire falsifier below needs it guaranteed. + if let Some(last) = fk.last_mut() { + *last = foreign_rows as u32 + slack - 1; + } + let status: Vec = (0..n).map(|_| (lcg(&mut s) % 3) as u32).collect(); + let amount: Vec = (0..n).map(|_| (lcg(&mut s) % 4000) as i32 - 2000).collect(); + let key_range = groups + slack; + let key: Vec = (0..n) + .map(|_| { + if key_range == 0 { + 0 + } else { + (lcg(&mut s) % u64::from(key_range)) as u32 + } + }) + .collect(); + let fwords = words_for(foreign_rows); + let mut foreign_bits: Vec = (0..fwords) + .map(|_| { + let mut w = lcg(&mut s); + w &= lcg(&mut s); + w + }) + .collect(); + if foreign_rows > 0 && !foreign_rows.is_multiple_of(64) { + let last = fwords - 1; + foreign_bits[last] &= (1u64 << (foreign_rows % 64)) - 1; + } + Self { + foreign_rows, + fk, + status, + amount, + key, + foreign_bits, + } + } + + fn planes(&self) -> (Vec>, Vec<&[u64]>) { + let lanes = vec![ + LaneRef::U32(&self.fk), + LaneRef::U32(&self.status), + LaneRef::I32(&self.amount), + LaneRef::U32(&self.key), + ]; + (lanes, vec![]) + } + + fn foreign_plane(&self) -> ForeignPlane<'_> { + ForeignPlane { + words: &self.foreign_bits, + rows: self.foreign_rows, + } + } +} + +const ROWS: [usize; 5] = [1, 63, 64, 130, 1000]; + +/// FAILS IF: `Gather` disagrees with the oracle anywhere — including a +/// foreign table shorter than `n_rows`, fk indices past `foreign_rows` +/// (zero-fallback), and a non-word-multiple `n`. Also checks scratch +/// contents via `reference_scratch_with_foreign` so a wrong WORD hidden by +/// `Keep`'s own bit is still caught, and asserts the fixture is neither +/// all-hit nor all-miss (a vacuous gather would pass a same-value oracle +/// trivially). +#[test] +fn gather_matches_the_oracle_including_out_of_range_and_a_tail() { + for &n in &ROWS { + for &foreign_rows in &[5usize, 64, 70, n.saturating_sub(1).max(1)] { + let fx = Fixture::new(n, foreign_rows, 8, 0xA11CE); + let (lanes, masks) = fx.planes(); + let planes = Planes { + n_rows: n, + masks: &masks, + lanes: &lanes, + }; + let fp = fx.foreign_plane(); + let foreign = Foreign { + planes: std::slice::from_ref(&fp), + }; + let p = Program::new( + vec![MaskOp::Gather { + lane: 0, + foreign: 0, + dst: 0, + }], + Terminal::Keep { mask: S0 }, + ); + + let words = words_for(n); + let slots = p.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let got = execute_into(&p, &planes, &foreign, &mut scratch, Out::None).expect("runs"); + let want = + reference_execute_into(&p, &planes, &foreign, Out::None).expect("oracle runs"); + assert_eq!(got, want, "n={n} foreign_rows={foreign_rows}: value"); + + let got_slots = scratch.slot(0).expect("written").to_vec(); + let want_slots = reference_scratch_with_foreign(&p, &planes, &foreign) + .expect("oracle scratch") + .remove(0); + assert_eq!( + got_slots, want_slots, + "n={n} foreign_rows={foreign_rows}: scratch word" + ); + + // Non-vacuous: at least one fk value must be IN range (a real + // hit is reachable) and, since fk's range always exceeds + // `foreign_rows` by 5, at least one row's fk is genuinely + // out-of-range for n >= foreign_rows (checked structurally, + // not by popcount, since the foreign bits are random and a + // hit can legitimately be zero-heavy). + let any_out_of_range = fx.fk.iter().any(|&i| i as usize >= foreign_rows); + assert!( + any_out_of_range, + "n={n} foreign_rows={foreign_rows}: fixture never exercises the out-of-range fallback" + ); + } + } +} + +/// FAILS IF: `ScatterOrU32` disagrees with the oracle for `out_rows` both +/// SMALLER and LARGER than `n_rows`, or fails to union repeated targets. +/// Non-vacuous: asserts more than one source row scatters to the SAME +/// target bit at least once across the sweep (the union case), and that the +/// output is neither all-zero nor all-one. +#[test] +fn scatter_or_matches_the_oracle_smaller_and_larger_out_rows_with_repeats() { + let mut saw_a_union = false; + for &n in &ROWS { + for &out_rows in &[3u32, 40, 2000] { + let fx = Fixture::new(n, 10, 8, 0xBEEF ^ (out_rows as u64)); + let (lanes, masks) = fx.planes(); + let planes = Planes { + n_rows: n, + masks: &masks, + lanes: &lanes, + }; + // `status != 2` over the u32 `status` lane (index 1) selects + // roughly 2/3 of rows — non-trivial, and never all of them. + let p = Program::new( + vec![MaskOp::Pred { + pred: Pred::NeU32 { lane: 1, v: 2 }, + under: None, + dst: 0, + }], + Terminal::ScatterOrU32 { + mask: S0, + lane: 0, + out_rows, + }, + ); + let words = words_for(n); + let slots = p.scratch_slots as usize; + let mut exec_buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut exec_buf, words, slots).expect("carves"); + let out_words = words_for(out_rows as usize); + let mut got_out = vec![0u64; out_words]; + let got = execute_into( + &p, + &planes, + &Foreign::NONE, + &mut scratch, + Out::Mask(&mut got_out), + ) + .expect("runs"); + let mut want_out = vec![0u64; out_words]; + let want = + reference_execute_into(&p, &planes, &Foreign::NONE, Out::Mask(&mut want_out)) + .expect("oracle runs"); + assert_eq!(got, want, "n={n} out_rows={out_rows}: value"); + assert_eq!(got_out, want_out, "n={n} out_rows={out_rows}: out buffer"); + assert!(matches!(got, Value::Scattered)); + + // Detect a union: more selected rows than set bits in `got_out` + // means at least two rows scattered to the same target. + let selected = fx + .status + .iter() + .zip(fx.fk.iter()) + .filter(|(&st, _)| st != 2) + .count(); + let set_bits: u32 = got_out.iter().map(|w| w.count_ones()).sum(); + if selected > set_bits as usize { + saw_a_union = true; + } + assert!( + set_bits == 0 || set_bits <= out_rows, + "n={n} out_rows={out_rows}: a scattered bit landed past out_rows" + ); + } + } + assert!( + saw_a_union, + "the sweep never exercised the union-of-repeats case at all" + ); +} + +/// FAILS IF: `GroupSumI32` disagrees with the oracle when `K` is smaller +/// than the key range (some rows are DROPPED) or the value lane carries +/// negatives. Non-vacuous: asserts at least one row's key is dropped, at +/// least one group is non-empty, and not every group sum is identical. +#[test] +fn group_sum_matches_the_oracle_with_dropped_keys_and_negatives() { + let groups = 4u32; + let mut any_dropped_anywhere = false; + for &n in &ROWS { + let fx = Fixture::new(n, 10, groups, 0x600D ^ n as u64); + let (lanes, masks) = fx.planes(); + let planes = Planes { + n_rows: n, + masks: &masks, + lanes: &lanes, + }; + let p = Program::new( + vec![MaskOp::Pred { + pred: Pred::NeU32 { lane: 1, v: 2 }, + under: None, + dst: 0, + }], + Terminal::GroupSumI32 { + mask: S0, + key: 3, + val: 2, + }, + ); + let words = words_for(n); + let slots = p.scratch_slots as usize; + let mut exec_buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut exec_buf, words, slots).expect("carves"); + let mut got_out = vec![0i64; groups as usize]; + let got = execute_into( + &p, + &planes, + &Foreign::NONE, + &mut scratch, + Out::I64(&mut got_out), + ) + .expect("runs"); + let mut want_out = vec![0i64; groups as usize]; + let want = reference_execute_into(&p, &planes, &Foreign::NONE, Out::I64(&mut want_out)) + .expect("oracle runs"); + assert_eq!(got, want, "n={n}: value"); + assert_eq!(got_out, want_out, "n={n}: out buffer"); + assert!(matches!(got, Value::GroupSummed)); + + let dropped = fx + .status + .iter() + .zip(fx.key.iter()) + .filter(|(&st, &k)| st != 2 && k >= groups) + .count(); + if dropped > 0 { + any_dropped_anywhere = true; + } + let distinct: std::collections::HashSet = got_out.iter().copied().collect(); + assert!( + n < 4 || distinct.len() > 1, + "n={n}: every group summed to the same value — not a real fixture" + ); + } + assert!(any_dropped_anywhere, "the sweep never dropped a key at all"); +} + +/// FAILS IF: any of the four named validation refusals is not reported +/// IDENTICALLY by the executor and the oracle — same [`ExecError`] variant +/// and payload, not merely "both are `Err`". +#[test] +fn validation_refusals_match_between_executor_and_oracle() { + let fx = Fixture::new(200, 10, 4, 0xDEAD); + let (lanes, masks) = fx.planes(); + let planes = Planes { + n_rows: 200, + masks: &masks, + lanes: &lanes, + }; + let fp = fx.foreign_plane(); + let foreign = Foreign { + planes: std::slice::from_ref(&fp), + }; + + let assert_same_refusal = |label: &str, + p: &Program, + foreign: &Foreign<'_>, + out: Out<'_>, + out2: Out<'_>, + want: ExecError| { + let words = words_for(planes.n_rows); + let slots = p.scratch_slots.max(1) as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let got = execute_into(p, &planes, foreign, &mut scratch, out); + assert_eq!(got, Err(want), "{label}: executor"); + let oracle = reference_execute_into(p, &planes, foreign, out2); + assert_eq!(oracle, Err(want), "{label}: oracle"); + }; + + // (a) foreign index out of range. + let p_bad_foreign = Program::new( + vec![MaskOp::Gather { + lane: 0, + foreign: 3, + dst: 0, + }], + Terminal::Keep { mask: S0 }, + ); + assert_same_refusal( + "foreign out of range", + &p_bad_foreign, + &foreign, + Out::None, + Out::None, + ExecError::ForeignOutOfRange(3), + ); + + // (b) wrong lane kind — `Gather`'s fk must be U32; lane 2 is I32. + let p_wrong_kind = Program::new( + vec![MaskOp::Gather { + lane: 2, + foreign: 0, + dst: 0, + }], + Terminal::Keep { mask: S0 }, + ); + assert_same_refusal( + "wrong lane kind", + &p_wrong_kind, + &foreign, + Out::None, + Out::None, + ExecError::LaneKind { + lane: 2, + expected: lance_graph_mask_risc::LaneKind::U32, + found: lance_graph_mask_risc::LaneKind::I32, + }, + ); + + // (c) wrong Out kind for the terminal that IS present. + let p_group = Program::new( + vec![MaskOp::Pred { + pred: Pred::NeU32 { lane: 1, v: 99 }, + under: None, + dst: 0, + }], + Terminal::GroupSumI32 { + mask: S0, + key: 3, + val: 2, + }, + ); + let mut wrong = [0i32; 1]; + let mut wrong2 = [0i32; 1]; + assert_same_refusal( + "wrong Out kind", + &p_group, + &Foreign::NONE, + Out::I32(&mut wrong), + Out::I32(&mut wrong2), + ExecError::TerminalNeedsOut { + what: "GroupSumI32", + }, + ); + + // (d) out length mismatch — `ScatterOrU32` wants `words_for(out_rows)`. + let p_scatter = Program::new( + vec![MaskOp::Pred { + pred: Pred::NeU32 { lane: 1, v: 99 }, + under: None, + dst: 0, + }], + Terminal::ScatterOrU32 { + mask: S0, + lane: 0, + out_rows: 500, + }, + ); + let mut too_short = [0u64; 1]; // words_for(500) == 8 + let mut too_short2 = [0u64; 1]; + assert_same_refusal( + "out length mismatch", + &p_scatter, + &Foreign::NONE, + Out::Mask(&mut too_short), + Out::Mask(&mut too_short2), + ExecError::LenMismatch { + what: "out", + expected: 8, + found: 1, + }, + ); +} + +/// FAILS IF: an `AND` between a `Gather` leaf and a plain predicate — the +/// composed-semijoin shape a consumer builds by hand until this IR gets an +/// `under` for `Gather` — disagrees with the oracle. Exercises the "no +/// `under` variant (compose with `And`)" doc claim directly. +#[test] +fn gather_composes_with_and_like_any_other_leaf() { + let fx = Fixture::new(300, 40, 4, 0xC0FFEE); + let (lanes, masks) = fx.planes(); + let planes = Planes { + n_rows: 300, + masks: &masks, + lanes: &lanes, + }; + let fp = fx.foreign_plane(); + let foreign = Foreign { + planes: std::slice::from_ref(&fp), + }; + let p = Program::new( + vec![ + MaskOp::Pred { + pred: Pred::NeU32 { lane: 1, v: 2 }, + under: None, + dst: 0, + }, + MaskOp::Gather { + lane: 0, + foreign: 0, + dst: 1, + }, + MaskOp::And { + a: S0, + b: S1, + dst: 0, + }, + ], + Terminal::Count { mask: S0 }, + ); + let words = words_for(300); + let slots = p.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let got = execute_into(&p, &planes, &foreign, &mut scratch, Out::None).expect("runs"); + let want = reference_execute_into(&p, &planes, &foreign, Out::None).expect("oracle runs"); + assert_eq!(got, want); + if let Value::Count(c) = got { + assert!(c > 0 && c < 300, "the composed semijoin is not selective"); + } else { + panic!("expected a count, got {got:?}"); + } +} diff --git a/crates/lance-graph-quack/src/lib.rs b/crates/lance-graph-quack/src/lib.rs index c72b9cfb0..5ad6de6e1 100644 --- a/crates/lance-graph-quack/src/lib.rs +++ b/crates/lance-graph-quack/src/lib.rs @@ -177,6 +177,15 @@ pub struct Col(pub u16); #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Mask(pub u16); +/// A FOREIGN table's kept mask — an index into +/// [`Foreign::planes`](lance_graph_mask_risc::Foreign), never +/// [`Planes::masks`](lance_graph_mask_risc::Planes). A row-space belonging to +/// another table, addressed through [`Filter::semijoin`] rather than read as +/// an ordinary operand — the same distinction [`lance_graph_mask_risc::ir`] +/// draws between [`Mask`] (this table) and this. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ForeignMask(pub u16); + /// A predicate over one column — the filter's whole vocabulary. /// /// One variant per masking op that produces a mask from a value lane. That is @@ -288,6 +297,21 @@ pub enum Filter { /// with no `WHERE`: the table is its validity plane, and a query over /// every row of it lowers to zero ops. Plane(Mask), + /// The fk SEMIJOIN — `EXISTS(SELECT 1 FROM foreign f WHERE f.rid = + /// this.fk AND )`, lowered to [`MaskOp::Gather`] over the + /// caller-supplied [`Foreign`](lance_graph_mask_risc::Foreign). A LEAF, + /// like [`Filter::Cmp`] / [`Filter::Plane`]: it composes with + /// `And`/`Or`/`Not` normally, and both [`lower`] and [`lower_fused`] + /// treat it as opaque — the fuser never looks inside it, it only sees + /// the scratch slot its `Gather` wrote (or, when `lower_fused` gives it + /// its own slot, `AND`ed with a resident gate as a `BoolExpr::And`, + /// exactly what happens to a gated `Cmp`). + Semijoin { + /// The foreign-key column on THIS table. + fk: Col, + /// Which foreign table's kept mask the fk resolves into. + foreign: ForeignMask, + }, /// Every child must hold. And(Vec), /// Some child must hold. @@ -307,6 +331,11 @@ impl Filter { Filter::Plane(mask) } + /// The fk semijoin leaf — see [`Filter::Semijoin`]. + pub fn semijoin(fk: Col, foreign: ForeignMask) -> Self { + Filter::Semijoin { fk, foreign } + } + /// Conjunction. pub fn and(parts: impl IntoIterator) -> Self { Filter::And(parts.into_iter().collect()) @@ -730,6 +759,29 @@ pub enum Agg { /// The lane read where it does not. els: Col, }, + /// The one-to-many hop back: for every surviving row, sets bit `fk[row]` + /// of a caller-supplied `Out::Mask` buffer of `words_for(out_rows)` — + /// `Terminal::ScatterOrU32`. `"docs that have a posted line"` is + /// `Agg::ScatterOrU32 { fk: doc_id, out_rows: DOC_ROWS }` over a + /// `status == 1` filter. + ScatterOrU32 { + /// The foreign-key column naming the target row on the OTHER table. + fk: Col, + /// The target table's row count — the caller's `Out::Mask` buffer + /// must be exactly `words_for(out_rows)` long. + out_rows: u32, + }, + /// The one-terminal `GROUP BY key SUM(val)` — `Terminal::GroupSumI32`, + /// ONE program instead of `lower_group_by`'s K. The caller supplies an + /// `Out::I64` buffer whose length IS the group universe; a `key` at or + /// past that length is dropped, not an error (the same zero-fallback + /// `Agg::ScatterOrU32`'s out-of-range fk gets). + GroupSumI32 { + /// The group key column (`u32`). + key: Col, + /// The value column summed per group. + val: Col, + }, } /// One query: a filter and what to ask of the rows that pass it. @@ -871,6 +923,15 @@ enum Node { }, /// A resident plane read as an operand. Plane(Mask), + /// The fk semijoin leaf ([`Filter::Semijoin`]), gated by `under` when + /// there is one — same shape as `Pred`, except the gate cannot be baked + /// into `MaskOp::Gather` itself (it has no `under` field), so the two + /// emitters apply it as an explicit `AND` after the `Gather` write. + Gather { + fk: u16, + foreign: u16, + under: Option, + }, And(Vec), Or(Vec), Not(Box), @@ -923,6 +984,19 @@ fn gate_walk(f: &Filter, gate: Option) -> Result<(Node, bool), LowerError> gate.is_some(), ), Filter::Plane(m) => (Node::Plane(*m), gate == Some(*m)), + // Same "vanishes with the gate" reasoning as `Cmp` above: both + // emitters apply `under` as an explicit `AND` on the `Gather` + // result, so the emitted value is literally `gather & gate` + // whenever `under` is `Some` — a real subset of the gate, not a + // conservative guess. + Filter::Semijoin { fk, foreign } => ( + Node::Gather { + fk: fk.0, + foreign: foreign.0, + under: gate, + }, + gate.is_some(), + ), // Gating passes THROUGH a negation (the rewrite is sound for any // remainder while the gate stays a leaf), but `!x` is 1 where the // gate is 0, so a negation never lets the gate be dropped. @@ -1112,6 +1186,26 @@ fn emit_gated( Ok(Operand::Scratch(dst)) } Node::Plane(m) => Ok(Operand::Plane(m.0)), + Node::Gather { fk, foreign, under } => { + ops.push(MaskOp::Gather { + lane: *fk, + foreign: *foreign, + dst, + }); + // Same choice as `Pred` above: the plane gate `under` wins over + // the accumulator when both are available — `Gather` has no + // `under` field of its own, so this is an explicit `AND` rather + // than a baked-in gate, but the precedence rule is identical. + let gate = under.map(|m| Operand::Plane(m.0)).or(acc_gate); + if let Some(g) = gate { + ops.push(MaskOp::And { + a: Operand::Scratch(dst), + b: g, + dst, + }); + } + Ok(Operand::Scratch(dst)) + } Node::Not(inner) => { let a = emit_gated(inner, dst, acc_gate, ops)?; ops.push(MaskOp::Not { a, dst }); @@ -1191,6 +1285,29 @@ fn assign_slots(n: &Node, ops: &mut Vec) -> Result Ok(BoolExpr::Leaf(Operand::Scratch(dst))) } Node::Plane(m) => Ok(BoolExpr::Leaf(Operand::Plane(m.0))), + Node::Gather { fk, foreign, under } => { + let dst = u16::try_from(ops.len()).map_err(|_| LowerError::TooManySlots { + needed: ops.len() + 1, + })?; + ops.push(MaskOp::Gather { + lane: *fk, + foreign: *foreign, + dst, + }); + let leaf = BoolExpr::Leaf(Operand::Scratch(dst)); + // `Gather` has no `under` field, so the gate — unlike a `Pred`'s + // — cannot be baked into the op. Folding it into the BOOLEAN + // EXPRESSION instead lets the fuser absorb the `AND` into a + // ternlog along with everything else, rather than paying a + // separate mask pass the way `emit_gated`'s in-place form does. + Ok(match under { + Some(m) => BoolExpr::And( + Box::new(leaf), + Box::new(BoolExpr::Leaf(Operand::Plane(m.0))), + ), + None => leaf, + }) + } Node::Not(inner) => Ok(BoolExpr::Not(Box::new(assign_slots(inner, ops)?))), Node::And(parts) | Node::Or(parts) => { let is_and = matches!(n, Node::And(_)); @@ -1228,6 +1345,16 @@ fn terminal_of(agg: Agg, mask: Operand) -> Terminal { then: then.0, els: els.0, }, + Agg::ScatterOrU32 { fk, out_rows } => Terminal::ScatterOrU32 { + mask, + lane: fk.0, + out_rows, + }, + Agg::GroupSumI32 { key, val } => Terminal::GroupSumI32 { + mask, + key: key.0, + val: val.0, + }, } } @@ -1287,6 +1414,14 @@ mod tests { /// the range claim would be untestable. addr: Vec, masks: Vec>, + /// Foreign tables' kept masks, indexed by `ForeignMask.0` — empty for + /// every single-table fixture; populated only by the two-table + /// semijoin tests. + foreign: Vec>, + /// The EXACT row count each `foreign[i]` spans — needed because a + /// word-rounded length alone cannot distinguish "row 63 of a 64-row + /// table" from "the dirty tail of a 60-row one". + foreign_rows: Vec, } fn plane(n: usize, set: impl Fn(usize) -> bool) -> Vec { @@ -1312,9 +1447,20 @@ mod tests { alt, addr, masks, + foreign: Vec::new(), + foreign_rows: Vec::new(), } } + /// Register a foreign table's kept mask at the next `ForeignMask` + /// index (`0`, `1`, …, in call order) and return that index. + fn push_foreign(&mut self, bits: Vec, rows: usize) -> ForeignMask { + let idx = self.foreign.len() as u16; + self.foreign.push(bits); + self.foreign_rows.push(rows); + ForeignMask(idx) + } + fn n(&self) -> usize { self.vals.len() } @@ -1370,6 +1516,12 @@ mod tests { Cmp::Range { lo, hi } => (lo as usize) <= row && row < (hi as usize), }, Filter::Plane(m) => self.bit(*m, row), + Filter::Semijoin { fk, foreign } => { + let idx = self.u32_at(*fk, row) as usize; + let bits = &self.foreign[usize::from(foreign.0)]; + let rows = self.foreign_rows[usize::from(foreign.0)]; + idx < rows && (bits[idx / 64] >> (idx % 64)) & 1 == 1 + } Filter::And(ps) => ps.iter().all(|p| self.oracle(p, row)), Filter::Or(ps) => ps.iter().any(|p| self.oracle(p, row)), Filter::Not(p) => !self.oracle(p, row), @@ -1694,6 +1846,11 @@ mod tests { MaskOp::Ternlog { a, b, c, .. } => { read_as_leaf |= a == gate || b == gate || c == gate; } + // `Gather` names no `Operand` among its own fields (`lane` + // and `foreign` are plain indices, not `a`/`b`/`c`), so it + // can never read `gate` as a Boolean operand the way the + // other ops can. + MaskOp::Gather { .. } => {} } } let terminal_reads_gate = matches!( @@ -1705,6 +1862,8 @@ mod tests { | Terminal::MaskedMinI32 { mask, .. } | Terminal::MaskedMaxI32 { mask, .. } | Terminal::BlendI32 { mask, .. } + | Terminal::ScatterOrU32 { mask, .. } + | Terminal::GroupSumI32 { mask, .. } | Terminal::Keep { mask } if mask == gate ); ( @@ -2408,6 +2567,174 @@ mod tests { assert_eq!(fx.reference(&inplace, None), Value::Count(expected)); assert_eq!(fx.reference(&fused, None), Value::Count(expected)); } + + /// FAILS IF: `Filter::Semijoin` disagrees with an independent per-row + /// reading (the oracle's new `Semijoin` arm), in EITHER lowering — a + /// two-table shape: `CLASS` is the fk into a 5-row foreign table whose + /// kept mask selects classes `{1, 3}`. Anti-vacuity: the semijoin alone + /// is selective (neither 0 nor every row), and composing it with `AND` + /// narrows further still. + #[test] + fn semijoin_matches_an_independent_per_row_reading() { + let mut fx = Fx::new(N); + let foreign_rows = 5usize; + let foreign_bits = plane(foreign_rows, |r| r == 1 || r == 3); + let foreign = fx.push_foreign(foreign_bits.clone(), foreign_rows); + + let bare = Filter::semijoin(CLASS, foreign); + let bare_expected = fx.rows(&bare).len(); + assert!( + bare_expected > 0 && bare_expected < N, + "the bare semijoin selects {bare_expected}/{N} — not selective" + ); + + let f = Filter::and([Filter::cmp(VALS, Cmp::GtI32(0)), bare.clone()]); + let expected: i64 = (0..fx.n()) + .filter(|&r| fx.oracle(&f, r)) + .map(|r| i64::from(fx.alt[r])) + .sum(); + let narrowed = fx.rows(&f).len(); + assert!( + narrowed > 0 && narrowed < bare_expected, + "composing with AND must narrow: {narrowed} vs {bare_expected}" + ); + + let q = Query { + filter: f, + agg: Agg::SumI32(ALT), + }; + let mask_risc_foreign = lance_graph_mask_risc::Foreign { + planes: &[lance_graph_mask_risc::ForeignPlane { + words: &foreign_bits, + rows: foreign_rows, + }], + }; + for (label, program) in [ + ("in place", lower(&q).expect("lowers in place")), + ("fused", lower_fused(&q).expect("lowers fused")), + ] { + let words = words_for(fx.n()); + let slots = program.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let value = fx.with_planes(&[], |planes| { + lance_graph_mask_risc::execute_into( + &program, + planes, + &mask_risc_foreign, + &mut scratch, + lance_graph_mask_risc::Out::None, + ) + .expect("runs") + }); + assert_eq!(value, Value::SumI64(expected), "{label}: value"); + } + } + + /// FAILS IF: `Agg::ScatterOrU32` disagrees with an independent per-row + /// reading — CLASS is the fk, out_rows = 5 (the same "foreign" row + /// space, addressed as a plain OWNED `Out::Mask` this time, not a + /// foreign read). Anti-vacuity: the scattered mask is neither empty nor + /// full, and more rows survive the filter than bits end up set (a real + /// union of repeats — CLASS only takes 5 values over 1000 rows). + #[test] + fn scatter_or_u32_matches_an_independent_per_row_reading() { + let fx = Fx::new(N); + let out_rows = 5u32; + let f = Filter::cmp(VALS, Cmp::GtI32(0)); + let selected = fx.rows(&f); + assert!(!selected.is_empty() && selected.len() < N); + + let mut want = vec![0u64; words_for(out_rows as usize)]; + for &r in &selected { + let idx = fx.classes[r] as usize; + if idx < out_rows as usize { + want[idx / 64] |= 1u64 << (idx % 64); + } + } + let want_bits: u32 = want.iter().map(|w| w.count_ones()).sum(); + assert!( + want_bits > 0 && (selected.len() as u32) > want_bits, + "expected a real union of repeats: {} rows -> {want_bits} bits", + selected.len() + ); + + let q = Query { + filter: f, + agg: Agg::ScatterOrU32 { + fk: CLASS, + out_rows, + }, + }; + let program = lower(&q).expect("lowers"); + let words = words_for(fx.n()); + let slots = program.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut got = vec![0u64; words_for(out_rows as usize)]; + let value = fx.with_planes(&[], |planes| { + lance_graph_mask_risc::execute_into( + &program, + planes, + &lance_graph_mask_risc::Foreign::NONE, + &mut scratch, + lance_graph_mask_risc::Out::Mask(&mut got), + ) + .expect("runs") + }); + assert_eq!(value, Value::Scattered); + assert_eq!(got, want); + } + + /// FAILS IF: `Agg::GroupSumI32` disagrees with an independent per-row + /// reading. `groups = 3` while `CLASS` ranges `0..5`, so classes 3 and 4 + /// are DROPPED — exercised deliberately, not incidentally. + #[test] + fn group_sum_i32_matches_an_independent_per_row_reading() { + let fx = Fx::new(N); + let groups = 3usize; + let f = Filter::cmp(VALS, Cmp::GtI32(-50)); + let selected = fx.rows(&f); + assert!(!selected.is_empty() && selected.len() < N); + let dropped = selected.iter().any(|&r| fx.classes[r] as usize >= groups); + assert!(dropped, "the fixture never drops a key at all"); + + let mut want = vec![0i64; groups]; + for &r in &selected { + let k = fx.classes[r] as usize; + if k < groups { + want[k] = want[k].wrapping_add(i64::from(fx.alt[r])); + } + } + let distinct: std::collections::HashSet = want.iter().copied().collect(); + assert!(distinct.len() > 1, "every group summed to the same value"); + + let q = Query { + filter: f, + agg: Agg::GroupSumI32 { + key: CLASS, + val: ALT, + }, + }; + let program = lower(&q).expect("lowers"); + let words = words_for(fx.n()); + let slots = program.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut got = vec![0i64; groups]; + let value = fx.with_planes(&[], |planes| { + lance_graph_mask_risc::execute_into( + &program, + planes, + &lance_graph_mask_risc::Foreign::NONE, + &mut scratch, + lance_graph_mask_risc::Out::I64(&mut got), + ) + .expect("runs") + }); + assert_eq!(value, Value::GroupSummed); + assert_eq!(got, want); + } } /// D-DIAMOND-1 R2 — the witnessed prefix lowering, differential against the diff --git a/crates/lance-graph-quack/tests/duckdb/fixture.rs b/crates/lance-graph-quack/tests/duckdb/fixture.rs index 7a7251877..257b68394 100644 --- a/crates/lance-graph-quack/tests/duckdb/fixture.rs +++ b/crates/lance-graph-quack/tests/duckdb/fixture.rs @@ -89,6 +89,13 @@ pub struct DocTable { /// needs an order, so they stay `u32`, matching the schema's own typing. pub struct LineTable { pub doc_id: Vec, + /// `doc_id`, again, but `u32` — the fk shape [`MaskOp::Gather`] / + /// [`Terminal::ScatterOrU32`] need (their lane must be + /// [`LaneKind::U32`](lance_graph_mask_risc::LaneKind::U32); `doc_id` + /// stays `i32` above for `range_docid`'s ordered compare). Same values, + /// cast at generation time — the fixture's `doc_id` domain is always + /// `0..DOC_ROWS`, so the cast loses nothing. + pub doc_id_u32: Vec, pub partner_id: Vec, pub amount: Vec, pub qty: Vec, @@ -151,8 +158,13 @@ pub fn generate() -> Fixture { cost_center.push(next_u32(&mut state, 8)); gl_account.push(next_gl_account(&mut state)); } + // Derived, not drawn: no RNG state is consumed here, so this can never + // shift any later draw or desync the committed CSVs / DuckDB `expected` + // values from the generator. + let doc_id_u32: Vec = doc_id.iter().map(|&d| d as u32).collect(); let line = LineTable { doc_id, + doc_id_u32, partner_id, amount, qty, @@ -235,10 +247,11 @@ impl Fixture { } /// The `line` table's lanes, in the fixed order the `Col` constants below -/// index into. Only `line` is exposed as [`Planes`] — `partner` and `doc` -/// are never the target of a lowered [`lance_graph_quack::Query`] in this -/// suite (Quack has no join lowering; see the `join_*_is_the_open_seam` -/// tests), so they exist only as CSV rows for the oracle. +/// index into. `line` is the table every lowered [`lance_graph_quack::Query`] +/// runs against; `partner` is now ALSO exposed as [`Planes`] (see +/// [`col::partner`]) so the two join cases can lower a real partner-side +/// filter to a real `Program` — `doc` still exists only as CSV rows for the +/// oracle, since no case in this suite filters it directly. pub mod col { use lance_graph_quack::Col; @@ -248,6 +261,24 @@ pub mod col { pub const STATUS: Col = Col(3); pub const COST_CENTER: Col = Col(4); pub const GL_ACCOUNT: Col = Col(5); + /// `line.partner_id`, `u32` — the fk into `partner` (`p.rid`), the + /// lane [`lance_graph_mask_risc::MaskOp::Gather`] needs. + pub const PARTNER_ID: Col = Col(6); + /// `line.doc_id`, `u32` — see [`super::LineTable::doc_id_u32`]. The + /// same value as [`DOC_ID`], cast; `DOC_ID` stays for `range_docid`'s + /// ordered compare. + pub const DOC_ID_U32: Col = Col(7); + + /// `partner` table columns, over `partner`'s OWN row space + /// ([`super::PARTNER_ROWS`]) — a separate `Planes` from `line`'s. + /// `pgroup` (lane 1, see [`super::PartnerTable::lanes`]) has no named + /// constant here — no case in this suite filters on it — but the lane + /// itself stays borrowed so a future case can reach it as `Col(1)`. + pub mod partner { + use lance_graph_quack::Col; + + pub const COUNTRY: Col = Col(0); + } } /// The `line` table's lanes, borrowed and held so [`Planes`] can borrow them @@ -256,7 +287,7 @@ pub mod col { /// return, so this struct is the thing that DOES outlive it, owned by the /// caller for exactly as long as the [`Planes`] it lends out. pub struct LineLanes<'a> { - lanes: [LaneRef<'a>; 6], + lanes: [LaneRef<'a>; 8], } impl LineTable { @@ -270,11 +301,38 @@ impl LineTable { LaneRef::U32(&self.status), LaneRef::U32(&self.cost_center), LaneRef::U32(&self.gl_account), + LaneRef::U32(&self.partner_id), + LaneRef::U32(&self.doc_id_u32), ], } } } +/// The `partner` table's lanes — see [`col::partner`]. +pub struct PartnerLanes<'a> { + lanes: [LaneRef<'a>; 2], +} + +impl PartnerTable { + /// Borrow every lane in the fixed order `col::partner` indexes into. + pub fn lanes(&self) -> PartnerLanes<'_> { + PartnerLanes { + lanes: [LaneRef::U32(&self.country), LaneRef::U32(&self.pgroup)], + } + } +} + +impl<'a> PartnerLanes<'a> { + /// Borrow `PARTNER_ROWS` rows over these lanes, no resident mask planes. + pub fn planes(&self) -> Planes<'_> { + Planes { + n_rows: PARTNER_ROWS, + masks: &[], + lanes: &self.lanes, + } + } +} + impl<'a> LineLanes<'a> { /// Borrow `LINE_ROWS` rows over these lanes, no resident mask planes — /// every case in this suite is a plain `WHERE`, never a plane leaf. diff --git a/crates/lance-graph-quack/tests/duckdb_differential.rs b/crates/lance-graph-quack/tests/duckdb_differential.rs index 6c3c0c4ba..43abab3c9 100644 --- a/crates/lance-graph-quack/tests/duckdb_differential.rs +++ b/crates/lance-graph-quack/tests/duckdb_differential.rs @@ -7,12 +7,19 @@ //! encoded the same way, and compared. **Expected values are never //! hand-edited** — see `tests/duckdb/README.txt`. //! -//! `join_*_is_the_open_seam` tests are the honest exception: Quack has no -//! join lowering (`src/lib.rs`'s own status section names it — `src_mask → -//! hop → dst_mask` is `lance-graph-mask-risc`'s PR5 and there is no `hop` op -//! to lower to yet). Those three still load their oracle-computed `expected` -//! value — proving the oracle side is real and committed — then `todo!()`. -//! They are `#[should_panic]` by design, not disabled and not deleted. +//! `join_group_sum_country_is_the_open_seam` is the one honest exception +//! left: it needs a group-sum key resolved through a FOREIGN table +//! (`SUM(l.amount) GROUP BY p.country`), which waits on +//! `ndarray::simd::masked_group_sum_i32_via` — `Terminal::GroupSumI32`'s key +//! is a lane on THIS table, not a fk-resolved one. `join_sum_country` and +//! `join_count_docs_with_posted` are no longer open seams: the former lowers +//! a real `Filter::Semijoin` (the fk gather) against a real partner-side +//! `Program`; the latter lowers `Agg::ScatterOrU32` (the one-to-many hop +//! back) into a caller-owned mask, then counts it with a second tiny +//! program. `group_sum_cc` now runs the one-terminal `Agg::GroupSumI32` +//! (`Terminal::GroupSumI32`, ONE program) alongside the pre-existing K-program +//! `lower_group_by` reading, printing both METRIC lines so the fold is +//! visible. #[path = "duckdb/fixture.rs"] mod fixture; @@ -22,11 +29,17 @@ use std::path::Path; use std::sync::atomic::{AtomicUsize, Ordering}; use lance_graph_mask_risc::{ - execute, materialize_rows, scratch_words_for, words_for, Operand, Planes, Scratch, Value, + execute, execute_into, materialize_rows, scratch_words_for, words_for, Foreign, ForeignPlane, + Operand, Out, Planes, Scratch, Value, +}; +use lance_graph_quack::{ + lower, lower_group_by, Agg, Cmp, Col, Filter, ForeignMask, GroupBy, Query, }; -use lance_graph_quack::{lower, lower_group_by, Agg, Cmp, Col, Filter, GroupBy, Query}; -use fixture::col::{AMOUNT, COST_CENTER, DOC_ID, GL_ACCOUNT, QTY, STATUS}; +use fixture::col::{ + partner::COUNTRY, AMOUNT, COST_CENTER, DOC_ID, DOC_ID_U32, GL_ACCOUNT, PARTNER_ID, QTY, STATUS, +}; +use fixture::DOC_ROWS; // --------------------------------------------------------------------- // The counting allocator — `no_alloc.rs`'s pattern (lance-graph-mask-risc), @@ -139,29 +152,37 @@ struct CaseMetrics { /// are then the SUM over the phase-1 filter and all K phase-2 programs, /// per the spec's "record ops/allocs as the SUM over the K runs". programs: Option, + /// Bytes an N×M pair-relation object would have cost — always `0` here. + /// No such object is ever built: a join is a `Gather`/`ScatterOrU32` + /// read against a caller-owned foreign mask, never a materialized + /// row-pair list. Named explicitly (rather than left implicit) so the + /// absence is a printed fact, not an inference from a missing field. + pair_relation_bytes: usize, } fn print_metric(id: &str, m: &CaseMetrics) { match m.programs { Some(k) => eprintln!( "METRIC case={id} ops={} scratch_words={} alloc_bytes_exec={} \ - rows_materialized={} index_vec_len={} out_bytes={} programs={k}", + rows_materialized={} index_vec_len={} out_bytes={} pair_relation_bytes={} programs={k}", m.ops, m.scratch_words, m.alloc_bytes_exec, m.rows_materialized, m.index_vec_len, - m.out_bytes + m.out_bytes, + m.pair_relation_bytes, ), None => eprintln!( "METRIC case={id} ops={} scratch_words={} alloc_bytes_exec={} \ - rows_materialized={} index_vec_len={} out_bytes={}", + rows_materialized={} index_vec_len={} out_bytes={} pair_relation_bytes={}", m.ops, m.scratch_words, m.alloc_bytes_exec, m.rows_materialized, m.index_vec_len, - m.out_bytes + m.out_bytes, + m.pair_relation_bytes, ), } } @@ -210,6 +231,12 @@ fn run_query(id: &str, planes: &Planes<'_>, filter: Filter, agg: Agg) -> (String (s, n, n) } Value::Blended => panic!("case {id}: no BlendI32 case in this suite"), + Value::Scattered => panic!( + "case {id}: run_query doesn't handle ScatterOrU32 — see join_count_docs_with_posted" + ), + Value::GroupSummed => panic!( + "case {id}: run_query doesn't handle GroupSumI32 — see group_sum_cc's one-terminal arm" + ), }; let out_bytes = encoded.len(); ( @@ -222,6 +249,7 @@ fn run_query(id: &str, planes: &Planes<'_>, filter: Filter, agg: Agg) -> (String index_vec_len, out_bytes, programs: None, + pair_relation_bytes: 0, }, ) } @@ -305,6 +333,7 @@ fn run_group( index_vec_len: 0, out_bytes, programs: Some(plan.groups.len()), + pair_relation_bytes: 0, }, ) } @@ -555,7 +584,58 @@ fn group_sum_cc() { let lanes = fx.line.lanes(); let planes = lanes.planes(); let filter = Filter::cmp(STATUS, Cmp::EqU32(1)); - let (actual, m) = run_group( + + // The one-terminal spelling: ONE program, `Terminal::GroupSumI32`, no + // K-program loop. + let program = lower(&Query { + filter: filter.clone(), + agg: Agg::GroupSumI32 { + key: COST_CENTER, + val: AMOUNT, + }, + }) + .expect("lowers"); + let words = words_for(planes.n_rows); + let slots = program.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let scratch_words = buf.len(); + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut out = [0i64; 8]; + let before = BYTES.load(Ordering::Relaxed); + let value = execute_into( + &program, + &planes, + &Foreign::NONE, + &mut scratch, + Out::I64(&mut out), + ) + .expect("runs"); + let alloc_bytes_exec = alloc_delta(before); + assert_eq!(value, Value::GroupSummed); + let encoded = out + .iter() + .enumerate() + .map(|(k, v)| format!("{k}:{v}")) + .collect::>() + .join(";"); + let out_bytes = encoded.len(); + let m = CaseMetrics { + ops: program.ops.len(), + scratch_words, + alloc_bytes_exec, + rows_materialized: 0, + index_vec_len: 0, + out_bytes, + programs: Some(1), + pair_relation_bytes: 0, + }; + print_metric("group_sum_cc", &m); + assert_case(&cases, "group_sum_cc", &encoded); + + // The pre-existing K-program (`lower_group_by`) spelling, run and + // printed alongside so the fold from K programs to one is visible in + // the METRIC output rather than only in the source diff. + let (actual_k, m_k) = run_group( "group_sum_cc", &planes, filter, @@ -563,45 +643,196 @@ fn group_sum_cc() { 8, Agg::SumI32(AMOUNT), ); - print_metric("group_sum_cc", &m); - assert_case(&cases, "group_sum_cc", &actual); + print_metric("group_sum_cc_kprogram", &m_k); + assert_case(&cases, "group_sum_cc", &actual_k); } // --------------------------------------------------------------------- -// The 3 join cases — the open seam. Quack lowers filters and one-table -// aggregates; it has no join lowering (`src/lib.rs`'s `# Status` section -// names the gap explicitly). Each of these loads its oracle-computed -// `expected` — proving the DuckDB side is real and committed — then -// `todo!()`s, so the case is red until join lowering exists rather than -// silently absent from the suite. +// The join cases. `join_sum_country` (fk gather) and +// `join_count_docs_with_posted` (the one-to-many hop back) are real +// two-program lowerings now; `join_group_sum_country` stays the honest +// open seam — see the module doc for why. // --------------------------------------------------------------------- +/// `SELECT SUM(l.amount) FROM line l JOIN partner p ON p.rid=l.partner_id +/// WHERE l.status=1 AND p.country=3` — two programs: the partner-side +/// filter kept as a mask, handed to the line-side program as a +/// [`ForeignPlane`] its `Filter::Semijoin` gathers through. #[test] -#[should_panic(expected = "NO JOIN LOWERING")] -fn join_sum_country_is_the_open_seam() { +fn join_sum_country() { let cases = load_cases(); - let expected = expected_only(&cases, "join_sum_country"); - assert!( - !expected.is_empty(), - "join_sum_country has no oracle value — run oracle.py first" - ); - todo!("NO JOIN LOWERING: join_sum_country"); + let fx = fixture::generate(); + + // Phase 1 (partner side): `country == 3` -> Keep. + let partner_lanes = fx.partner.lanes(); + let partner_planes = partner_lanes.planes(); + let partner_program = lower(&Query { + filter: Filter::cmp(COUNTRY, Cmp::EqU32(3)), + agg: Agg::Rows, + }) + .expect("lowers"); + let p_words = words_for(partner_planes.n_rows); + let p_slots = partner_program.scratch_slots as usize; + let mut p_buf = vec![0u64; scratch_words_for(p_words, p_slots).expect("sized")]; + let p_scratch_words = p_buf.len(); + let mut p_scratch = Scratch::over(&mut p_buf, p_words, p_slots).expect("carves"); + let before1 = BYTES.load(Ordering::Relaxed); + let p_value = execute(&partner_program, &partner_planes, &mut p_scratch, None).expect("runs"); + let alloc1 = alloc_delta(before1); + let p_op = match p_value { + Value::Mask(op) => op, + other => panic!("partner-side filter did not Keep a mask: {other:?}"), + }; + let partner_bits = mask_bits(&partner_planes, &p_scratch, p_op); + + let foreign_plane = ForeignPlane { + words: &partner_bits, + rows: fixture::PARTNER_ROWS, + }; + let foreign = Foreign { + planes: &[foreign_plane], + }; + + // Phase 2 (line side): `status == 1 AND Semijoin(partner_id, 0)` -> SUM. + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let line_filter = Filter::and([ + Filter::cmp(STATUS, Cmp::EqU32(1)), + Filter::semijoin(PARTNER_ID, ForeignMask(0)), + ]); + let line_program = lower(&Query { + filter: line_filter, + agg: Agg::SumI32(AMOUNT), + }) + .expect("lowers"); + let words = words_for(planes.n_rows); + let slots = line_program.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let scratch_words = buf.len(); + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let before2 = BYTES.load(Ordering::Relaxed); + let value = + execute_into(&line_program, &planes, &foreign, &mut scratch, Out::None).expect("runs"); + let alloc2 = alloc_delta(before2); + + let encoded = match value { + Value::SumI64(s) => s.to_string(), + other => panic!("expected a sum, got {other:?}"), + }; + let out_bytes = encoded.len(); + let m = CaseMetrics { + ops: partner_program.ops.len() + line_program.ops.len(), + scratch_words: p_scratch_words + scratch_words, + alloc_bytes_exec: alloc1 + alloc2, + rows_materialized: 0, + index_vec_len: 0, + out_bytes, + programs: Some(2), + // No N×M pair-relation object exists anywhere in this case: the fk + // resolves through one `MaskOp::Gather` read of the foreign plane + // per line row, never a materialized `(line, partner)` pair list. + pair_relation_bytes: 0, + }; + print_metric("join_sum_country", &m); + assert_case(&cases, "join_sum_country", &encoded); } +/// `SELECT COUNT(*) FROM doc d WHERE EXISTS(SELECT 1 FROM line l WHERE +/// l.doc_id=d.rid AND l.status=1)` — the one-to-many hop BACK: +/// `Agg::ScatterOrU32` sets bit `doc_id` of an 8-word `Out::Mask` buffer for +/// every posted line, then a tiny doc-side program counts the buffer as a +/// resident plane. #[test] -#[should_panic(expected = "NO JOIN LOWERING")] -fn join_count_docs_with_posted_is_the_open_seam() { +fn join_count_docs_with_posted() { let cases = load_cases(); - let expected = expected_only(&cases, "join_count_docs_with_posted"); - assert!( - !expected.is_empty(), - "join_count_docs_with_posted has no oracle value — run oracle.py first" - ); - todo!("NO JOIN LOWERING: join_count_docs_with_posted"); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + + // Phase 1 (line side): `status == 1` -> ScatterOrU32(doc_id, DOC_ROWS). + let line_program = lower(&Query { + filter: Filter::cmp(STATUS, Cmp::EqU32(1)), + agg: Agg::ScatterOrU32 { + fk: DOC_ID_U32, + out_rows: DOC_ROWS as u32, + }, + }) + .expect("lowers"); + let words = words_for(planes.n_rows); + let slots = line_program.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let scratch_words = buf.len(); + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut scattered = vec![0u64; words_for(DOC_ROWS)]; + let before1 = BYTES.load(Ordering::Relaxed); + let value1 = execute_into( + &line_program, + &planes, + &Foreign::NONE, + &mut scratch, + Out::Mask(&mut scattered), + ) + .expect("runs"); + let alloc1 = alloc_delta(before1); + assert_eq!(value1, Value::Scattered); + // The demanded terminal RESULT of this phase is the 8-word buffer + // itself — count it in bytes here rather than the final scalar answer. + let scattered_bytes = scattered.len() * std::mem::size_of::(); + + // Phase 2 (doc side): a tiny program reading `scattered` as a resident + // plane and counting it — a second `Terminal::Count`, per the module + // doc's "say which": this repo's own executor over a plain `Filter::Plane`, + // not a bespoke `ndarray::simd::popcount_batch_u64` call, so the count + // goes through the SAME validated, differential-tested path as every + // other case in this suite. + let doc_masks: [&[u64]; 1] = [&scattered]; + let doc_planes = Planes { + n_rows: DOC_ROWS, + masks: &doc_masks, + lanes: &[], + }; + let doc_program = lower(&Query { + filter: Filter::plane(lance_graph_quack::Mask(0)), + agg: Agg::Count, + }) + .expect("lowers"); + let d_words = words_for(DOC_ROWS); + let d_slots = doc_program.scratch_slots as usize; + let mut d_buf = vec![0u64; scratch_words_for(d_words, d_slots).expect("sized")]; + let d_scratch_words = d_buf.len(); + let mut d_scratch = Scratch::over(&mut d_buf, d_words, d_slots).expect("carves"); + let before2 = BYTES.load(Ordering::Relaxed); + let value2 = execute(&doc_program, &doc_planes, &mut d_scratch, None).expect("runs"); + let alloc2 = alloc_delta(before2); + + let encoded = match value2 { + Value::Count(c) => c.to_string(), + other => panic!("expected a count, got {other:?}"), + }; + let out_bytes = scattered_bytes + encoded.len(); + let m = CaseMetrics { + ops: line_program.ops.len() + doc_program.ops.len(), + scratch_words: scratch_words + d_scratch_words, + alloc_bytes_exec: alloc1 + alloc2, + rows_materialized: 0, + index_vec_len: 0, + out_bytes, + programs: Some(2), + pair_relation_bytes: 0, + }; + print_metric("join_count_docs_with_posted", &m); + assert_case(&cases, "join_count_docs_with_posted", &encoded); } +/// `SELECT p.country, SUM(l.amount) FROM line l JOIN partner p ON +/// p.rid=l.partner_id WHERE l.status=1 GROUP BY p.country ORDER BY +/// p.country` — a group-sum whose KEY lives on the FOREIGN table +/// (`p.country`, reached through `l.partner_id`). `Terminal::GroupSumI32`'s +/// `key` lane is read directly off THIS program's own `Planes`; there is no +/// via-key variant wired to `ndarray::simd::masked_group_sum_i32_via` yet +/// (see the module doc). Still the honest open seam. #[test] -#[should_panic(expected = "NO JOIN LOWERING")] +#[should_panic(expected = "NO VIA-KEY GROUP SUM")] fn join_group_sum_country_is_the_open_seam() { let cases = load_cases(); let expected = expected_only(&cases, "join_group_sum_country"); @@ -609,5 +840,5 @@ fn join_group_sum_country_is_the_open_seam() { !expected.is_empty(), "join_group_sum_country has no oracle value — run oracle.py first" ); - todo!("NO JOIN LOWERING: join_group_sum_country"); + todo!("NO VIA-KEY GROUP SUM: join_group_sum_country"); } From e3f155e6f5cab5bcab3e3a7f62640194fd87db07 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 20:52:04 +0000 Subject: [PATCH 03/15] mask-risc: fk-keyed GroupSumViaI32 terminal; Quack lowers join_group_sum_country to one program Foreign gains value lanes (`Foreign::lanes`), the twin of `Foreign::planes`. `Terminal::GroupSumViaI32 { mask, fk, key, val }` delegates once to `ndarray::simd::masked_group_sum_i32_via`; the oracle reproduces the two-hop zero-fallback row by row. Quack: `Agg::GroupSumViaI32`. Harness: the last should_panic join case is now a real differential test (programs=1, alloc_bytes_exec=0, pair_relation_bytes=0, equal to DuckDB). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/lance-graph-mask-risc/src/exec.rs | 35 +++- crates/lance-graph-mask-risc/src/ir.rs | 40 +++- crates/lance-graph-mask-risc/src/lib.rs | 16 +- crates/lance-graph-mask-risc/src/reference.rs | 64 ++++++ crates/lance-graph-mask-risc/src/value.rs | 14 ++ crates/lance-graph-mask-risc/tests/foreign.rs | 183 ++++++++++++++++++ crates/lance-graph-quack/src/lib.rs | 143 +++++++++++++- .../tests/duckdb_differential.rs | 130 +++++++++---- 8 files changed, 561 insertions(+), 64 deletions(-) diff --git a/crates/lance-graph-mask-risc/src/exec.rs b/crates/lance-graph-mask-risc/src/exec.rs index 971be61f6..fff632114 100644 --- a/crates/lance-graph-mask-risc/src/exec.rs +++ b/crates/lance-graph-mask-risc/src/exec.rs @@ -28,10 +28,10 @@ use ndarray::simd::{ le_i32_to_mask_under, lt_i32_to_mask, lt_i32_to_mask_under, mask_all, mask_and, mask_and_assign, mask_andnot, mask_andnot_assign, mask_any, mask_gather_u32, mask_not, mask_not_assign, mask_or, mask_or_assign, mask_scatter_or_u32, mask_set_range, mask_xor, - mask_xor_assign, masked_group_sum_i32, masked_max_i32, masked_min_i32, masked_sum_i32, - ne_i32_to_mask, ne_i32_to_mask_under, ne_u32_to_mask, ne_u32_to_mask_under, popcount_batch_u64, - ternary_match_u32_to_mask, ternary_match_u32_to_mask_under, ternary_match_u64_to_mask, - ternary_match_u64_to_mask_under, + mask_xor_assign, masked_group_sum_i32, masked_group_sum_i32_via, masked_max_i32, + masked_min_i32, masked_sum_i32, ne_i32_to_mask, ne_i32_to_mask_under, ne_u32_to_mask, + ne_u32_to_mask_under, popcount_batch_u64, ternary_match_u32_to_mask, + ternary_match_u32_to_mask_under, ternary_match_u64_to_mask, ternary_match_u64_to_mask_under, }; use crate::ir::{ @@ -452,6 +452,17 @@ fn lane_u64<'a>(planes: &Planes<'a>, lane: u16) -> &'a [u64] { } } +/// Borrow a foreign `U32` lane for reading — [`lane_u32`]'s twin over +/// [`Foreign::lanes`], a SEPARATE address space from `planes.lanes` (see +/// [`Foreign`]'s own doc). Unreachable after `validate` on a well-typed +/// program, same fallback discipline as `lane_i32`/`lane_u32`/`lane_u64`. +fn foreign_lane_u32<'a>(foreign: &Foreign<'a>, key: u16) -> &'a [u32] { + match foreign.lanes[usize::from(key)] { + LaneRef::U32(v) => v, + _ => &[], + } +} + /// One predicate pass into `dst`: the ungated facade member, or the `_under` /// member when a gate is present (cost then follows the gate's live words). fn run_pred<'a>( @@ -790,6 +801,22 @@ pub fn execute_into( } Value::GroupSummed } + Terminal::GroupSumViaI32 { mask, fk, key, val } => { + // `validate` already refused a missing/too-small `out`, a + // wrong-width `fk`/`val`, and an out-of-range or wrong-width + // foreign `key` — one delegation, same as every other terminal + // (law L3). + if let Out::I64(o) = out { + masked_group_sum_i32_via( + read(planes, &all, mask), + lane_u32(planes, fk), + foreign_lane_u32(foreign, key), + lane_i32(planes, val), + o, + ); + } + Value::GroupSummed + } Terminal::Keep { mask } => Value::Mask(mask), }) } diff --git a/crates/lance-graph-mask-risc/src/ir.rs b/crates/lance-graph-mask-risc/src/ir.rs index c7c659677..75e8a4fd5 100644 --- a/crates/lance-graph-mask-risc/src/ir.rs +++ b/crates/lance-graph-mask-risc/src/ir.rs @@ -73,16 +73,29 @@ pub struct ForeignPlane<'a> { /// never a member of [`Planes::masks`] and its length is never checked /// against the executing program's `n_rows`: that would be checking a /// population against a population it is not. +/// +/// `lanes` is the value-lane twin: foreign VALUE lanes over the SAME other +/// table's rows, addressed by [`Terminal::GroupSumViaI32::key`]. Its length +/// is the foreign table's row count — never `n_rows` either, and never +/// [`ForeignPlane::rows`]-checked against a particular plane, since a +/// program may name planes and lanes belonging to different foreign tables +/// in the same [`Foreign`] (there is no assumption that plane `i` and lane +/// `i` share a row space). #[derive(Debug, Clone, Copy)] pub struct Foreign<'a> { /// Foreign planes, indexed by [`MaskOp::Gather::foreign`]. pub planes: &'a [ForeignPlane<'a>], + /// Foreign value lanes, indexed by [`Terminal::GroupSumViaI32::key`]. + pub lanes: &'a [LaneRef<'a>], } impl Foreign<'_> { - /// The empty foreign set — every program that names no `Gather` runs - /// against this. - pub const NONE: Foreign<'static> = Foreign { planes: &[] }; + /// The empty foreign set — every program that names no `Gather` or + /// `GroupSumViaI32` runs against this. + pub const NONE: Foreign<'static> = Foreign { + planes: &[], + lanes: &[], + }; } /// A value-lane predicate that produces a mask — the vector half of a @@ -246,6 +259,26 @@ pub enum Terminal { /// past it either, since it is strictly less work than the one-group /// sum the bound was derived against. GroupSumI32 { mask: Operand, key: u16, val: u16 }, + /// The FK-KEYED `GROUP BY … SUM`: `SUM(line.amount) GROUP BY + /// partner.country` — `fk` is a `U32` lane of THIS table (the foreign + /// key), `key` indexes [`Foreign::lanes`] and must be a `U32` lane on + /// the foreign table (the group key there), and `val` is an `I32` lane + /// of this table. For every row `i` where `mask` holds, resolves + /// `foreign.lanes[key][fk[i]]` and adds `val[i]` at that index into the + /// caller's `Out::I64` buffer + /// ([`ndarray::simd::masked_group_sum_i32_via`]'s contract). Zero + /// fallback at BOTH hops — `fk[i] >= foreign.lanes[key].len()` drops the + /// row (the fk names no foreign row), and a resolved key `>= out.len()` + /// drops it too (the resolved key names no group); neither is an error. + /// The indirection is FUSED: no remapped key lane is ever materialised + /// between the two hops. Same carry bound as [`Terminal::GroupSumI32`] + /// ([`MASKED_SUM_I32_MAX_ROWS`]). + GroupSumViaI32 { + mask: Operand, + fk: u16, + key: u16, + val: u16, + }, } /// The widest plane [`Terminal::MaskedSumI32`] is defined on: `2^32` rows. @@ -357,6 +390,7 @@ impl Program { | Terminal::BlendI32 { mask, .. } | Terminal::ScatterOrU32 { mask, .. } | Terminal::GroupSumI32 { mask, .. } + | Terminal::GroupSumViaI32 { mask, .. } | Terminal::Keep { mask } => touch(mask), } Self { diff --git a/crates/lance-graph-mask-risc/src/lib.rs b/crates/lance-graph-mask-risc/src/lib.rs index ce5b3ab6e..7b79aa4b7 100644 --- a/crates/lance-graph-mask-risc/src/lib.rs +++ b/crates/lance-graph-mask-risc/src/lib.rs @@ -16,13 +16,15 @@ //! zero-allocation law. `hop` (PR5) LANDED as [`MaskOp::Gather`] (the fk //! semijoin, over [`ir::Foreign`]) and [`Terminal::ScatterOrU32`] (the //! one-to-many hop back); [`Terminal::GroupSumI32`] is the one-terminal -//! `GROUP BY … SUM`. Still absent, named: a strided `Operand` — the gap is -//! in THIS IR, not in T1: `ndarray::simd` already ships -//! `ternary_match_strided_to_mask`, `eq_u32_strided_to_mask` and -//! `masked_strided_group_sum`, and nothing here can name a `(base, stride, -//! group)` source; a via-key group-sum ([`ndarray::simd::masked_group_sum_i32_via`], -//! `SUM(...) GROUP BY partner.country`); and the Cypher `mask_lower` seam -//! (the Cypher plan's Wave 1 consumes this crate). +//! `GROUP BY … SUM`, and [`Terminal::GroupSumViaI32`] is its fk-keyed +//! sibling ([`ndarray::simd::masked_group_sum_i32_via`], +//! `SUM(...) GROUP BY partner.country`) — [`ir::Foreign::lanes`] is the +//! foreign VALUE-lane twin of `Foreign::planes`. Still absent, named: a +//! strided `Operand` — the gap is in THIS IR, not in T1: `ndarray::simd` +//! already ships `ternary_match_strided_to_mask`, `eq_u32_strided_to_mask` +//! and `masked_strided_group_sum`, and nothing here can name a `(base, +//! stride, group)` source; and the Cypher `mask_lower` seam (the Cypher +//! plan's Wave 1 consumes this crate). //! //! One duplication is filed rather than resolved here: `lgj-abi` already //! carries its own runtime-immediate → const-generic ternlog bridge diff --git a/crates/lance-graph-mask-risc/src/reference.rs b/crates/lance-graph-mask-risc/src/reference.rs index df5145c41..5814e7043 100644 --- a/crates/lance-graph-mask-risc/src/reference.rs +++ b/crates/lance-graph-mask-risc/src/reference.rs @@ -127,6 +127,25 @@ fn check_lane(planes: &Planes<'_>, lane: u16, expected: LaneKind) -> Result<(), } } +/// [`check_lane`]'s twin over [`Foreign::lanes`] — a SEPARATE address space +/// from `Planes::lanes`, so `key` is bounded against `foreign.lanes.len()`, +/// never against `planes.lanes.len()`. +fn check_foreign_lane( + foreign: &Foreign<'_>, + key: u16, + expected: LaneKind, +) -> Result<(), ExecError> { + match foreign.lanes.get(usize::from(key)) { + None => Err(ExecError::ForeignLaneOutOfRange(key)), + Some(l) if kind_of(l) != expected => Err(ExecError::ForeignLaneKind { + lane: key, + expected, + found: kind_of(l), + }), + Some(_) => Ok(()), + } +} + /// A scratch slot is readable only after some EARLIER op has written it. /// /// Without this rule the executor would read whatever the caller's REUSED @@ -452,6 +471,24 @@ pub(crate) fn validate( } } } + Terminal::GroupSumViaI32 { mask, fk, key, val } => { + check_operand(p, planes, mask)?; + written_slots.readable(mask)?; + check_lane(planes, fk, LaneKind::U32)?; + check_foreign_lane(foreign, key, LaneKind::U32)?; + check_lane(planes, val, LaneKind::I32)?; + if n > MASKED_SUM_I32_MAX_ROWS { + return Err(ExecError::SumRowBound { n_rows: n }); + } + match out { + OutShape::I64(len) if len >= 1 => Ok(()), + OutShape::None | OutShape::I32(_) | OutShape::I64(_) | OutShape::Mask(_) => { + Err(ExecError::TerminalNeedsOut { + what: "GroupSumViaI32", + }) + } + } + } } } @@ -723,6 +760,33 @@ pub fn reference_execute_into( } Value::GroupSummed } + Terminal::GroupSumViaI32 { mask, fk, key, val } => { + if let Out::I64(o) = out { + for x in o.iter_mut() { + *x = 0; + } + // `key` was validated `LaneKind::U32` over `foreign.lanes`, + // so the fallback never fires on a well-typed program — the + // same "validated before this is called" rule + // `eval_pred`'s lane fallbacks already document. + let remap = match foreign.lanes.get(usize::from(key)) { + Some(LaneRef::U32(v)) => &v[..], + _ => &[][..], + }; + for r in survivors(mask) { + let idx = u32_at(planes, fk, r) as usize; + if idx >= remap.len() { + continue; + } + let k = remap[idx] as usize; + if k < o.len() { + let v = i64::from(i32_at(planes, val, r)); + o[k] = o[k].wrapping_add(v); + } + } + } + Value::GroupSummed + } Terminal::Keep { mask } => Value::Mask(mask), }) } diff --git a/crates/lance-graph-mask-risc/src/value.rs b/crates/lance-graph-mask-risc/src/value.rs index 307567af3..24feb00e9 100644 --- a/crates/lance-graph-mask-risc/src/value.rs +++ b/crates/lance-graph-mask-risc/src/value.rs @@ -134,6 +134,20 @@ pub enum ExecError { /// [`crate::MaskOp::Gather`]'s `foreign` names no entry of the caller's /// [`crate::Foreign::planes`]. ForeignOutOfRange(u16), + /// [`crate::Terminal::GroupSumViaI32`]'s `key` names no entry of the + /// caller's [`crate::Foreign::lanes`]. Distinct from [`Self::LaneOutOfRange`] + /// — that one bounds a program's OWN `Planes::lanes`, this one bounds + /// the SEPARATE foreign address space, and the two must never be + /// checked against the same length. + ForeignLaneOutOfRange(u16), + /// The foreign lane [`crate::Terminal::GroupSumViaI32::key`] names + /// exists, but at the wrong width — mirrors [`Self::LaneKind`] over + /// [`crate::Foreign::lanes`] rather than `Planes::lanes`. + ForeignLaneKind { + lane: u16, + expected: LaneKind, + found: LaneKind, + }, /// A terminal's [`crate::Out`] is missing, or is the wrong SHAPE /// (`Out::I32` where an `Out::I64` was needed, and so on) — or, for /// [`crate::Terminal::ScatterOrU32`], sized to the wrong `out_rows`, or diff --git a/crates/lance-graph-mask-risc/tests/foreign.rs b/crates/lance-graph-mask-risc/tests/foreign.rs index b3a5d302d..9b6c0a488 100644 --- a/crates/lance-graph-mask-risc/tests/foreign.rs +++ b/crates/lance-graph-mask-risc/tests/foreign.rs @@ -135,6 +135,7 @@ fn gather_matches_the_oracle_including_out_of_range_and_a_tail() { let fp = fx.foreign_plane(); let foreign = Foreign { planes: std::slice::from_ref(&fp), + lanes: &[], }; let p = Program::new( vec![MaskOp::Gather { @@ -336,6 +337,7 @@ fn validation_refusals_match_between_executor_and_oracle() { let fp = fx.foreign_plane(); let foreign = Foreign { planes: std::slice::from_ref(&fp), + lanes: &[], }; let assert_same_refusal = |label: &str, @@ -465,6 +467,7 @@ fn gather_composes_with_and_like_any_other_leaf() { let fp = fx.foreign_plane(); let foreign = Foreign { planes: std::slice::from_ref(&fp), + lanes: &[], }; let p = Program::new( vec![ @@ -499,3 +502,183 @@ fn gather_composes_with_and_like_any_other_leaf() { panic!("expected a count, got {got:?}"); } } + +/// FAILS IF: `Terminal::GroupSumViaI32` disagrees with the oracle anywhere +/// in a sweep that mixes BOTH drop kinds — a fk naming no foreign row (the +/// first hop) and a resolved key naming no group (the second hop) — with +/// negative values in the summed lane. Non-vacuous: asserts both drop +/// kinds actually fire somewhere across the sweep. +#[test] +fn group_sum_via_matches_the_oracle_with_both_hops_dropping_and_negatives() { + let groups = 4u32; + let mut any_first_hop_dropped = false; + let mut any_second_hop_dropped = false; + for &n in &ROWS { + let fx = Fixture::new(n, 10, groups, 0xFEED ^ n as u64); + let (lanes, masks) = fx.planes(); + let planes = Planes { + n_rows: n, + masks: &masks, + lanes: &lanes, + }; + + // A SECOND remap lane over the foreign table's own row space + // (`fx.foreign_rows`), values `0..(groups + slack)` so some + // resolved keys are dropped at the SECOND hop, independently of + // `fx.fk`'s own out-of-range draws (the FIRST hop). + let mut s = 0x600D_FEEDu64 ^ n as u64; + let key_range = groups + 5; + let remap: Vec = (0..fx.foreign_rows) + .map(|_| { + if key_range == 0 { + 0 + } else { + (lcg(&mut s) % u64::from(key_range)) as u32 + } + }) + .collect(); + let foreign_lanes = [LaneRef::U32(&remap)]; + let foreign = Foreign { + planes: &[], + lanes: &foreign_lanes, + }; + + let p = Program::new( + vec![MaskOp::Pred { + pred: Pred::NeU32 { lane: 1, v: 2 }, + under: None, + dst: 0, + }], + Terminal::GroupSumViaI32 { + mask: S0, + fk: 0, + key: 0, + val: 2, + }, + ); + let words = words_for(n); + let slots = p.scratch_slots as usize; + let mut exec_buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut exec_buf, words, slots).expect("carves"); + let mut got_out = vec![0i64; groups as usize]; + let got = execute_into(&p, &planes, &foreign, &mut scratch, Out::I64(&mut got_out)) + .expect("runs"); + let mut want_out = vec![0i64; groups as usize]; + let want = reference_execute_into(&p, &planes, &foreign, Out::I64(&mut want_out)) + .expect("oracle runs"); + assert_eq!(got, want, "n={n}: value"); + assert_eq!(got_out, want_out, "n={n}: out buffer"); + assert!(matches!(got, Value::GroupSummed)); + + let first_hop_dropped = fx + .status + .iter() + .zip(fx.fk.iter()) + .filter(|(&st, &fk)| st != 2 && fk as usize >= fx.foreign_rows) + .count(); + if first_hop_dropped > 0 { + any_first_hop_dropped = true; + } + let second_hop_dropped = fx + .status + .iter() + .zip(fx.fk.iter()) + .filter(|(&st, &fk)| { + st != 2 && (fk as usize) < fx.foreign_rows && remap[fk as usize] >= groups + }) + .count(); + if second_hop_dropped > 0 { + any_second_hop_dropped = true; + } + } + assert!( + any_first_hop_dropped, + "the sweep never dropped at the first hop (fk out of range)" + ); + assert!( + any_second_hop_dropped, + "the sweep never dropped at the second hop (resolved key >= groups)" + ); +} + +/// FAILS IF: `GroupSumViaI32`'s two NEW refusals — a `key` past +/// `foreign.lanes.len()`, and a `key` naming a foreign lane of the wrong +/// width — are not reported IDENTICALLY by the executor and the oracle. +#[test] +fn group_sum_via_refusals_match_between_executor_and_oracle() { + let fx = Fixture::new(200, 10, 4, 0xFACE); + let (lanes, masks) = fx.planes(); + let planes = Planes { + n_rows: 200, + masks: &masks, + lanes: &lanes, + }; + + // Foreign lane 0 is the correct kind (U32); foreign lane 1 is the + // WRONG kind (I32) for a group key. + let remap_u32: Vec = (0..fx.foreign_rows).map(|i| (i % 4) as u32).collect(); + let remap_i32: Vec = (0..fx.foreign_rows).map(|i| i as i32).collect(); + let foreign_lanes = [LaneRef::U32(&remap_u32), LaneRef::I32(&remap_i32)]; + let foreign = Foreign { + planes: &[], + lanes: &foreign_lanes, + }; + + let assert_same_refusal = |label: &str, p: &Program, want: ExecError| { + let words = words_for(planes.n_rows); + let slots = p.scratch_slots.max(1) as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut got_out = [0i64; 4]; + let got = execute_into(p, &planes, &foreign, &mut scratch, Out::I64(&mut got_out)); + assert_eq!(got, Err(want), "{label}: executor"); + let mut want_out = [0i64; 4]; + let oracle = reference_execute_into(p, &planes, &foreign, Out::I64(&mut want_out)); + assert_eq!(oracle, Err(want), "{label}: oracle"); + }; + + // (a) `key` past `foreign.lanes.len()`. + let p_bad_key = Program::new( + vec![MaskOp::Pred { + pred: Pred::NeU32 { lane: 1, v: 99 }, + under: None, + dst: 0, + }], + Terminal::GroupSumViaI32 { + mask: S0, + fk: 0, + key: 5, + val: 2, + }, + ); + assert_same_refusal( + "foreign lane out of range", + &p_bad_key, + ExecError::ForeignLaneOutOfRange(5), + ); + + // (b) `key` names a foreign lane of the wrong width (I32 where a U32 + // group key is required). + let p_wrong_kind = Program::new( + vec![MaskOp::Pred { + pred: Pred::NeU32 { lane: 1, v: 99 }, + under: None, + dst: 0, + }], + Terminal::GroupSumViaI32 { + mask: S0, + fk: 0, + key: 1, + val: 2, + }, + ); + assert_same_refusal( + "wrong foreign lane kind", + &p_wrong_kind, + ExecError::ForeignLaneKind { + lane: 1, + expected: lance_graph_mask_risc::LaneKind::U32, + found: lance_graph_mask_risc::LaneKind::I32, + }, + ); +} diff --git a/crates/lance-graph-quack/src/lib.rs b/crates/lance-graph-quack/src/lib.rs index 5ad6de6e1..72ed3c066 100644 --- a/crates/lance-graph-quack/src/lib.rs +++ b/crates/lance-graph-quack/src/lib.rs @@ -146,13 +146,15 @@ //! Filter (`=`/`<>`/`<`/`<=`/`>`/`>=`/ternary match/`IN`, `AND`/`OR`/`NOT`, //! resident planes), the aggregates `COUNT`/`EXISTS`/`ALL`/`SUM`/`MIN`/`MAX`, //! projection ([`Agg::Rows`] keeps the mask, [`Agg::BlendI32`] is the `CASE` -//! shape), and a two-phase `GROUP BY` over a categorical key. Absent, named: -//! the join (`src_mask → hop → dst_mask` is `lance-graph-mask-risc`'s PR5 — -//! there is no `hop` op to lower to yet); a one-terminal `GROUP BY SUM` -//! (`ndarray::simd` ships `masked_strided_group_sum`, but the IR names no -//! strided operand or group terminal, so K programs is the honest spelling -//! today); and everything the IR itself excludes — strings, `ORDER BY`, -//! three-valued NULL. +//! shape), a two-phase `GROUP BY` over a categorical key, and the fk join +//! ([`Filter::Semijoin`] / [`Agg::ScatterOrU32`] over +//! `lance-graph-mask-risc`'s [`MaskOp::Gather`]/`Terminal::ScatterOrU32`). +//! The one-terminal `GROUP BY SUM` is ONE program either way now: +//! [`Agg::GroupSumI32`] for a key on THIS table, [`Agg::GroupSumViaI32`] for +//! a key resolved through a foreign table's [`ForeignLane`] +//! (`SUM(l.amount) GROUP BY p.country`) — the indirect-key group-sum is no +//! longer a gap. Absent, and everything the IR itself excludes: `ORDER BY`, +//! strings, three-valued NULL. #![forbid(unsafe_code)] @@ -186,6 +188,15 @@ pub struct Mask(pub u16); #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ForeignMask(pub u16); +/// A FOREIGN table's VALUE lane — an index into +/// [`Foreign::lanes`](lance_graph_mask_risc::Foreign), the value-lane twin of +/// [`ForeignMask`]. Addressed through [`Agg::GroupSumViaI32`]: `SUM(l.amount) +/// GROUP BY p.country` names the group key as a foreign lane, resolved +/// through THIS table's own fk column — never a lane this crate reads as an +/// ordinary [`Col`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ForeignLane(pub u16); + /// A predicate over one column — the filter's whole vocabulary. /// /// One variant per masking op that produces a mask from a value lane. That is @@ -782,6 +793,22 @@ pub enum Agg { /// The value column summed per group. val: Col, }, + /// The FK-KEYED `GROUP BY … SUM` — `Terminal::GroupSumViaI32`: + /// `SUM(line.amount) GROUP BY partner.country` is `Agg::GroupSumViaI32 { + /// fk: partner_id, key: ForeignLane(0), val: amount }` over a foreign + /// [`ForeignLane`] carrying `partner.country`. Same zero-fallback at + /// BOTH hops as [`Agg::GroupSumI32`]'s single-hop drop (an fk naming no + /// foreign row, or a resolved key at or past the group universe, are + /// both dropped, not errors). The indirection is fused — no remapped + /// key lane is ever materialised. + GroupSumViaI32 { + /// The foreign-key column on THIS table. + fk: Col, + /// The group key column on the FOREIGN table. + key: ForeignLane, + /// The value column summed per group, on THIS table. + val: Col, + }, } /// One query: a filter and what to ask of the rows that pass it. @@ -1355,6 +1382,12 @@ fn terminal_of(agg: Agg, mask: Operand) -> Terminal { key: key.0, val: val.0, }, + Agg::GroupSumViaI32 { fk, key, val } => Terminal::GroupSumViaI32 { + mask, + fk: fk.0, + key: key.0, + val: val.0, + }, } } @@ -1422,6 +1455,11 @@ mod tests { /// word-rounded length alone cannot distinguish "row 63 of a 64-row /// table" from "the dirty tail of a 60-row one". foreign_rows: Vec, + /// Foreign `u32` VALUE lanes, indexed by [`ForeignLane`] — opt-in and + /// EMPTY for every single-table fixture, populated only by the + /// `GroupSumViaI32` test via [`Fx::push_foreign_lane`]. No existing + /// fixture's shape moves. + foreign_lanes: Vec>, } fn plane(n: usize, set: impl Fn(usize) -> bool) -> Vec { @@ -1449,6 +1487,7 @@ mod tests { masks, foreign: Vec::new(), foreign_rows: Vec::new(), + foreign_lanes: Vec::new(), } } @@ -1461,6 +1500,15 @@ mod tests { ForeignMask(idx) } + /// Register a foreign `u32` VALUE lane at the next `ForeignLane` + /// index (`0`, `1`, …, in call order) and return that index — the + /// [`Fx::push_foreign`] twin for [`ForeignLane`]/`GroupSumViaI32`. + fn push_foreign_lane(&mut self, values: Vec) -> ForeignLane { + let idx = self.foreign_lanes.len() as u16; + self.foreign_lanes.push(values); + ForeignLane(idx) + } + fn n(&self) -> usize { self.vals.len() } @@ -1864,6 +1912,7 @@ mod tests { | Terminal::BlendI32 { mask, .. } | Terminal::ScatterOrU32 { mask, .. } | Terminal::GroupSumI32 { mask, .. } + | Terminal::GroupSumViaI32 { mask, .. } | Terminal::Keep { mask } if mask == gate ); ( @@ -2608,6 +2657,7 @@ mod tests { words: &foreign_bits, rows: foreign_rows, }], + lanes: &[], }; for (label, program) in [ ("in place", lower(&q).expect("lowers in place")), @@ -2735,6 +2785,85 @@ mod tests { assert_eq!(value, Value::GroupSummed); assert_eq!(got, want); } + + /// FAILS IF: `Agg::GroupSumViaI32` disagrees with an independent per-row + /// reading. `CLASS` is the fk into a 4-row foreign table (shorter than + /// `CLASS`'s own `0..5` range, so class 4 drops at the FIRST hop); its + /// `remap` lane resolves to `0..4`, while `groups = 3` drops some + /// resolved keys at the SECOND hop. Both drop kinds are exercised + /// deliberately, alongside negatives in the summed lane. + #[test] + fn group_sum_via_i32_matches_an_independent_per_row_reading() { + let mut fx = Fx::new(N); + let remap = vec![0u32, 3, 1, 3]; + let foreign_rows = remap.len(); + let foreign_lane = fx.push_foreign_lane(remap); + + let f = Filter::cmp(VALS, Cmp::GtI32(-50)); + let selected = fx.rows(&f); + assert!(!selected.is_empty() && selected.len() < N); + + let groups = 3usize; + let remap = &fx.foreign_lanes[usize::from(foreign_lane.0)]; + let mut want = vec![0i64; groups]; + let mut first_hop_dropped = false; + let mut second_hop_dropped = false; + for &r in &selected { + let fk = fx.classes[r] as usize; + if fk >= foreign_rows { + first_hop_dropped = true; + continue; + } + let k = remap[fk] as usize; + if k >= groups { + second_hop_dropped = true; + continue; + } + want[k] = want[k].wrapping_add(i64::from(fx.alt[r])); + } + assert!( + first_hop_dropped, + "the fixture never drops at the first hop" + ); + assert!( + second_hop_dropped, + "the fixture never drops at the second hop" + ); + let distinct: std::collections::HashSet = want.iter().copied().collect(); + assert!(distinct.len() > 1, "every group summed to the same value"); + + let q = Query { + filter: f, + agg: Agg::GroupSumViaI32 { + fk: CLASS, + key: foreign_lane, + val: ALT, + }, + }; + let program = lower(&q).expect("lowers"); + let words = words_for(fx.n()); + let slots = program.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut got = vec![0i64; groups]; + let foreign_lanes = [LaneRef::U32(remap)]; + let mask_risc_foreign = lance_graph_mask_risc::Foreign { + planes: &[], + lanes: &foreign_lanes, + }; + let value = fx.with_planes(&[], |planes| { + lance_graph_mask_risc::execute_into( + &program, + planes, + &mask_risc_foreign, + &mut scratch, + lance_graph_mask_risc::Out::I64(&mut got), + ) + .expect("runs") + }); + assert_eq!(value, Value::GroupSummed); + assert_eq!(got, want); + } } /// D-DIAMOND-1 R2 — the witnessed prefix lowering, differential against the diff --git a/crates/lance-graph-quack/tests/duckdb_differential.rs b/crates/lance-graph-quack/tests/duckdb_differential.rs index 43abab3c9..0a11984d9 100644 --- a/crates/lance-graph-quack/tests/duckdb_differential.rs +++ b/crates/lance-graph-quack/tests/duckdb_differential.rs @@ -7,19 +7,20 @@ //! encoded the same way, and compared. **Expected values are never //! hand-edited** — see `tests/duckdb/README.txt`. //! -//! `join_group_sum_country_is_the_open_seam` is the one honest exception -//! left: it needs a group-sum key resolved through a FOREIGN table -//! (`SUM(l.amount) GROUP BY p.country`), which waits on -//! `ndarray::simd::masked_group_sum_i32_via` — `Terminal::GroupSumI32`'s key -//! is a lane on THIS table, not a fk-resolved one. `join_sum_country` and -//! `join_count_docs_with_posted` are no longer open seams: the former lowers -//! a real `Filter::Semijoin` (the fk gather) against a real partner-side -//! `Program`; the latter lowers `Agg::ScatterOrU32` (the one-to-many hop -//! back) into a caller-owned mask, then counts it with a second tiny -//! program. `group_sum_cc` now runs the one-terminal `Agg::GroupSumI32` -//! (`Terminal::GroupSumI32`, ONE program) alongside the pre-existing K-program -//! `lower_group_by` reading, printing both METRIC lines so the fold is -//! visible. +//! `join_group_sum_country` closes the last open seam: `SUM(l.amount) +//! GROUP BY p.country` is a group-sum key resolved through a FOREIGN +//! table (`p.country`, reached via `l.partner_id`) — +//! `Terminal::GroupSumViaI32` over `ndarray::simd::masked_group_sum_i32_via`, +//! ONE program reading `partner.country` as a caller-owned +//! [`Foreign::lanes`] entry, no two-hop materialisation and no K-program +//! spelling. `join_sum_country` and `join_count_docs_with_posted` are the +//! other two join cases: the former lowers a real `Filter::Semijoin` (the +//! fk gather) against a real partner-side `Program`; the latter lowers +//! `Agg::ScatterOrU32` (the one-to-many hop back) into a caller-owned +//! mask, then counts it with a second tiny program. `group_sum_cc` runs +//! the one-terminal `Agg::GroupSumI32` (`Terminal::GroupSumI32`, ONE +//! program) alongside the pre-existing K-program `lower_group_by` reading, +//! printing both METRIC lines so the fold is visible. #[path = "duckdb/fixture.rs"] mod fixture; @@ -30,10 +31,10 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use lance_graph_mask_risc::{ execute, execute_into, materialize_rows, scratch_words_for, words_for, Foreign, ForeignPlane, - Operand, Out, Planes, Scratch, Value, + LaneRef, Operand, Out, Planes, Scratch, Value, }; use lance_graph_quack::{ - lower, lower_group_by, Agg, Cmp, Col, Filter, ForeignMask, GroupBy, Query, + lower, lower_group_by, Agg, Cmp, Col, Filter, ForeignLane, ForeignMask, GroupBy, Query, }; use fixture::col::{ @@ -124,18 +125,6 @@ fn assert_case(cases: &[Case], id: &str, actual: &str) { ); } -/// Load `id`'s expected value without asserting anything — the `join_*` -/// cases use this to prove the oracle side is real before `todo!()`-ing the -/// Rust side. -fn expected_only<'a>(cases: &'a [Case], id: &str) -> &'a str { - cases - .iter() - .find(|c| c.id == id) - .unwrap_or_else(|| panic!("case {id} is not in cases.tsv")) - .expected - .as_str() -} - // --------------------------------------------------------------------- // Execution + encoding — one place each, so the differential is actually // comparing the same shape of answer on both sides. @@ -648,10 +637,10 @@ fn group_sum_cc() { } // --------------------------------------------------------------------- -// The join cases. `join_sum_country` (fk gather) and -// `join_count_docs_with_posted` (the one-to-many hop back) are real -// two-program lowerings now; `join_group_sum_country` stays the honest -// open seam — see the module doc for why. +// The join cases. `join_sum_country` (fk gather), +// `join_count_docs_with_posted` (the one-to-many hop back), and +// `join_group_sum_country` (the fk-keyed group-sum) are all real, +// one-or-two-program lowerings now — no open seam remains. // --------------------------------------------------------------------- /// `SELECT SUM(l.amount) FROM line l JOIN partner p ON p.rid=l.partner_id @@ -691,6 +680,7 @@ fn join_sum_country() { }; let foreign = Foreign { planes: &[foreign_plane], + lanes: &[], }; // Phase 2 (line side): `status == 1 AND Semijoin(partner_id, 0)` -> SUM. @@ -827,18 +817,72 @@ fn join_count_docs_with_posted() { /// `SELECT p.country, SUM(l.amount) FROM line l JOIN partner p ON /// p.rid=l.partner_id WHERE l.status=1 GROUP BY p.country ORDER BY /// p.country` — a group-sum whose KEY lives on the FOREIGN table -/// (`p.country`, reached through `l.partner_id`). `Terminal::GroupSumI32`'s -/// `key` lane is read directly off THIS program's own `Planes`; there is no -/// via-key variant wired to `ndarray::simd::masked_group_sum_i32_via` yet -/// (see the module doc). Still the honest open seam. +/// (`p.country`, reached through `l.partner_id`). ONE program: +/// `Agg::GroupSumViaI32` lowers to `Terminal::GroupSumViaI32`, reading +/// `partner.country` directly as a caller-owned [`Foreign::lanes`] entry — +/// no partner-side filter program, no K-program loop, and no materialised +/// remapped key lane between the two hops. #[test] -#[should_panic(expected = "NO VIA-KEY GROUP SUM")] -fn join_group_sum_country_is_the_open_seam() { +fn join_group_sum_country() { let cases = load_cases(); - let expected = expected_only(&cases, "join_group_sum_country"); - assert!( - !expected.is_empty(), - "join_group_sum_country has no oracle value — run oracle.py first" - ); - todo!("NO VIA-KEY GROUP SUM: join_group_sum_country"); + let fx = fixture::generate(); + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + + let program = lower(&Query { + filter: Filter::cmp(STATUS, Cmp::EqU32(1)), + agg: Agg::GroupSumViaI32 { + fk: PARTNER_ID, + key: ForeignLane(0), + val: AMOUNT, + }, + }) + .expect("lowers"); + let words = words_for(planes.n_rows); + let slots = program.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; + let scratch_words = buf.len(); + let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + + let country_lane = [LaneRef::U32(&fx.partner.country)]; + let foreign = Foreign { + planes: &[], + lanes: &country_lane, + }; + let mut out = [0i64; 8]; + let before = BYTES.load(Ordering::Relaxed); + let value = execute_into( + &program, + &planes, + &foreign, + &mut scratch, + Out::I64(&mut out), + ) + .expect("runs"); + let alloc_bytes_exec = alloc_delta(before); + assert_eq!(value, Value::GroupSummed); + + let encoded = out + .iter() + .enumerate() + .map(|(k, v)| format!("{k}:{v}")) + .collect::>() + .join(";"); + let out_bytes = encoded.len(); + let m = CaseMetrics { + ops: program.ops.len(), + scratch_words, + alloc_bytes_exec, + rows_materialized: 0, + index_vec_len: 0, + out_bytes, + programs: Some(1), + // No N×M pair-relation object anywhere: the fk resolves through + // ONE `masked_group_sum_i32_via` call per selected row, reading + // `partner.country` in place — never a materialized `(line, + // partner)` pair list or a remapped key lane. + pair_relation_bytes: 0, + }; + print_metric("join_group_sum_country", &m); + assert_case(&cases, "join_group_sum_country", &encoded); } From 06ae0abab37a9d9ee770d7b04a56579f4e4a0f4a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 21:06:26 +0000 Subject: [PATCH 04/15] helix, perturbation-sim: resolve ndarray from the one sibling path, not git MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every other crate under this tree resolves the fork at ../../../ndarray; these two declared a git source and so carried a second ndarray package identity. One ndarray means one resolved identity. Verified: both check clean and cargo metadata reports path+file:///…/ndarray#0.17.2 only. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/helix/Cargo.toml | 17 +++++++++-------- crates/perturbation-sim/Cargo.toml | 15 +++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/helix/Cargo.toml b/crates/helix/Cargo.toml index ec0621b7d..c8b0052d5 100644 --- a/crates/helix/Cargo.toml +++ b/crates/helix/Cargo.toml @@ -11,14 +11,15 @@ description = "Place/Residue encoding: golden-spiral hemisphere placement, Fishe # helix's batch hot path (src/simd.rs) runs on `ndarray::simd`, and per the stack # rule "ndarray is mandatory for lance-graph" it is a hard dep, not an opt-in. [dependencies] -# MANDATORY ndarray (the AdaWorldAPI fork). Sourced by GIT, not a local path: a -# local *path* dep forces Cargo to read the sibling manifest during resolution, -# so a clean checkout without the sibling fails (codex P2 on #460); a git source -# resolves the manifest remotely. `master` is the commit helix was tested against. -# The fork is self-contained (internal subcrates only — no external path dep, no -# back-dependency on lance-graph → no import cycle). `std` enables `ndarray::simd` -# (simd_ln_f32 / F32x16 / U8x64). -ndarray = { git = "https://github.com/AdaWorldAPI/ndarray.git", branch = "master", default-features = false, features = ["std"] } +# MANDATORY ndarray (the AdaWorldAPI fork), the SAME sibling path every other +# crate in this tree resolves — one Cargo package identity, one execution +# algebra. It was a git source until 2026-09-21 (codex P2 on #460: a clean +# checkout without the sibling fails to resolve); that bought a second ndarray +# package identity, which is the worse trade. A sibling checkout is a stated +# requirement of the whole tree, not of this crate. The fork is self-contained +# (no back-dependency on lance-graph → no import cycle). `std` enables +# `ndarray::simd` (simd_ln_f32 / F32x16 / U8x64). +ndarray = { path = "../../../ndarray", default-features = false, features = ["std"] } [dev-dependencies] diff --git a/crates/perturbation-sim/Cargo.toml b/crates/perturbation-sim/Cargo.toml index ba3f437ce..37d522f83 100644 --- a/crates/perturbation-sim/Cargo.toml +++ b/crates/perturbation-sim/Cargo.toml @@ -20,14 +20,13 @@ description = "Spectral + edge-propagation outage simulator: models the perturba ndarray-simd = ["dep:ndarray"] [dependencies] -# The AdaWorldAPI fork ("The Foundation"), optional + behind `ndarray-simd`. -# Sourced by GIT (not a local path): an optional *path* dep is read at manifest -# resolution, so a clean checkout without the sibling `../../../ndarray` fails -# even with the feature OFF — breaking the zero-dep default (codex P2 on #504, -# the helix-#460 lesson). A git source resolves remotely and is only fetched/ -# built when `ndarray-simd` activates it. Local-dev alternative: swap to -# ndarray = { path = "../../../ndarray", optional = true, default-features = false, features = ["std"] } -ndarray = { git = "https://github.com/AdaWorldAPI/ndarray.git", branch = "master", optional = true, default-features = false, features = ["std"] } +# The AdaWorldAPI fork ("The Foundation"), optional + behind `ndarray-simd`, +# from the SAME sibling path every other crate in this tree resolves — one +# Cargo package identity. It was a git source until 2026-09-21 (codex P2 on +# #504: an optional path dep is read at resolution even with the feature OFF, +# so a checkout without the sibling fails); a second ndarray identity is the +# worse trade, and the sibling checkout is a requirement of the whole tree. +ndarray = { path = "../../../ndarray", optional = true, default-features = false, features = ["std"] } [lib] name = "perturbation_sim" From 4fedb9a9e9cb7520013c1cf74dd4900f932dd731 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 21:29:44 +0000 Subject: [PATCH 05/15] =?UTF-8?q?mask-risc:=20tiled=20execution=20?= =?UTF-8?q?=E2=80=94=20no=20population-sized=20state=20outside=20the=20dem?= =?UTF-8?q?anded=20sink?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator ruling: an intermediate population is forbidden whenever the next fold can consume the projection directly; allocation is irrelevant and a caller-owned scratch bitmap is still materialisation. execute_into now runs a program one tile at a time (tile = the scratch slot width; Scratch::for_program carves TILE_WORDS = 8 words), so execution state is slots × 8 words whatever n_rows is. Terminals fold each tile into O(1) accumulators or the caller's Out; ScatterOr / GroupSum sinks are zeroed once and accumulated (the ndarray kernels no longer zero). A Keep takes its mask through Out::Mask; with Out::None it is answered only by a single population-wide tile (TerminalNeedsOut otherwise). The differential suite now runs every program shape tiled AND single-tile and diffs both against the oracle. Pred::EqU32Via { fk, key, v } — the join filter in factored form (ndarray eq_u32_via_to_mask): foreign.lanes[key][fk[i]] == v in one pass, no predicate plane over the foreign table, no gathered mask. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/lance-graph-mask-risc/src/exec.rs | 659 +++++++++++------- crates/lance-graph-mask-risc/src/ir.rs | 13 +- crates/lance-graph-mask-risc/src/lib.rs | 11 +- crates/lance-graph-mask-risc/src/reference.rs | 57 +- .../tests/differential.rs | 40 +- crates/lance-graph-mask-risc/tests/foreign.rs | 140 +++- 6 files changed, 658 insertions(+), 262 deletions(-) diff --git a/crates/lance-graph-mask-risc/src/exec.rs b/crates/lance-graph-mask-risc/src/exec.rs index fff632114..dd96632a1 100644 --- a/crates/lance-graph-mask-risc/src/exec.rs +++ b/crates/lance-graph-mask-risc/src/exec.rs @@ -24,8 +24,8 @@ use ndarray::simd::{ blend_i32, eq_i32_to_mask, eq_i32_to_mask_under, eq_u32_to_mask, eq_u32_to_mask_under, - ge_i32_to_mask, ge_i32_to_mask_under, gt_i32_to_mask, gt_i32_to_mask_under, le_i32_to_mask, - le_i32_to_mask_under, lt_i32_to_mask, lt_i32_to_mask_under, mask_all, mask_and, + eq_u32_via_to_mask, ge_i32_to_mask, ge_i32_to_mask_under, gt_i32_to_mask, gt_i32_to_mask_under, + le_i32_to_mask, le_i32_to_mask_under, lt_i32_to_mask, lt_i32_to_mask_under, mask_all, mask_and, mask_and_assign, mask_andnot, mask_andnot_assign, mask_any, mask_gather_u32, mask_not, mask_not_assign, mask_or, mask_or_assign, mask_scatter_or_u32, mask_set_range, mask_xor, mask_xor_assign, masked_group_sum_i32, masked_group_sum_i32_via, masked_max_i32, @@ -79,6 +79,18 @@ impl Store<'_> { /// buffer for [`Scratch::over`] asks this rather than reimplementing the /// layout, which is the only way the two can never disagree. `None` on /// overflow, never a wrapped answer that would silently under-allocate. +/// Words per scratch slot the default constructors carve. The executor runs a +/// program one tile at a time (see [`execute_into`]), so execution state is +/// `slots × TILE_WORDS` words however many rows the planes hold — 8 words = +/// 512 rows = one full-width vector per facade call. +pub const TILE_WORDS: usize = 8; + +/// The slot width [`Scratch::for_program`] / [`Scratch::over_for_program`] +/// carve for `n_rows`: one tile, or the whole (shorter) population. +pub fn tile_words_for(n_rows: usize) -> usize { + words_for(n_rows).min(TILE_WORDS) +} + pub fn scratch_words_for(words: usize, slots: usize) -> Option { slots.checked_mul(words)?.checked_add(slots.div_ceil(64)) } @@ -157,7 +169,8 @@ impl Scratch<'static> { } } - /// Allocate exactly what `program` needs over `n_rows` rows. + /// Allocate exactly what `program` needs over `n_rows` rows: `scratch_slots` + /// slots of ONE TILE each ([`tile_words_for`]), never the population. /// /// Fallible, and deliberately so. `Operand::Scratch` is a `u16`, so no /// program can ADDRESS more than [`MAX_SCRATCH_SLOTS`] slots; a larger @@ -176,7 +189,10 @@ impl Scratch<'static> { } // `scratch_slots` is a `u32` count; a program naming slot `u16::MAX` // needs 65,536 buffers, which fits `usize` on every supported target. - Ok(Self::new(words_for(n_rows), program.scratch_slots as usize)) + Ok(Self::new( + tile_words_for(n_rows), + program.scratch_slots as usize, + )) } } @@ -232,7 +248,7 @@ impl<'a> Scratch<'a> { declared: program.scratch_slots, }); } - Self::over(buf, words_for(n_rows), program.scratch_slots as usize) + Self::over(buf, tile_words_for(n_rows), program.scratch_slots as usize) } /// Words per slot. @@ -245,7 +261,9 @@ impl<'a> Scratch<'a> { self.slots } - /// Borrow slot `i` (the way a caller reads back a [`Terminal::Keep`] result). + /// Borrow slot `i` — ONE tile wide; after a tiled run it holds the LAST + /// tile only. A [`Terminal::Keep`] result is read from its [`Out::Mask`], + /// or from this slot when the scratch is a single population-wide tile. pub fn slot(&self, i: u16) -> Option<&[u64]> { let i = usize::from(i); if i >= self.slots { @@ -377,6 +395,7 @@ fn two_input( op: fn(&[u64], &[u64], &mut [u64]), assign: fn(&mut [u64], &[u64]), imm: u8, + t: Tile, ) { let d = Operand::Scratch(dst); // Every two-input table is EVEN (`f(0,0,0) = 0`), which is why this @@ -386,19 +405,20 @@ fn two_input( debug_assert!(imm & 1 == 0, "two-input table {imm:#04x} is odd"); let commutative = remap_imm(imm, [1, 0, 2]) == imm; let (x, rest) = s.split(dst); + let x = &mut x[..t.words]; if a == d && b == d { - ternlog_self(remap_imm(imm, [0, 0, 0]), x, planes.n_rows); + ternlog_self(remap_imm(imm, [0, 0, 0]), x, t.rows); } else if a == d { - assign(x, read(planes, &rest, b)); + assign(x, read(planes, &rest, b, t)); } else if b == d { - let aa = read(planes, &rest, a); + let aa = read(planes, &rest, a, t); if commutative { assign(x, aa); } else { ternlog_dispatch_assign(remap_imm(imm, [1, 0, 0]), x, aa, aa); } } else { - op(read(planes, &rest, a), read(planes, &rest, b), x); + op(read(planes, &rest, a, t), read(planes, &rest, b, t), x); } } @@ -419,43 +439,64 @@ fn ternlog_self(imm: u8, x: &mut [u64], n_rows: usize) { } } +/// One tile of the population: words `[w0, w0 + words)` of every mask and +/// scratch slot, rows `[r0, r0 + rows)` of every lane. The executor walks a +/// program tile by tile, so nothing it writes is ever population-sized: the +/// scratch is `slots × tile` words whatever `n_rows` is, and the only +/// population-sized writes are the DEMANDED sinks (`Out`). The last tile is +/// short when `words` is not a multiple of the slot width; `rows` is then +/// exactly the rows those words carry, so every tail law holds per tile. +#[derive(Debug, Clone, Copy)] +struct Tile { + w0: usize, + words: usize, + r0: usize, + rows: usize, +} + /// Borrow an operand for reading (the scratch arena with `dst` already taken -/// out, so a read of `dst`'s own slot here is a bug the aliasing arms prevent). -fn read<'a>(planes: &Planes<'a>, s: &Slots<'a>, o: Operand) -> &'a [u64] { +/// out, so a read of `dst`'s own slot here is a bug the aliasing arms prevent), +/// restricted to tile `t`. +fn read<'a>(planes: &Planes<'a>, s: &Slots<'a>, o: Operand, t: Tile) -> &'a [u64] { match o { - Operand::Plane(i) => planes.masks[usize::from(i)], - Operand::Scratch(i) => s.get(usize::from(i)), + Operand::Plane(i) => &planes.masks[usize::from(i)][t.w0..t.w0 + t.words], + Operand::Scratch(i) => &s.get(usize::from(i))[..t.words], } } -fn lane_i32<'a>(planes: &Planes<'a>, lane: u16) -> &'a [i32] { +/// Rows `[r0, r0 + rows)` of `v`, or the empty slice when the lane is short — +/// unreachable after `validate`, but a slice out of range would panic where +/// an empty lane makes every facade call a no-op of the right shape. +fn rows_of(v: &[T], t: Tile) -> &[T] { + v.get(t.r0..t.r0 + t.rows).unwrap_or(&[]) +} + +fn lane_i32<'a>(planes: &Planes<'a>, lane: u16, t: Tile) -> &'a [i32] { match planes.lanes[usize::from(lane)] { - LaneRef::I32(v) => v, - // unreachable after `validate`, but the type system does not know - // that: an empty lane makes every facade call a no-op of the right - // shape rather than a panic. + LaneRef::I32(v) => rows_of(v, t), _ => &[], } } -fn lane_u32<'a>(planes: &Planes<'a>, lane: u16) -> &'a [u32] { +fn lane_u32<'a>(planes: &Planes<'a>, lane: u16, t: Tile) -> &'a [u32] { match planes.lanes[usize::from(lane)] { - LaneRef::U32(v) => v, + LaneRef::U32(v) => rows_of(v, t), _ => &[], } } -fn lane_u64<'a>(planes: &Planes<'a>, lane: u16) -> &'a [u64] { +fn lane_u64<'a>(planes: &Planes<'a>, lane: u16, t: Tile) -> &'a [u64] { match planes.lanes[usize::from(lane)] { - LaneRef::U64(v) => v, + LaneRef::U64(v) => rows_of(v, t), _ => &[], } } /// Borrow a foreign `U32` lane for reading — [`lane_u32`]'s twin over /// [`Foreign::lanes`], a SEPARATE address space from `planes.lanes` (see -/// [`Foreign`]'s own doc). Unreachable after `validate` on a well-typed -/// program, same fallback discipline as `lane_i32`/`lane_u32`/`lane_u64`. +/// [`Foreign`]'s own doc). WHOLE, never tiled: it is addressed through a +/// foreign key, not by this population's row. Unreachable after `validate` +/// on a well-typed program, same fallback discipline as the lane helpers. fn foreign_lane_u32<'a>(foreign: &Foreign<'a>, key: u16) -> &'a [u32] { match foreign.lanes[usize::from(key)] { LaneRef::U32(v) => v, @@ -463,47 +504,65 @@ fn foreign_lane_u32<'a>(foreign: &Foreign<'a>, key: u16) -> &'a [u32] { } } -/// One predicate pass into `dst`: the ungated facade member, or the `_under` -/// member when a gate is present (cost then follows the gate's live words). +/// One predicate pass into `dst` (one tile of it): the ungated facade member, +/// or the `_under` member when a gate is present (cost then follows the +/// gate's live words). fn run_pred<'a>( planes: &Planes<'a>, + foreign: &Foreign<'a>, s: &Slots<'a>, pred: Pred, under: Option, dst: &mut [u64], + t: Tile, ) { match (pred, under) { - (Pred::GtI32 { lane, t }, None) => gt_i32_to_mask(lane_i32(planes, lane), t, dst), - (Pred::GtI32 { lane, t }, Some(u)) => { - gt_i32_to_mask_under(lane_i32(planes, lane), t, read(planes, s, u), dst) + // The join filter in factored form: the foreign lane is read WHOLE + // (addressed by the key), this table's fk one tile at a time. The + // gate is applied after, as for `Range` — there is no `_under` twin + // and one AND over a tile costs less than a second facade word. + (Pred::EqU32Via { fk, key, v }, under) => { + eq_u32_via_to_mask( + lane_u32(planes, fk, t), + foreign_lane_u32(foreign, key), + v, + dst, + ); + if let Some(u) = under { + mask_and_assign(dst, read(planes, s, u, t)); + } } - (Pred::LtI32 { lane, t }, None) => lt_i32_to_mask(lane_i32(planes, lane), t, dst), - (Pred::LtI32 { lane, t }, Some(u)) => { - lt_i32_to_mask_under(lane_i32(planes, lane), t, read(planes, s, u), dst) + (Pred::GtI32 { lane, t: v }, None) => gt_i32_to_mask(lane_i32(planes, lane, t), v, dst), + (Pred::GtI32 { lane, t: v }, Some(u)) => { + gt_i32_to_mask_under(lane_i32(planes, lane, t), v, read(planes, s, u, t), dst) } - (Pred::GeI32 { lane, t }, None) => ge_i32_to_mask(lane_i32(planes, lane), t, dst), - (Pred::GeI32 { lane, t }, Some(u)) => { - ge_i32_to_mask_under(lane_i32(planes, lane), t, read(planes, s, u), dst) + (Pred::LtI32 { lane, t: v }, None) => lt_i32_to_mask(lane_i32(planes, lane, t), v, dst), + (Pred::LtI32 { lane, t: v }, Some(u)) => { + lt_i32_to_mask_under(lane_i32(planes, lane, t), v, read(planes, s, u, t), dst) } - (Pred::LeI32 { lane, t }, None) => le_i32_to_mask(lane_i32(planes, lane), t, dst), - (Pred::LeI32 { lane, t }, Some(u)) => { - le_i32_to_mask_under(lane_i32(planes, lane), t, read(planes, s, u), dst) + (Pred::GeI32 { lane, t: v }, None) => ge_i32_to_mask(lane_i32(planes, lane, t), v, dst), + (Pred::GeI32 { lane, t: v }, Some(u)) => { + ge_i32_to_mask_under(lane_i32(planes, lane, t), v, read(planes, s, u, t), dst) } - (Pred::EqI32 { lane, v }, None) => eq_i32_to_mask(lane_i32(planes, lane), v, dst), + (Pred::LeI32 { lane, t: v }, None) => le_i32_to_mask(lane_i32(planes, lane, t), v, dst), + (Pred::LeI32 { lane, t: v }, Some(u)) => { + le_i32_to_mask_under(lane_i32(planes, lane, t), v, read(planes, s, u, t), dst) + } + (Pred::EqI32 { lane, v }, None) => eq_i32_to_mask(lane_i32(planes, lane, t), v, dst), (Pred::EqI32 { lane, v }, Some(u)) => { - eq_i32_to_mask_under(lane_i32(planes, lane), v, read(planes, s, u), dst) + eq_i32_to_mask_under(lane_i32(planes, lane, t), v, read(planes, s, u, t), dst) } - (Pred::NeI32 { lane, v }, None) => ne_i32_to_mask(lane_i32(planes, lane), v, dst), + (Pred::NeI32 { lane, v }, None) => ne_i32_to_mask(lane_i32(planes, lane, t), v, dst), (Pred::NeI32 { lane, v }, Some(u)) => { - ne_i32_to_mask_under(lane_i32(planes, lane), v, read(planes, s, u), dst) + ne_i32_to_mask_under(lane_i32(planes, lane, t), v, read(planes, s, u, t), dst) } - (Pred::EqU32 { lane, v }, None) => eq_u32_to_mask(lane_u32(planes, lane), v, dst), + (Pred::EqU32 { lane, v }, None) => eq_u32_to_mask(lane_u32(planes, lane, t), v, dst), (Pred::EqU32 { lane, v }, Some(u)) => { - eq_u32_to_mask_under(lane_u32(planes, lane), v, read(planes, s, u), dst) + eq_u32_to_mask_under(lane_u32(planes, lane, t), v, read(planes, s, u, t), dst) } - (Pred::NeU32 { lane, v }, None) => ne_u32_to_mask(lane_u32(planes, lane), v, dst), + (Pred::NeU32 { lane, v }, None) => ne_u32_to_mask(lane_u32(planes, lane, t), v, dst), (Pred::NeU32 { lane, v }, Some(u)) => { - ne_u32_to_mask_under(lane_u32(planes, lane), v, read(planes, s, u), dst) + ne_u32_to_mask_under(lane_u32(planes, lane, t), v, read(planes, s, u, t), dst) } ( Pred::MatchU32 { @@ -512,7 +571,7 @@ fn run_pred<'a>( care, }, None, - ) => ternary_match_u32_to_mask(lane_u32(planes, lane), pattern, care, dst), + ) => ternary_match_u32_to_mask(lane_u32(planes, lane, t), pattern, care, dst), ( Pred::MatchU32 { lane, @@ -521,10 +580,10 @@ fn run_pred<'a>( }, Some(u), ) => ternary_match_u32_to_mask_under( - lane_u32(planes, lane), + lane_u32(planes, lane, t), pattern, care, - read(planes, s, u), + read(planes, s, u, t), dst, ), ( @@ -534,7 +593,7 @@ fn run_pred<'a>( care, }, None, - ) => ternary_match_u64_to_mask(lane_u64(planes, lane), pattern, care, dst), + ) => ternary_match_u64_to_mask(lane_u64(planes, lane, t), pattern, care, dst), ( Pred::MatchU64 { lane, @@ -543,18 +602,22 @@ fn run_pred<'a>( }, Some(u), ) => ternary_match_u64_to_mask_under( - lane_u64(planes, lane), + lane_u64(planes, lane, t), pattern, care, - read(planes, s, u), + read(planes, s, u, t), dst, ), - // `lo <= hi <= n_rows` was validated, so the bounds are in-range for - // the scratch words and `mask_set_range`'s own asserts cannot fire. - (Pred::Range { lo, hi }, None) => mask_set_range(dst, lo as usize, hi as usize), - (Pred::Range { lo, hi }, Some(u)) => { - mask_set_range(dst, lo as usize, hi as usize); - mask_and_assign(dst, read(planes, s, u)); + // `lo <= hi <= n_rows` was validated; clipped to this tile the range + // is in-range for the tile's words and `mask_set_range`'s own asserts + // cannot fire. A range that misses the tile entirely clips to an empty + // one, which clears the tile — the correct answer for it. + (Pred::Range { lo, hi }, under) => { + let clip = |r: u32| (r as usize).clamp(t.r0, t.r0 + t.rows) - t.r0; + mask_set_range(dst, clip(lo), clip(hi)); + if let Some(u) = under { + mask_and_assign(dst, read(planes, s, u, t)); + } } } } @@ -582,20 +645,40 @@ pub fn execute( ) } +/// Fold one tile's `Option` reduction into the running one. +fn fold_opt(acc: Option, tile: Option, f: fn(i32, i32) -> i32) -> Option { + match (acc, tile) { + (Some(a), Some(b)) => Some(f(a, b)), + (a, None) => a, + (None, b) => b, + } +} + /// Run `program` over `planes` with the caller's `scratch`, resolving any -/// [`MaskOp::Gather`] against `foreign`; `out` is the destination a -/// [`Terminal::BlendI32`] / [`Terminal::ScatterOrU32`] / -/// [`Terminal::GroupSumI32`] writes (every other terminal ignores it — an -/// `Out` of the wrong shape for the terminal that IS present is refused by -/// [`validate`], never silently accepted and silently not written). -/// Validation is total and happens before any result write, so an `Err` -/// leaves the scratch slots and `out` untouched. +/// [`MaskOp::Gather`] / [`Terminal::GroupSumViaI32`] against `foreign`; +/// `out` is the DEMANDED sink — the destination a [`Terminal::BlendI32`] / +/// [`Terminal::ScatterOrU32`] / [`Terminal::GroupSumI32`] / +/// [`Terminal::GroupSumViaI32`] / [`Terminal::Keep`] writes (every other +/// terminal ignores it — an `Out` of the wrong shape for the terminal that IS +/// present is refused by [`validate`], never silently accepted and silently +/// not written). Validation is total and happens before any result write, so +/// an `Err` leaves the scratch slots and `out` untouched. +/// +/// **Execution is tiled.** The program runs once per `scratch.words()`-word +/// tile of the population; every op writes at most one tile of its slot, and +/// the terminal folds each tile into O(1) accumulators or into `out`. Nothing +/// population-sized is written except `out` itself — a mask exists as a +/// whole only where a caller demanded it ([`Out::Mask`]). A caller who carves +/// a scratch `words_for(n_rows)` wide gets one tile, i.e. the old +/// whole-population behaviour, and may then read a [`Terminal::Keep`] result +/// from its slot with `out: Out::None`; under a narrower scratch `Keep` +/// requires `Out::Mask` (`TerminalNeedsOut`). pub fn execute_into( program: &Program, planes: &Planes<'_>, foreign: &Foreign<'_>, scratch: &mut Scratch<'_>, - out: Out<'_>, + mut out: Out<'_>, ) -> Result { // BEFORE the capacity check, not after: an over-declared count is a lie // about the PROGRAM, and the caller's buffer is irrelevant to it. Checked @@ -616,10 +699,11 @@ pub fn execute_into( }); } let words = words_for(planes.n_rows); - if scratch.words != words { + let tw = scratch.words; + if words > 0 && tw == 0 { return Err(ExecError::ScratchWords { - expected: words, - found: scratch.words, + expected: 1, + found: 0, }); } validate( @@ -629,194 +713,265 @@ pub fn execute_into( out_shape(&out), scratch.written_mut(), )?; + if matches!(program.terminal, Terminal::Keep { .. }) && matches!(out, Out::None) && tw < words { + return Err(ExecError::TerminalNeedsOut { what: "Keep" }); + } + // The sinks that ACCUMULATE across tiles start from zero here, once. The + // sink is the requested result, so this is a write the law permits; the + // facade kernels themselves never zero a destination they only add to. + match (&program.terminal, &mut out) { + (Terminal::ScatterOrU32 { .. }, Out::Mask(o)) => o.fill(0), + (Terminal::GroupSumI32 { .. } | Terminal::GroupSumViaI32 { .. }, Out::I64(o)) => o.fill(0), + _ => {} + } let n_rows = planes.n_rows; - - for op in &program.ops { - match *op { - MaskOp::Pred { pred, under, dst } => { - let (d, rest) = scratch.split(dst); - run_pred(planes, &rest, pred, under, d); - } - MaskOp::And { a, b, dst } => two_input( - planes, - scratch, - a, - b, - dst, - mask_and, - mask_and_assign, - AND_IMM, - ), - MaskOp::Or { a, b, dst } => { - two_input(planes, scratch, a, b, dst, mask_or, mask_or_assign, OR_IMM) - } - MaskOp::Xor { a, b, dst } => two_input( - planes, - scratch, - a, - b, - dst, - mask_xor, - mask_xor_assign, - XOR_IMM, - ), - MaskOp::AndNot { a, b, dst } => two_input( - planes, - scratch, - a, - b, - dst, - mask_andnot, - mask_andnot_assign, - ANDNOT_IMM, - ), - MaskOp::Not { a, dst } => { - let (d, rest) = scratch.split(dst); - if a == Operand::Scratch(dst) { - mask_not_assign(d, n_rows); - } else { - mask_not(read(planes, &rest, a), n_rows, d); + let mut count = 0usize; + let mut any = false; + let mut all = true; + let mut sum = 0i64; + let mut min: Option = None; + let mut max: Option = None; + + let mut w0 = 0usize; + while w0 < words { + let tws = tw.min(words - w0); + let r0 = w0 * 64; + let t = Tile { + w0, + words: tws, + r0, + rows: (n_rows - r0).min(tws * 64), + }; + for op in &program.ops { + match *op { + MaskOp::Pred { pred, under, dst } => { + let (d, rest) = scratch.split(dst); + run_pred(planes, foreign, &rest, pred, under, &mut d[..t.words], t); } - } - MaskOp::Ternlog { imm, a, b, c, dst } => { - let d = Operand::Scratch(dst); - let (x, rest) = scratch.split(dst); - if a != d && b != d && c != d { - ternlog_dispatch( - imm, - read(planes, &rest, a), - read(planes, &rest, b), - read(planes, &rest, c), - x, - ); - } else { - // `dst` is an input: the in-place form takes it as `x`, - // the remaining distinct operands become `y` (and `z`), - // and the table is re-indexed to match. - let mut map = [0u8; 3]; - let mut others: [Option; 2] = [None, None]; - let mut n = 0usize; - for (i, o) in [a, b, c].into_iter().enumerate() { - if o == d { - map[i] = 0; - } else if let Some(pos) = others[..n].iter().position(|&p| p == Some(o)) { - map[i] = pos as u8 + 1; - } else { - others[n] = Some(o); - map[i] = n as u8 + 1; - n += 1; - } + MaskOp::And { a, b, dst } => two_input( + planes, + scratch, + a, + b, + dst, + mask_and, + mask_and_assign, + AND_IMM, + t, + ), + MaskOp::Or { a, b, dst } => two_input( + planes, + scratch, + a, + b, + dst, + mask_or, + mask_or_assign, + OR_IMM, + t, + ), + MaskOp::Xor { a, b, dst } => two_input( + planes, + scratch, + a, + b, + dst, + mask_xor, + mask_xor_assign, + XOR_IMM, + t, + ), + MaskOp::AndNot { a, b, dst } => two_input( + planes, + scratch, + a, + b, + dst, + mask_andnot, + mask_andnot_assign, + ANDNOT_IMM, + t, + ), + MaskOp::Not { a, dst } => { + let (d, rest) = scratch.split(dst); + let d = &mut d[..t.words]; + if a == Operand::Scratch(dst) { + mask_not_assign(d, t.rows); + } else { + mask_not(read(planes, &rest, a, t), t.rows, d); } - let imm2 = remap_imm(imm, map); - match (others[0], others[1]) { - (Some(y), Some(z)) => ternlog_dispatch_assign( - imm2, + } + MaskOp::Ternlog { imm, a, b, c, dst } => { + let d = Operand::Scratch(dst); + let (x, rest) = scratch.split(dst); + let x = &mut x[..t.words]; + if a != d && b != d && c != d { + ternlog_dispatch( + imm, + read(planes, &rest, a, t), + read(planes, &rest, b, t), + read(planes, &rest, c, t), x, - read(planes, &rest, y), - read(planes, &rest, z), - ), - (Some(y), None) => { - let yy = read(planes, &rest, y); - ternlog_dispatch_assign(imm2, x, yy, yy) + ); + } else { + // `dst` is an input: the in-place form takes it as + // `x`, the remaining distinct operands become `y` + // (and `z`), and the table is re-indexed to match. + let mut map = [0u8; 3]; + let mut others: [Option; 2] = [None, None]; + let mut n = 0usize; + for (i, o) in [a, b, c].into_iter().enumerate() { + if o == d { + map[i] = 0; + } else if let Some(pos) = others[..n].iter().position(|&p| p == Some(o)) + { + map[i] = pos as u8 + 1; + } else { + others[n] = Some(o); + map[i] = n as u8 + 1; + n += 1; + } } - _ => ternlog_self(imm2, x, n_rows), + let imm2 = remap_imm(imm, map); + match (others[0], others[1]) { + (Some(y), Some(z)) => ternlog_dispatch_assign( + imm2, + x, + read(planes, &rest, y, t), + read(planes, &rest, z, t), + ), + (Some(y), None) => { + let yy = read(planes, &rest, y, t); + ternlog_dispatch_assign(imm2, x, yy, yy) + } + _ => ternlog_self(imm2, x, t.rows), + } + } + if imm & 1 == 1 { + clear_tail(x, t.rows); } } - if imm & 1 == 1 { - clear_tail(x, n_rows); + MaskOp::Gather { + lane, + foreign: fidx, + dst, + } => { + // No aliasing shape to consider: unlike every other op, + // `Gather` never reads `dst` as an input — it only writes + // it — so there is no `dst == a` case to route to an + // in-place facade form. The foreign plane is read WHOLE + // (it is addressed by the key, not by this tile's rows). + let (d, _rest) = scratch.split(dst); + let fp = &foreign.planes[usize::from(fidx)]; + mask_gather_u32( + fp.words, + fp.rows, + lane_u32(planes, lane, t), + &mut d[..t.words], + ); } } - MaskOp::Gather { - lane, - foreign: fidx, - dst, - } => { - // No aliasing shape to consider: unlike every other op, - // `Gather` never reads `dst` as an input — it only writes it - // — so there is no `dst == a` case to route to an in-place - // facade form. - let (d, _rest) = scratch.split(dst); - let fp = &foreign.planes[usize::from(fidx)]; - mask_gather_u32(fp.words, fp.rows, lane_u32(planes, lane), d); - } } - } - let all = scratch.all(); - Ok(match program.terminal { - Terminal::Count { mask } => { - Value::Count(popcount_batch_u64(read(planes, &all, mask)) as usize) - } - Terminal::Any { mask } => Value::Bool(mask_any(read(planes, &all, mask))), - Terminal::All { mask } => Value::Bool(mask_all(read(planes, &all, mask), n_rows)), - Terminal::MaskedSumI32 { mask, lane } => Value::SumI64(masked_sum_i32( - lane_i32(planes, lane), - read(planes, &all, mask), - )), - Terminal::MaskedMinI32 { mask, lane } => Value::OptI32(masked_min_i32( - lane_i32(planes, lane), - read(planes, &all, mask), - )), - Terminal::MaskedMaxI32 { mask, lane } => Value::OptI32(masked_max_i32( - lane_i32(planes, lane), - read(planes, &all, mask), - )), - Terminal::BlendI32 { mask, then, els } => { - // `validate` already refused a missing or mis-shaped `out`. - if let Out::I32(o) = out { - blend_i32( - read(planes, &all, mask), - lane_i32(planes, then), - lane_i32(planes, els), - o, - ); + let slots = scratch.all(); + match program.terminal { + Terminal::Count { mask } => { + count += popcount_batch_u64(read(planes, &slots, mask, t)) as usize; } - Value::Blended - } - Terminal::ScatterOrU32 { - mask, - lane, - out_rows, - } => { - // `validate` already refused a missing or mis-sized `out`. - if let Out::Mask(o) = out { - mask_scatter_or_u32( - read(planes, &all, mask), - lane_u32(planes, lane), - o, - out_rows as usize, - ); + Terminal::Any { mask } => any |= mask_any(read(planes, &slots, mask, t)), + Terminal::All { mask } => all &= mask_all(read(planes, &slots, mask, t), t.rows), + Terminal::MaskedSumI32 { mask, lane } => { + sum += masked_sum_i32(lane_i32(planes, lane, t), read(planes, &slots, mask, t)); } - Value::Scattered - } - Terminal::GroupSumI32 { mask, key, val } => { - // `validate` already refused a missing or too-small `out`. - if let Out::I64(o) = out { - masked_group_sum_i32( - read(planes, &all, mask), - lane_u32(planes, key), - lane_i32(planes, val), - o, + Terminal::MaskedMinI32 { mask, lane } => { + min = fold_opt( + min, + masked_min_i32(lane_i32(planes, lane, t), read(planes, &slots, mask, t)), + i32::min, ); } - Value::GroupSummed - } - Terminal::GroupSumViaI32 { mask, fk, key, val } => { - // `validate` already refused a missing/too-small `out`, a - // wrong-width `fk`/`val`, and an out-of-range or wrong-width - // foreign `key` — one delegation, same as every other terminal - // (law L3). - if let Out::I64(o) = out { - masked_group_sum_i32_via( - read(planes, &all, mask), - lane_u32(planes, fk), - foreign_lane_u32(foreign, key), - lane_i32(planes, val), - o, + Terminal::MaskedMaxI32 { mask, lane } => { + max = fold_opt( + max, + masked_max_i32(lane_i32(planes, lane, t), read(planes, &slots, mask, t)), + i32::max, ); } - Value::GroupSummed + Terminal::BlendI32 { mask, then, els } => { + // `validate` already refused a missing or mis-shaped `out`. + if let Out::I32(o) = &mut out { + blend_i32( + read(planes, &slots, mask, t), + lane_i32(planes, then, t), + lane_i32(planes, els, t), + &mut o[t.r0..t.r0 + t.rows], + ); + } + } + Terminal::ScatterOrU32 { + mask, + lane, + out_rows, + } => { + // `validate` already refused a missing or mis-sized `out`; + // the kernel ORs into it, tile after tile. + if let Out::Mask(o) = &mut out { + mask_scatter_or_u32( + read(planes, &slots, mask, t), + lane_u32(planes, lane, t), + o, + out_rows as usize, + ); + } + } + Terminal::GroupSumI32 { mask, key, val } => { + // `validate` already refused a missing or too-small `out`; + // the kernel adds into it, tile after tile. + if let Out::I64(o) = &mut out { + masked_group_sum_i32( + read(planes, &slots, mask, t), + lane_u32(planes, key, t), + lane_i32(planes, val, t), + o, + ); + } + } + Terminal::GroupSumViaI32 { mask, fk, key, val } => { + // `validate` already refused a missing/too-small `out`, a + // wrong-width `fk`/`val`, and an out-of-range or wrong-width + // foreign `key` — one delegation per tile (law L3). + if let Out::I64(o) = &mut out { + masked_group_sum_i32_via( + read(planes, &slots, mask, t), + lane_u32(planes, fk, t), + foreign_lane_u32(foreign, key), + lane_i32(planes, val, t), + o, + ); + } + } + Terminal::Keep { mask } => { + // The demanded mask, one tile at a time. With `Out::None` the + // scratch is single-tile (checked above) and the slot IS the + // result. + if let Out::Mask(o) = &mut out { + o[t.w0..t.w0 + t.words].copy_from_slice(read(planes, &slots, mask, t)); + } + } } + w0 += tws; + } + + Ok(match program.terminal { + Terminal::Count { .. } => Value::Count(count), + Terminal::Any { .. } => Value::Bool(any), + Terminal::All { .. } => Value::Bool(all), + Terminal::MaskedSumI32 { .. } => Value::SumI64(sum), + Terminal::MaskedMinI32 { .. } => Value::OptI32(min), + Terminal::MaskedMaxI32 { .. } => Value::OptI32(max), + Terminal::BlendI32 { .. } => Value::Blended, + Terminal::ScatterOrU32 { .. } => Value::Scattered, + Terminal::GroupSumI32 { .. } | Terminal::GroupSumViaI32 { .. } => Value::GroupSummed, Terminal::Keep { mask } => Value::Mask(mask), }) } @@ -903,11 +1058,11 @@ mod borrowed_scratch_tests { lanes: &lanes, }; - let need = scratch_words_for(words_for(n), p.scratch_slots as usize).unwrap(); + let need = scratch_words_for(tile_words_for(n), p.scratch_slots as usize).unwrap(); // twice the room, every excess word poisoned let mut buf = vec![u64::MAX; need * 2 + 16]; let excess_start = need; - let mut s = Scratch::over(&mut buf, words_for(n), p.scratch_slots as usize) + let mut s = Scratch::over(&mut buf, tile_words_for(n), p.scratch_slots as usize) .expect("buffer is long enough"); let got = execute(&p, &planes, &mut s, None).expect("runs"); let got_slots: Vec> = (0..s.slots()) @@ -1349,17 +1504,25 @@ mod tests { execute(&p, &planes, &mut small, None), Err(ExecError::ScratchTooSmall { need: 4, have: 2 }) ); - let mut wrong = Scratch::new(1, 4); + let mut wrong = Scratch::new(0, 4); assert_eq!( execute(&p, &planes, &mut wrong, None), Err(ExecError::ScratchWords { - expected: 2, - found: 1 + expected: 1, + found: 0 }) ); - assert!( - wrong.slot(3).is_some_and(|w| w.iter().all(|&x| x == 0)), - "nothing was written" + // A NARROW scratch is not wrong — it is a tile width. One word over + // 70 rows runs as two tiles and answers exactly what two words do. + let mut narrow = Scratch::new(1, 4); + let mut wide = Scratch::new(2, 4); + assert_eq!( + execute(&p, &planes, &mut narrow, None), + execute(&p, &planes, &mut wide, None) + ); + assert_eq!( + execute(&p, &planes, &mut narrow, None), + Ok(Value::Bool(true)) ); } } diff --git a/crates/lance-graph-mask-risc/src/ir.rs b/crates/lance-graph-mask-risc/src/ir.rs index 75e8a4fd5..9eff5937e 100644 --- a/crates/lance-graph-mask-risc/src/ir.rs +++ b/crates/lance-graph-mask-risc/src/ir.rs @@ -122,6 +122,17 @@ pub enum Pred { MatchU32 { lane: u16, pattern: u32, care: u32 }, /// `((lane[i] ^ pattern) & care) == 0` over a `u64` lane. MatchU64 { lane: u16, pattern: u64, care: u64 }, + /// `foreign.lanes[key][fk[i]] == v` — an equality predicate on the OTHER + /// table, evaluated THROUGH this table's foreign key, row by row, in one + /// facade pass (`ndarray::simd::eq_u32_via_to_mask`). This is the join + /// filter in factored form: `line WHERE partner.country = 3` reads + /// `country[partner_id[i]]` directly, so neither a predicate plane over + /// `partner` nor a gathered mask over `line` ever exists — the same + /// address indirection [`Terminal::GroupSumViaI32`] uses for its key, + /// applied to a predicate. Zero fallback: an `fk` that names no foreign + /// row does not match. `fk` is a `U32` lane of THIS table; `key` indexes + /// [`Foreign::lanes`] and must be `U32`. + EqU32Via { fk: u16, key: u16, v: u32 }, /// `lo <= i < hi` — a predicate on the ROW INDEX, reading no lane. /// /// The contiguous-range write. On an address-ordered lane an address @@ -199,7 +210,7 @@ pub enum MaskOp { /// `dst[i] = foreign.planes[foreign].words[lane_u32[i]]` — the fk /// SEMIJOIN: `dst` is set for row `i` exactly when `lane`'s value names a /// row that survives on the foreign table. Out-of-range is the - /// zero-fallback ([`crate::exec::Gather`]'s underlying facade primitive + /// zero-fallback ([`MaskOp::Gather`]'s underlying facade primitive /// contract), never an error: `line WHERE EXISTS partner p ON p.rid = /// line.partner_id AND ` is `Gather{lane: partner_id, foreign: /// }` — the fk gather. diff --git a/crates/lance-graph-mask-risc/src/lib.rs b/crates/lance-graph-mask-risc/src/lib.rs index 7b79aa4b7..4d7a1fbb5 100644 --- a/crates/lance-graph-mask-risc/src/lib.rs +++ b/crates/lance-graph-mask-risc/src/lib.rs @@ -13,7 +13,16 @@ //! word lowers to is its question, answered by its own parity tests, and //! running this suite under another realization would test ndarray through a //! proxy rather than test this crate. `tests/no_alloc.rs` pins the -//! zero-allocation law. `hop` (PR5) LANDED as [`MaskOp::Gather`] (the fk +//! zero-allocation law. **Execution is TILED** (2026-09-21, operator ruling: +//! an intermediate population is forbidden when the next fold can consume the +//! projection directly — allocation is irrelevant, a caller-owned bitmap is +//! still materialisation): [`exec::execute_into`] runs a program one +//! [`exec::TILE_WORDS`]-word tile at a time, so execution state is +//! `slots × 8` words whatever `n_rows` is, and the only population-sized +//! writes are the DEMANDED sinks ([`Out`]). A [`Terminal::Keep`] therefore +//! takes its mask through [`Out::Mask`]; `tests/differential.rs` proves every +//! program shape answers identically tiled and single-tile. `hop` (PR5) +//! LANDED as [`MaskOp::Gather`] (the fk //! semijoin, over [`ir::Foreign`]) and [`Terminal::ScatterOrU32`] (the //! one-to-many hop back); [`Terminal::GroupSumI32`] is the one-terminal //! `GROUP BY … SUM`, and [`Terminal::GroupSumViaI32`] is its fk-keyed diff --git a/crates/lance-graph-mask-risc/src/reference.rs b/crates/lance-graph-mask-risc/src/reference.rs index 5814e7043..2344fdfe5 100644 --- a/crates/lance-graph-mask-risc/src/reference.rs +++ b/crates/lance-graph-mask-risc/src/reference.rs @@ -75,6 +75,9 @@ fn pred_lane_and_kind(pred: Pred) -> Option<(u16, LaneKind)> { (lane, LaneKind::U32) } Pred::MatchU64 { lane, .. } => (lane, LaneKind::U64), + // The fk is this table's lane; the foreign lane is checked separately + // (`validate`'s `Pred` arm), against the OTHER address space. + Pred::EqU32Via { fk, .. } => (fk, LaneKind::U32), Pred::Range { .. } => return None, }) } @@ -334,6 +337,9 @@ pub(crate) fn validate( if let Some((lane, kind)) = pred_lane_and_kind(pred) { check_lane(planes, lane, kind)?; } + if let Pred::EqU32Via { key, .. } = pred { + check_foreign_lane(foreign, key, LaneKind::U32)?; + } if let Pred::Range { lo, hi } = pred { let hi_fits = usize::try_from(hi).is_ok_and(|h| h <= planes.n_rows); if lo > hi || !hi_fits { @@ -394,13 +400,28 @@ pub(crate) fn validate( written_slots.mark(dst_of(op)); } match p.terminal { - Terminal::Count { mask } - | Terminal::Any { mask } - | Terminal::All { mask } - | Terminal::Keep { mask } => { + Terminal::Count { mask } | Terminal::Any { mask } | Terminal::All { mask } => { check_operand(p, planes, mask)?; written_slots.readable(mask) } + Terminal::Keep { mask } => { + check_operand(p, planes, mask)?; + written_slots.readable(mask)?; + // The kept mask is a DEMANDED sink: `Out::Mask` sized to the + // population, or `Out::None` for a caller whose single-tile + // scratch holds it (the executor refuses `None` under tiling). + // Any other shape is ignored, as `Keep` always ignored `out` + // (the legacy `execute` passes `Out::I32` for every terminal). + let want = words_for(n); + match out { + OutShape::Mask(len) if len != want => Err(ExecError::LenMismatch { + what: "out", + expected: want, + found: len, + }), + _ => Ok(()), + } + } Terminal::MaskedSumI32 { mask, lane } => { check_operand(p, planes, mask)?; written_slots.readable(mask)?; @@ -527,7 +548,7 @@ fn u32_at(planes: &Planes<'_>, lane: u16, row: usize) -> u32 { /// safe only because `validate` has already refused a mismatched lane kind /// (`ExecError::LaneKind`) before this is ever called. Both the executor and /// this oracle call that same `validate`, so neither can reach the fallback. -fn eval_pred(planes: &Planes<'_>, pred: Pred, row: usize) -> bool { +fn eval_pred(planes: &Planes<'_>, foreign: &Foreign<'_>, pred: Pred, row: usize) -> bool { let u32_at = |lane: u16| match planes.lanes[usize::from(lane)] { LaneRef::U32(v) => v[row], _ => 0, @@ -555,6 +576,15 @@ fn eval_pred(planes: &Planes<'_>, pred: Pred, row: usize) -> bool { pattern, care, } => (u64_at(lane) ^ pattern) & care == 0, + // Both hops are the zero fallback: a key past the foreign lane's end + // names no row and therefore does not match. + Pred::EqU32Via { fk, key, v } => { + let idx = u32_at(fk) as usize; + match foreign.lanes[usize::from(key)] { + LaneRef::U32(f) => f.get(idx).is_some_and(|&x| x == v), + _ => false, + } + } // `hi <= n_rows` and `lo <= hi` were validated, so both fit a usize. Pred::Range { lo, hi } => (lo as usize..hi as usize).contains(&row), } @@ -605,7 +635,10 @@ fn run(p: &Program, planes: &Planes<'_>, foreign: &Foreign<'_>) -> Rows { pred, under, dst: _, - } => under.is_none_or(|u| rows.bit(planes, u, row)) && eval_pred(planes, pred, row), + } => { + under.is_none_or(|u| rows.bit(planes, u, row)) + && eval_pred(planes, foreign, pred, row) + } MaskOp::And { a, b, .. } => rows.bit(planes, a, row) & rows.bit(planes, b, row), MaskOp::Or { a, b, .. } => rows.bit(planes, a, row) | rows.bit(planes, b, row), MaskOp::Xor { a, b, .. } => rows.bit(planes, a, row) ^ rows.bit(planes, b, row), @@ -787,7 +820,17 @@ pub fn reference_execute_into( } Value::GroupSummed } - Terminal::Keep { mask } => Value::Mask(mask), + Terminal::Keep { mask } => { + if let Out::Mask(o) = out { + for w in o.iter_mut() { + *w = 0; + } + for r in survivors(mask) { + o[r / 64] |= 1u64 << (r % 64); + } + } + Value::Mask(mask) + } }) } diff --git a/crates/lance-graph-mask-risc/tests/differential.rs b/crates/lance-graph-mask-risc/tests/differential.rs index e3a96ef8e..131945d65 100644 --- a/crates/lance-graph-mask-risc/tests/differential.rs +++ b/crates/lance-graph-mask-risc/tests/differential.rs @@ -4,10 +4,10 @@ //! 256 ternlog immediates; every `Terminal`. Values AND scratch contents are //! compared, so a wrong word that a terminal happens to hide is still caught. -use lance_graph_mask_risc::exec::{execute, Scratch}; +use lance_graph_mask_risc::exec::{execute, execute_into, Scratch}; use lance_graph_mask_risc::reference::{reference_execute, reference_scratch}; use lance_graph_mask_risc::{ - words_for, LaneRef, MaskOp, Operand, Planes, Pred, Program, Terminal, Value, + words_for, Foreign, LaneRef, MaskOp, Operand, Out, Planes, Pred, Program, Terminal, Value, }; const ROWS: [usize; 8] = [0, 1, 63, 64, 65, 130, 1000, 65_536]; @@ -92,7 +92,9 @@ impl Fixture { masks: &masks, lanes: &lanes, }; - let mut scratch = Scratch::for_program(p, self.n).expect("addressable"); + // ONE tile as wide as the population: every intermediate stays in + // its slot, so the oracle's packed slots can be diffed word for word. + let mut scratch = Scratch::new(words_for(self.n), p.scratch_slots as usize); let mut out_exec = vec![0i32; self.n]; let mut out_ref = vec![0i32; self.n]; let got = execute(p, &planes, &mut scratch, Some(&mut out_exec)); @@ -110,6 +112,38 @@ impl Fixture { ); } } + // The DEFAULT scratch — tiled — must answer identically: the same + // `Value`, the same blend output, and for a `Keep` the demanded + // `Out::Mask` equal to the single-tile slot (or plane) it names. + let mut tiled = Scratch::for_program(p, self.n).expect("addressable"); + let mut out_tiled = vec![0i32; self.n]; + let mut kept = vec![0u64; words_for(self.n)]; + let got_tiled = if let Terminal::Keep { .. } = p.terminal { + execute_into(p, &planes, &Foreign::NONE, &mut tiled, Out::Mask(&mut kept)) + } else { + execute_into( + p, + &planes, + &Foreign::NONE, + &mut tiled, + Out::I32(&mut out_tiled), + ) + }; + assert_eq!(got_tiled, want, "{name} @ n={}: tiled value", self.n); + if let Ok(Value::Blended) = want { + assert_eq!( + out_tiled, out_ref, + "{name} @ n={}: tiled blend output", + self.n + ); + } + if let Ok(Value::Mask(op)) = want { + let full: &[u64] = match op { + Operand::Scratch(i) => scratch.slot(i).expect("kept slot"), + Operand::Plane(i) => planes.masks[usize::from(i)], + }; + assert_eq!(kept.as_slice(), full, "{name} @ n={}: tiled Keep", self.n); + } } fn count(&self, p: &Program) -> usize { diff --git a/crates/lance-graph-mask-risc/tests/foreign.rs b/crates/lance-graph-mask-risc/tests/foreign.rs index 9b6c0a488..cf492ccb7 100644 --- a/crates/lance-graph-mask-risc/tests/foreign.rs +++ b/crates/lance-graph-mask-risc/tests/foreign.rs @@ -5,8 +5,8 @@ use lance_graph_mask_risc::exec::{execute_into, Scratch}; use lance_graph_mask_risc::reference::{reference_execute_into, reference_scratch_with_foreign}; use lance_graph_mask_risc::{ - scratch_words_for, words_for, ExecError, Foreign, ForeignPlane, LaneRef, MaskOp, Operand, Out, - Planes, Pred, Program, Terminal, Value, + scratch_words_for, words_for, ExecError, Foreign, ForeignPlane, LaneKind, LaneRef, MaskOp, + Operand, Out, Planes, Pred, Program, Terminal, Value, }; fn lcg(seed: &mut u64) -> u64 { @@ -682,3 +682,139 @@ fn group_sum_via_refusals_match_between_executor_and_oracle() { }, ); } + +/// FAILS IF: `Pred::EqU32Via` disagrees with the oracle — ungated and under a +/// gate — for a foreign lane shorter than the fk range (zero-fallback), on +/// the TILED default scratch with the mask demanded through `Out::Mask`, at +/// every `n` in `ROWS`. Non-vacuous: the fixture must produce hits AND +/// misses, and at least one out-of-range fk. This is the factored join +/// filter: no predicate plane over the foreign table, no gathered mask. +#[test] +fn eq_u32_via_matches_the_oracle_gated_and_ungated_on_the_tiled_default() { + for &n in &ROWS { + for &foreign_rows in &[5usize, 64, 70, n.saturating_sub(1).max(1)] { + let fx = Fixture::new(n, foreign_rows, 8, 0xE0_1A); + let (lanes, masks) = fx.planes(); + let planes = Planes { + n_rows: n, + masks: &masks, + lanes: &lanes, + }; + // country[j] = j % 4 for every foreign row — a 4-way key. + let country: Vec = (0..foreign_rows).map(|j| (j % 4) as u32).collect(); + let flanes = [LaneRef::U32(&country)]; + let foreign = Foreign { + planes: &[], + lanes: &flanes, + }; + for gated in [false, true] { + let mut ops = Vec::new(); + if gated { + ops.push(MaskOp::Pred { + pred: Pred::EqU32 { lane: 1, v: 1 }, + under: None, + dst: 1, + }); + } + ops.push(MaskOp::Pred { + pred: Pred::EqU32Via { + fk: 0, + key: 0, + v: 3, + }, + under: gated.then_some(S1), + dst: 0, + }); + let p = Program::new(ops, Terminal::Keep { mask: S0 }); + + let mut scratch = Scratch::for_program(&p, n).expect("addressable"); + let mut kept = vec![u64::MAX; words_for(n)]; + let got = execute_into(&p, &planes, &foreign, &mut scratch, Out::Mask(&mut kept)) + .expect("runs"); + let mut want_kept = vec![0u64; words_for(n)]; + let want = reference_execute_into(&p, &planes, &foreign, Out::Mask(&mut want_kept)) + .expect("oracle runs"); + assert_eq!( + got, want, + "n={n} foreign_rows={foreign_rows} gated={gated}: value" + ); + assert_eq!( + kept, want_kept, + "n={n} foreign_rows={foreign_rows} gated={gated}: kept mask" + ); + + // Independent per-row expectation, so the two sides cannot + // agree by sharing a mistake. + let mut expect = vec![0u64; words_for(n)]; + let mut hits = 0usize; + for i in 0..n { + let via = country.get(fx.fk[i] as usize).is_some_and(|&c| c == 3); + let gate = !gated || fx.status[i] == 1; + if via && gate { + expect[i / 64] |= 1 << (i % 64); + hits += 1; + } + } + assert_eq!( + kept, expect, + "n={n} foreign_rows={foreign_rows} gated={gated}: expected" + ); + if n >= 64 { + assert!( + hits > 0 && hits < n, + "n={n} gated={gated}: vacuous fixture ({hits} hits)" + ); + } + } + assert!( + fx.fk.iter().any(|&i| i as usize >= foreign_rows), + "n={n} foreign_rows={foreign_rows}: no out-of-range fk" + ); + } + } +} + +/// FAILS IF: a `Pred::EqU32Via` naming a missing or wrong-width foreign lane +/// is not refused identically by executor and oracle, before any write. +#[test] +fn eq_u32_via_refusals_match_between_executor_and_oracle() { + let n = 70; + let fx = Fixture::new(n, 16, 4, 0xBAD); + let (lanes, masks) = fx.planes(); + let planes = Planes { + n_rows: n, + masks: &masks, + lanes: &lanes, + }; + let i32_lane: Vec = vec![0; 16]; + let flanes = [LaneRef::I32(&i32_lane)]; + let foreign = Foreign { + planes: &[], + lanes: &flanes, + }; + for (key, want) in [ + (1u16, ExecError::ForeignLaneOutOfRange(1)), + ( + 0u16, + ExecError::ForeignLaneKind { + lane: 0, + expected: LaneKind::U32, + found: LaneKind::I32, + }, + ), + ] { + let p = Program::new( + vec![MaskOp::Pred { + pred: Pred::EqU32Via { fk: 0, key, v: 0 }, + under: None, + dst: 0, + }], + Terminal::Count { mask: S0 }, + ); + let mut scratch = Scratch::for_program(&p, n).expect("addressable"); + let got = execute_into(&p, &planes, &foreign, &mut scratch, Out::None); + let oracle = reference_execute_into(&p, &planes, &foreign, Out::None); + assert_eq!(got, Err(want), "executor refusal for key={key}"); + assert_eq!(oracle, Err(want), "oracle refusal for key={key}"); + } +} From 4fbee5f55e7592da5e0786629d1748e246fe72cf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 21:32:02 +0000 Subject: [PATCH 06/15] quack: tiled scratch everywhere; a kept mask lands in the demanded Out::Mask Every scratch in the lowering tests and the DuckDB harness is now the tiled default (Scratch::for_program); no scratch is population-sized. A Keep result is read from the caller's Out::Mask, never from a slot. METRIC gains tile_words and scratch_bytes: on the 4096-row line table a two-slot filter drops from 129 to 17 scratch words; every case still equals DuckDB with alloc_bytes_exec=0. mask-risc re-exports tile_words_for / TILE_WORDS at the crate root. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/lance-graph-mask-risc/src/lib.rs | 4 +- crates/lance-graph-quack/src/lib.rs | 100 +++++----- .../tests/duckdb_differential.rs | 178 ++++++++++-------- 3 files changed, 162 insertions(+), 120 deletions(-) diff --git a/crates/lance-graph-mask-risc/src/lib.rs b/crates/lance-graph-mask-risc/src/lib.rs index 4d7a1fbb5..7a544a009 100644 --- a/crates/lance-graph-mask-risc/src/lib.rs +++ b/crates/lance-graph-mask-risc/src/lib.rs @@ -118,7 +118,9 @@ pub mod reference; pub mod ternlog_dispatch; pub mod value; -pub use exec::{execute, execute_into, materialize_rows, scratch_words_for, Scratch}; +pub use exec::{ + execute, execute_into, materialize_rows, scratch_words_for, tile_words_for, Scratch, TILE_WORDS, +}; pub use fuse::{fuse, fuse_program, ternlog_imm, BoolExpr, FuseError, Fused}; pub use ir::{ Foreign, ForeignPlane, LaneRef, MaskOp, Operand, Planes, Pred, Program, Terminal, diff --git a/crates/lance-graph-quack/src/lib.rs b/crates/lance-graph-quack/src/lib.rs index 72ed3c066..e4f894e6e 100644 --- a/crates/lance-graph-quack/src/lib.rs +++ b/crates/lance-graph-quack/src/lib.rs @@ -849,9 +849,12 @@ pub struct GroupBy { /// The two-phase plan a [`GroupBy`] lowers to — DuckDB's pipeline break, /// with a mask plane where the hash table would be. /// -/// The caller runs `filter` (a [`Terminal::Keep`]), reads the mask its -/// `Value::Mask(op)` names — a scratch slot, or for a bare-plane filter the -/// plane itself — presents it as `planes.masks[filter_plane]`, and runs each +/// The caller runs `filter` (a [`Terminal::Keep`]) through `execute_into` +/// with an `Out::Mask` buffer of `words_for(n_rows)` — the kept bits land +/// there (tiled execution never leaves them sitting in a scratch slot the +/// caller must chase down by `Value::Mask`'s operand, which for `filter` +/// names a scratch slot or, for a bare-plane filter, the plane itself), +/// presents that buffer as `planes.masks[filter_plane]`, and runs each /// program in `groups` over the widened planes. Each group program is ONE /// gated equality and a terminal: the key lane is compared once per group, /// but only over the live words of the kept filter (the survivor skip), which @@ -1422,7 +1425,7 @@ fn pred_of(col: Col, cmp: Cmp) -> Pred { mod tests { use super::*; use lance_graph_mask_risc::{ - execute, materialize_rows, reference_execute, scratch_words_for, words_for, LaneRef, + execute_into, materialize_rows, reference_execute, words_for, Foreign, LaneRef, Out, Planes, Scratch, Value, }; @@ -1595,28 +1598,43 @@ mod tests { }) } - /// The executor, on a scratch sized exactly from the program. + /// The executor, tiled per [`Scratch::for_program`]. A `Keep` + /// terminal is handed an owned `Out::Mask` buffer (discarded here — + /// `exec_mask` is the caller that wants the bits); every other + /// terminal gets the caller's `out` or `Out::None`. fn exec(&self, program: &Program, extra: &[Vec], out: Option<&mut [i32]>) -> Value { self.with_planes(extra, |planes| { - let words = words_for(self.n()); - let slots = program.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; - let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); - execute(program, planes, &mut scratch, out).expect("runs") + let mut scratch = Scratch::for_program(program, self.n()).expect("carves"); + let mut kept_buf = vec![0u64; words_for(self.n())]; + let dst = if matches!(program.terminal, Terminal::Keep { .. }) { + Out::Mask(&mut kept_buf) + } else if let Some(o) = out { + Out::I32(o) + } else { + Out::None + }; + execute_into(program, planes, &Foreign::NONE, &mut scratch, dst).expect("runs") }) } - /// The executor on a `Keep` program, copying the kept mask out of - /// wherever `Value::Mask` says it landed. + /// The executor on a `Keep` program: tiled execution lands the kept + /// bits in the `Out::Mask` buffer this owns and returns, never in a + /// scratch slot the caller must chase down by `Value::Mask`'s + /// operand. fn exec_mask(&self, program: &Program, extra: &[Vec]) -> Vec { self.with_planes(extra, |planes| { - let words = words_for(self.n()); - let slots = program.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; - let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); - match execute(program, planes, &mut scratch, None).expect("runs") { - Value::Mask(Operand::Scratch(i)) => scratch.slot(i).expect("written").to_vec(), - Value::Mask(Operand::Plane(p)) => planes.masks[usize::from(p)].to_vec(), + let mut scratch = Scratch::for_program(program, self.n()).expect("carves"); + let mut kept = vec![0u64; words_for(self.n())]; + match execute_into( + program, + planes, + &Foreign::NONE, + &mut scratch, + Out::Mask(&mut kept), + ) + .expect("runs") + { + Value::Mask(_) => kept, other => panic!("not a kept mask: {other:?}"), } }) @@ -2663,10 +2681,7 @@ mod tests { ("in place", lower(&q).expect("lowers in place")), ("fused", lower_fused(&q).expect("lowers fused")), ] { - let words = words_for(fx.n()); - let slots = program.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; - let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut scratch = Scratch::for_program(&program, fx.n()).expect("carves"); let value = fx.with_planes(&[], |planes| { lance_graph_mask_risc::execute_into( &program, @@ -2717,10 +2732,7 @@ mod tests { }, }; let program = lower(&q).expect("lowers"); - let words = words_for(fx.n()); - let slots = program.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; - let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut scratch = Scratch::for_program(&program, fx.n()).expect("carves"); let mut got = vec![0u64; words_for(out_rows as usize)]; let value = fx.with_planes(&[], |planes| { lance_graph_mask_risc::execute_into( @@ -2767,10 +2779,7 @@ mod tests { }, }; let program = lower(&q).expect("lowers"); - let words = words_for(fx.n()); - let slots = program.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; - let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut scratch = Scratch::for_program(&program, fx.n()).expect("carves"); let mut got = vec![0i64; groups]; let value = fx.with_planes(&[], |planes| { lance_graph_mask_risc::execute_into( @@ -2841,10 +2850,7 @@ mod tests { }, }; let program = lower(&q).expect("lowers"); - let words = words_for(fx.n()); - let slots = program.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; - let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut scratch = Scratch::for_program(&program, fx.n()).expect("carves"); let mut got = vec![0i64; groups]; let foreign_lanes = [LaneRef::U32(remap)]; let mask_risc_foreign = lance_graph_mask_risc::Foreign { @@ -2874,8 +2880,7 @@ mod diamond_lowering_tests { use lance_graph_contract::facet::FacetCascade; use lance_graph_contract::ordered_lane::WitnessError; use lance_graph_mask_risc::{ - execute, materialize_rows, scratch_words_for, words_for, LaneRef, Operand, Planes, Scratch, - Value, + execute_into, materialize_rows, words_for, Foreign, LaneRef, Out, Planes, Scratch, Value, }; const LANE: Col = Col(0); // provenance only @@ -2954,15 +2959,20 @@ mod diamond_lowering_tests { masks: &masks, lanes: &lanes, }; - let words = words_for(self.n()); - let slots = program.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; - let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); - let mask = match execute(&program, &planes, &mut scratch, None).expect("runs") { - Value::Mask(Operand::Scratch(i)) => scratch.slot(i).expect("written").to_vec(), - Value::Mask(Operand::Plane(p)) => planes.masks[usize::from(p)].to_vec(), + let mut scratch = Scratch::for_program(&program, self.n()).expect("carves"); + let mut mask = vec![0u64; words_for(self.n())]; + match execute_into( + &program, + &planes, + &Foreign::NONE, + &mut scratch, + Out::Mask(&mut mask), + ) + .expect("runs") + { + Value::Mask(_) => {} other => panic!("not a mask: {other:?}"), - }; + } materialize_rows(&mask, self.n()) } diff --git a/crates/lance-graph-quack/tests/duckdb_differential.rs b/crates/lance-graph-quack/tests/duckdb_differential.rs index 0a11984d9..d21dc56fd 100644 --- a/crates/lance-graph-quack/tests/duckdb_differential.rs +++ b/crates/lance-graph-quack/tests/duckdb_differential.rs @@ -30,8 +30,8 @@ use std::path::Path; use std::sync::atomic::{AtomicUsize, Ordering}; use lance_graph_mask_risc::{ - execute, execute_into, materialize_rows, scratch_words_for, words_for, Foreign, ForeignPlane, - LaneRef, Operand, Out, Planes, Scratch, Value, + execute_into, materialize_rows, scratch_words_for, words_for, Foreign, ForeignPlane, LaneRef, + Out, Planes, Scratch, Terminal, Value, }; use lance_graph_quack::{ lower, lower_group_by, Agg, Cmp, Col, Filter, ForeignLane, ForeignMask, GroupBy, Query, @@ -133,6 +133,11 @@ fn assert_case(cases: &[Case], id: &str, actual: &str) { struct CaseMetrics { ops: usize, scratch_words: usize, + /// The scratch slot width [`Scratch::for_program`] actually carved — + /// `tile_words_for(n_rows)`, never `words_for(n_rows)`. For a + /// multi-phase case this is the FIRST (filter/phase-1) scratch's width; + /// `scratch_words` stays the sum over every phase, as before. + tile_words: usize, alloc_bytes_exec: usize, rows_materialized: usize, index_vec_len: usize, @@ -150,12 +155,16 @@ struct CaseMetrics { } fn print_metric(id: &str, m: &CaseMetrics) { + let scratch_bytes = m.scratch_words * std::mem::size_of::(); match m.programs { Some(k) => eprintln!( - "METRIC case={id} ops={} scratch_words={} alloc_bytes_exec={} \ - rows_materialized={} index_vec_len={} out_bytes={} pair_relation_bytes={} programs={k}", + "METRIC case={id} ops={} scratch_words={} tile_words={} scratch_bytes={} \ + alloc_bytes_exec={} rows_materialized={} index_vec_len={} out_bytes={} \ + pair_relation_bytes={} programs={k}", m.ops, m.scratch_words, + m.tile_words, + scratch_bytes, m.alloc_bytes_exec, m.rows_materialized, m.index_vec_len, @@ -163,10 +172,13 @@ fn print_metric(id: &str, m: &CaseMetrics) { m.pair_relation_bytes, ), None => eprintln!( - "METRIC case={id} ops={} scratch_words={} alloc_bytes_exec={} \ - rows_materialized={} index_vec_len={} out_bytes={} pair_relation_bytes={}", + "METRIC case={id} ops={} scratch_words={} tile_words={} scratch_bytes={} \ + alloc_bytes_exec={} rows_materialized={} index_vec_len={} out_bytes={} \ + pair_relation_bytes={}", m.ops, m.scratch_words, + m.tile_words, + scratch_bytes, m.alloc_bytes_exec, m.rows_materialized, m.index_vec_len, @@ -176,28 +188,29 @@ fn print_metric(id: &str, m: &CaseMetrics) { } } -/// Copy a kept mask's bits out of wherever `Value::Mask` says they landed — -/// a scratch slot, or (for a bare-plane filter) the plane itself. -fn mask_bits(planes: &Planes<'_>, scratch: &Scratch<'_>, op: Operand) -> Vec { - match op { - Operand::Scratch(i) => scratch.slot(i).expect("Keep names a written slot").to_vec(), - Operand::Plane(p) => planes.masks[usize::from(p)].to_vec(), - } -} - /// A plain scalar/projection query: lower, size scratch exactly, execute, /// encode. The encoding matches `oracle.py`'s exactly — see /// `tests/duckdb/README.txt`. fn run_query(id: &str, planes: &Planes<'_>, filter: Filter, agg: Agg) -> (String, CaseMetrics) { let program = lower(&Query { filter, agg }).expect("lowers"); - let words = words_for(planes.n_rows); - let slots = program.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; - let scratch_words = buf.len(); - let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut scratch = Scratch::for_program(&program, planes.n_rows).expect("carves"); + let tile_words = scratch.words(); + let scratch_words = scratch_words_for(tile_words, scratch.slots()).expect("sized"); + + // `Terminal::Keep` under tiled execution demands a caller `Out::Mask` + // buffer — the kept bits land THERE, never in a scratch slot the caller + // chases down by `Value::Mask`'s operand (see D-LGJ… the mask-risc + // tiling note). Every other terminal folds into its own `Value`, so + // `Out::None` is enough for it. + let mut kept_buf = vec![0u64; words_for(planes.n_rows)]; + let out = if matches!(program.terminal, Terminal::Keep { .. }) { + Out::Mask(&mut kept_buf) + } else { + Out::None + }; let before = BYTES.load(Ordering::Relaxed); - let value = execute(&program, planes, &mut scratch, None).expect("runs"); + let value = execute_into(&program, planes, &Foreign::NONE, &mut scratch, out).expect("runs"); let alloc_bytes_exec = alloc_delta(before); let (encoded, rows_materialized, index_vec_len) = match value { @@ -208,9 +221,8 @@ fn run_query(id: &str, planes: &Planes<'_>, filter: Filter, agg: Agg) -> (String Value::OptI32(None) => { panic!("case {id}: min/max over an empty set — fixture is not what the case assumes") } - Value::Mask(op) => { - let bits = mask_bits(planes, &scratch, op); - let rows = materialize_rows(&bits, planes.n_rows); + Value::Mask(_) => { + let rows = materialize_rows(&kept_buf, planes.n_rows); let n = rows.len(); let s = rows .iter() @@ -233,6 +245,7 @@ fn run_query(id: &str, planes: &Planes<'_>, filter: Filter, agg: Agg) -> (String CaseMetrics { ops: program.ops.len(), scratch_words, + tile_words, alloc_bytes_exec, rows_materialized, index_vec_len, @@ -269,20 +282,29 @@ fn run_group( "case {id}: filter_plane drifted from where we place the kept mask" ); - let words = words_for(planes.n_rows); - let f_slots = plan.filter.scratch_slots as usize; - let mut f_buf = vec![0u64; scratch_words_for(words, f_slots).expect("sized")]; - let mut scratch_words_total = f_buf.len(); - let mut f_scratch = Scratch::over(&mut f_buf, words, f_slots).expect("carves"); + let mut f_scratch = Scratch::for_program(&plan.filter, planes.n_rows).expect("carves"); + let f_tile_words = f_scratch.words(); + let mut scratch_words_total = + scratch_words_for(f_tile_words, f_scratch.slots()).expect("sized"); + // Phase 1 is a `Terminal::Keep` — its kept bits land in this owned + // `Out::Mask` buffer, never in a scratch slot (see `run_query`'s note). + let mut kept_bits = vec![0u64; words_for(planes.n_rows)]; let before = BYTES.load(Ordering::Relaxed); - let kept = execute(&plan.filter, planes, &mut f_scratch, None).expect("runs"); + let kept = execute_into( + &plan.filter, + planes, + &Foreign::NONE, + &mut f_scratch, + Out::Mask(&mut kept_bits), + ) + .expect("runs"); let mut alloc_bytes_exec = alloc_delta(before); - let kept_bits = match kept { - Value::Mask(op) => mask_bits(planes, &f_scratch, op), + match kept { + Value::Mask(_) => {} other => panic!("case {id}: group filter did not Keep a mask: {other:?}"), - }; + } let widened_masks: Vec<&[u64]> = std::iter::once(kept_bits.as_slice()).collect(); let widened = Planes { n_rows: planes.n_rows, @@ -293,13 +315,13 @@ fn run_group( let mut ops = plan.filter.ops.len(); let mut pairs = Vec::with_capacity(plan.groups.len()); for (k, prog) in plan.groups.iter().enumerate() { - let g_slots = prog.scratch_slots as usize; - let mut g_buf = vec![0u64; scratch_words_for(words, g_slots).expect("sized")]; - scratch_words_total += g_buf.len(); - let mut g_scratch = Scratch::over(&mut g_buf, words, g_slots).expect("carves"); + let mut g_scratch = Scratch::for_program(prog, planes.n_rows).expect("carves"); + scratch_words_total += + scratch_words_for(g_scratch.words(), g_scratch.slots()).expect("sized"); let before = BYTES.load(Ordering::Relaxed); - let v = execute(prog, &widened, &mut g_scratch, None).expect("runs"); + let v = + execute_into(prog, &widened, &Foreign::NONE, &mut g_scratch, Out::None).expect("runs"); alloc_bytes_exec += alloc_delta(before); ops += prog.ops.len(); @@ -317,6 +339,7 @@ fn run_group( CaseMetrics { ops, scratch_words: scratch_words_total, + tile_words: f_tile_words, alloc_bytes_exec, rows_materialized: 0, index_vec_len: 0, @@ -584,11 +607,9 @@ fn group_sum_cc() { }, }) .expect("lowers"); - let words = words_for(planes.n_rows); - let slots = program.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; - let scratch_words = buf.len(); - let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut scratch = Scratch::for_program(&program, planes.n_rows).expect("carves"); + let tile_words = scratch.words(); + let scratch_words = scratch_words_for(tile_words, scratch.slots()).expect("sized"); let mut out = [0i64; 8]; let before = BYTES.load(Ordering::Relaxed); let value = execute_into( @@ -611,6 +632,7 @@ fn group_sum_cc() { let m = CaseMetrics { ops: program.ops.len(), scratch_words, + tile_words, alloc_bytes_exec, rows_materialized: 0, index_vec_len: 0, @@ -660,19 +682,27 @@ fn join_sum_country() { agg: Agg::Rows, }) .expect("lowers"); - let p_words = words_for(partner_planes.n_rows); - let p_slots = partner_program.scratch_slots as usize; - let mut p_buf = vec![0u64; scratch_words_for(p_words, p_slots).expect("sized")]; - let p_scratch_words = p_buf.len(); - let mut p_scratch = Scratch::over(&mut p_buf, p_words, p_slots).expect("carves"); + let mut p_scratch = + Scratch::for_program(&partner_program, partner_planes.n_rows).expect("carves"); + let p_tile_words = p_scratch.words(); + let p_scratch_words = scratch_words_for(p_tile_words, p_scratch.slots()).expect("sized"); + // Phase 1 is a `Terminal::Keep` — its kept bits land in this owned + // `Out::Mask` buffer, never in a scratch slot. + let mut partner_bits = vec![0u64; words_for(partner_planes.n_rows)]; let before1 = BYTES.load(Ordering::Relaxed); - let p_value = execute(&partner_program, &partner_planes, &mut p_scratch, None).expect("runs"); + let p_value = execute_into( + &partner_program, + &partner_planes, + &Foreign::NONE, + &mut p_scratch, + Out::Mask(&mut partner_bits), + ) + .expect("runs"); let alloc1 = alloc_delta(before1); - let p_op = match p_value { - Value::Mask(op) => op, + match p_value { + Value::Mask(_) => {} other => panic!("partner-side filter did not Keep a mask: {other:?}"), - }; - let partner_bits = mask_bits(&partner_planes, &p_scratch, p_op); + } let foreign_plane = ForeignPlane { words: &partner_bits, @@ -695,11 +725,8 @@ fn join_sum_country() { agg: Agg::SumI32(AMOUNT), }) .expect("lowers"); - let words = words_for(planes.n_rows); - let slots = line_program.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; - let scratch_words = buf.len(); - let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut scratch = Scratch::for_program(&line_program, planes.n_rows).expect("carves"); + let scratch_words = scratch_words_for(scratch.words(), scratch.slots()).expect("sized"); let before2 = BYTES.load(Ordering::Relaxed); let value = execute_into(&line_program, &planes, &foreign, &mut scratch, Out::None).expect("runs"); @@ -713,6 +740,7 @@ fn join_sum_country() { let m = CaseMetrics { ops: partner_program.ops.len() + line_program.ops.len(), scratch_words: p_scratch_words + scratch_words, + tile_words: p_tile_words, alloc_bytes_exec: alloc1 + alloc2, rows_materialized: 0, index_vec_len: 0, @@ -748,11 +776,9 @@ fn join_count_docs_with_posted() { }, }) .expect("lowers"); - let words = words_for(planes.n_rows); - let slots = line_program.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; - let scratch_words = buf.len(); - let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut scratch = Scratch::for_program(&line_program, planes.n_rows).expect("carves"); + let tile_words = scratch.words(); + let scratch_words = scratch_words_for(tile_words, scratch.slots()).expect("sized"); let mut scattered = vec![0u64; words_for(DOC_ROWS)]; let before1 = BYTES.load(Ordering::Relaxed); let value1 = execute_into( @@ -786,13 +812,17 @@ fn join_count_docs_with_posted() { agg: Agg::Count, }) .expect("lowers"); - let d_words = words_for(DOC_ROWS); - let d_slots = doc_program.scratch_slots as usize; - let mut d_buf = vec![0u64; scratch_words_for(d_words, d_slots).expect("sized")]; - let d_scratch_words = d_buf.len(); - let mut d_scratch = Scratch::over(&mut d_buf, d_words, d_slots).expect("carves"); + let mut d_scratch = Scratch::for_program(&doc_program, DOC_ROWS).expect("carves"); + let d_scratch_words = scratch_words_for(d_scratch.words(), d_scratch.slots()).expect("sized"); let before2 = BYTES.load(Ordering::Relaxed); - let value2 = execute(&doc_program, &doc_planes, &mut d_scratch, None).expect("runs"); + let value2 = execute_into( + &doc_program, + &doc_planes, + &Foreign::NONE, + &mut d_scratch, + Out::None, + ) + .expect("runs"); let alloc2 = alloc_delta(before2); let encoded = match value2 { @@ -803,6 +833,7 @@ fn join_count_docs_with_posted() { let m = CaseMetrics { ops: line_program.ops.len() + doc_program.ops.len(), scratch_words: scratch_words + d_scratch_words, + tile_words, alloc_bytes_exec: alloc1 + alloc2, rows_materialized: 0, index_vec_len: 0, @@ -838,11 +869,9 @@ fn join_group_sum_country() { }, }) .expect("lowers"); - let words = words_for(planes.n_rows); - let slots = program.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")]; - let scratch_words = buf.len(); - let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves"); + let mut scratch = Scratch::for_program(&program, planes.n_rows).expect("carves"); + let tile_words = scratch.words(); + let scratch_words = scratch_words_for(tile_words, scratch.slots()).expect("sized"); let country_lane = [LaneRef::U32(&fx.partner.country)]; let foreign = Foreign { @@ -872,6 +901,7 @@ fn join_group_sum_country() { let m = CaseMetrics { ops: program.ops.len(), scratch_words, + tile_words, alloc_bytes_exec, rows_materialized: 0, index_vec_len: 0, From 0eb14d9b9f02cde785a7c96a4b56dcab20ce1489 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 21:35:06 +0000 Subject: [PATCH 07/15] =?UTF-8?q?mask-risc:=20ScatterCountU32=20=E2=80=94?= =?UTF-8?q?=20the=20one-to-many=20hop=20folded=20to=20its=20distinct=20cou?= =?UTF-8?q?nt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit COUNT(DISTINCT fk) over the survivors in one terminal: the caller's Out::Mask is the fold's accumulator (one bit per distinct key), zeroed once, scattered into tile by tile, and only its popcount leaves as Value::Count. No second program reads the accumulator, so a "docs with a posted line" count no longer materialises a doc mask as intermediate state. ScatterOrU32 stays for the case where the mask itself is the demanded result (hop). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/lance-graph-mask-risc/src/exec.rs | 17 ++++- crates/lance-graph-mask-risc/src/ir.rs | 15 ++++ crates/lance-graph-mask-risc/src/reference.rs | 33 ++++++++- crates/lance-graph-mask-risc/tests/foreign.rs | 71 +++++++++++++++++++ 4 files changed, 131 insertions(+), 5 deletions(-) diff --git a/crates/lance-graph-mask-risc/src/exec.rs b/crates/lance-graph-mask-risc/src/exec.rs index dd96632a1..38b774237 100644 --- a/crates/lance-graph-mask-risc/src/exec.rs +++ b/crates/lance-graph-mask-risc/src/exec.rs @@ -720,7 +720,9 @@ pub fn execute_into( // sink is the requested result, so this is a write the law permits; the // facade kernels themselves never zero a destination they only add to. match (&program.terminal, &mut out) { - (Terminal::ScatterOrU32 { .. }, Out::Mask(o)) => o.fill(0), + (Terminal::ScatterOrU32 { .. } | Terminal::ScatterCountU32 { .. }, Out::Mask(o)) => { + o.fill(0) + } (Terminal::GroupSumI32 { .. } | Terminal::GroupSumViaI32 { .. }, Out::I64(o)) => o.fill(0), _ => {} } @@ -912,9 +914,16 @@ pub fn execute_into( mask, lane, out_rows, + } + | Terminal::ScatterCountU32 { + mask, + lane, + out_rows, } => { // `validate` already refused a missing or mis-sized `out`; - // the kernel ORs into it, tile after tile. + // the kernel ORs into it, tile after tile. For the count + // form the buffer is the fold's accumulator and only its + // popcount leaves. if let Out::Mask(o) = &mut out { mask_scatter_or_u32( read(planes, &slots, mask, t), @@ -971,6 +980,10 @@ pub fn execute_into( Terminal::MaskedMaxI32 { .. } => Value::OptI32(max), Terminal::BlendI32 { .. } => Value::Blended, Terminal::ScatterOrU32 { .. } => Value::Scattered, + Terminal::ScatterCountU32 { .. } => Value::Count(match &out { + Out::Mask(o) => popcount_batch_u64(o) as usize, + _ => 0, + }), Terminal::GroupSumI32 { .. } | Terminal::GroupSumViaI32 { .. } => Value::GroupSummed, Terminal::Keep { mask } => Value::Mask(mask), }) diff --git a/crates/lance-graph-mask-risc/src/ir.rs b/crates/lance-graph-mask-risc/src/ir.rs index 9eff5937e..15c6de59c 100644 --- a/crates/lance-graph-mask-risc/src/ir.rs +++ b/crates/lance-graph-mask-risc/src/ir.rs @@ -260,6 +260,20 @@ pub enum Terminal { lane: u16, out_rows: u32, }, + /// `COUNT(DISTINCT lane[i])` over the rows where `mask` holds, keys `< + /// out_rows` (out of range silently dropped) — the one-to-many hop + /// FOLDED to its count. The caller's `Out::Mask` (exactly + /// `words_for(out_rows)` long) is the fold's accumulator, one bit per + /// distinct key; the terminal zeroes it, scatters into it tile by tile + /// and answers its popcount as [`Value::Count`]. No second program ever + /// reads it: `docs WHERE EXISTS line … ` counted, without the doc mask + /// becoming an intermediate. (When the mask itself is the demanded + /// result — a `hop` — use [`Terminal::ScatterOrU32`].) + ScatterCountU32 { + mask: Operand, + lane: u16, + out_rows: u32, + }, /// The one-terminal `GROUP BY … SUM`: for every row `i` where `mask` /// holds, adds `val[i]` into the caller's `Out::I64` buffer at index /// `key[i]` — provided `key[i] < out.len()` @@ -400,6 +414,7 @@ impl Program { | Terminal::MaskedMaxI32 { mask, .. } | Terminal::BlendI32 { mask, .. } | Terminal::ScatterOrU32 { mask, .. } + | Terminal::ScatterCountU32 { mask, .. } | Terminal::GroupSumI32 { mask, .. } | Terminal::GroupSumViaI32 { mask, .. } | Terminal::Keep { mask } => touch(mask), diff --git a/crates/lance-graph-mask-risc/src/reference.rs b/crates/lance-graph-mask-risc/src/reference.rs index 2344fdfe5..65cfb95c8 100644 --- a/crates/lance-graph-mask-risc/src/reference.rs +++ b/crates/lance-graph-mask-risc/src/reference.rs @@ -456,11 +456,20 @@ pub(crate) fn validate( mask, lane, out_rows, + } + | Terminal::ScatterCountU32 { + mask, + lane, + out_rows, } => { check_operand(p, planes, mask)?; written_slots.readable(mask)?; check_lane(planes, lane, LaneKind::U32)?; let want = words_for(out_rows as usize); + let what = match p.terminal { + Terminal::ScatterCountU32 { .. } => "ScatterCountU32", + _ => "ScatterOrU32", + }; match out { OutShape::Mask(len) if len == want => Ok(()), OutShape::Mask(len) => Err(ExecError::LenMismatch { @@ -469,9 +478,7 @@ pub(crate) fn validate( found: len, }), OutShape::None | OutShape::I32(_) | OutShape::I64(_) => { - Err(ExecError::TerminalNeedsOut { - what: "ScatterOrU32", - }) + Err(ExecError::TerminalNeedsOut { what }) } } } @@ -778,6 +785,26 @@ pub fn reference_execute_into( } Value::Scattered } + Terminal::ScatterCountU32 { + mask, + lane, + out_rows, + } => { + let mut distinct = 0usize; + if let Out::Mask(o) = out { + for w in o.iter_mut() { + *w = 0; + } + for r in survivors(mask) { + let idx = u32_at(planes, lane, r) as usize; + if idx < out_rows as usize { + o[idx / 64] |= 1u64 << (idx % 64); + } + } + distinct = o.iter().map(|w| w.count_ones() as usize).sum(); + } + Value::Count(distinct) + } Terminal::GroupSumI32 { mask, key, val } => { if let Out::I64(o) = out { for x in o.iter_mut() { diff --git a/crates/lance-graph-mask-risc/tests/foreign.rs b/crates/lance-graph-mask-risc/tests/foreign.rs index cf492ccb7..99eec18b1 100644 --- a/crates/lance-graph-mask-risc/tests/foreign.rs +++ b/crates/lance-graph-mask-risc/tests/foreign.rs @@ -818,3 +818,74 @@ fn eq_u32_via_refusals_match_between_executor_and_oracle() { assert_eq!(oracle, Err(want), "oracle refusal for key={key}"); } } + +/// FAILS IF: `ScatterCountU32` disagrees with the oracle, or with an +/// independent distinct-count over the survivors' in-range keys, on the +/// tiled default scratch — for `out_rows` both smaller and larger than +/// `n_rows`. Non-vacuous: repeats must occur (distinct < survivors) and at +/// least one key must be out of range, so the union and the drop both bite. +#[test] +fn scatter_count_matches_the_oracle_and_an_independent_distinct_count() { + for &n in &ROWS { + for &out_rows in &[5u32, 64, 70, (n as u32).saturating_sub(1).max(1)] { + let fx = Fixture::new(n, out_rows as usize, 8, 0xC0_0E7); + let (lanes, masks) = fx.planes(); + let planes = Planes { + n_rows: n, + masks: &masks, + lanes: &lanes, + }; + let p = Program::new( + vec![MaskOp::Pred { + pred: Pred::EqU32 { lane: 1, v: 1 }, + under: None, + dst: 0, + }], + Terminal::ScatterCountU32 { + mask: S0, + lane: 0, + out_rows, + }, + ); + let mut scratch = Scratch::for_program(&p, n).expect("addressable"); + let mut acc = vec![u64::MAX; words_for(out_rows as usize)]; + let got = execute_into( + &p, + &planes, + &Foreign::NONE, + &mut scratch, + Out::Mask(&mut acc), + ) + .expect("runs"); + let mut acc_ref = vec![0u64; words_for(out_rows as usize)]; + let want = reference_execute_into(&p, &planes, &Foreign::NONE, Out::Mask(&mut acc_ref)) + .expect("oracle runs"); + assert_eq!(got, want, "n={n} out_rows={out_rows}: value"); + let mut seen = std::collections::BTreeSet::new(); + let mut survivors = 0usize; + for i in 0..n { + if fx.status[i] == 1 { + survivors += 1; + if (fx.fk[i] as usize) < out_rows as usize { + seen.insert(fx.fk[i]); + } + } + } + assert_eq!( + got, + Value::Count(seen.len()), + "n={n} out_rows={out_rows}: distinct" + ); + if n >= 130 { + assert!( + seen.len() < survivors, + "n={n} out_rows={out_rows}: no repeats" + ); + } + assert!( + fx.fk.iter().any(|&i| i >= out_rows), + "n={n} out_rows={out_rows}: no drop" + ); + } + } +} From a9a3e9d4d5da0d9bb600555a987fff81db3738b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 21:53:50 +0000 Subject: [PATCH 08/15] =?UTF-8?q?quack:=20join=20predicate=20factored=20th?= =?UTF-8?q?rough=20the=20index=20lane=20=E2=80=94=20no=20partner=20mask,?= =?UTF-8?q?=20one=20program?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Filter::EqU32Via { fk, key, v }` lowers straight to `Pred::EqU32Via`, so `join_sum_country` reads `partner.country` through `line.partner_id` inside the line fold: one program, no partner-side program, no kept partner mask, no `Gather`. The harness's Keep → foreign-plane → Gather pipeline for this case is deleted, along with the partner-side `Planes` the fixture no longer runs. `Filter::Semijoin` / `MaskOp::Gather` stay, with their survival condition stated: `foreign` must name a RESIDENT plane the caller holds (`ForeignPlane`, renamed from `ForeignMask`), never a mask another program produced. The one place a foreign plane is populated is the unit test that lowers the semijoin over a hand-built resident plane. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/lance-graph-quack/src/lib.rs | 202 +++++++++++++++--- .../lance-graph-quack/tests/duckdb/fixture.rs | 39 +--- .../tests/duckdb_differential.rs | 110 ++++------ 3 files changed, 213 insertions(+), 138 deletions(-) diff --git a/crates/lance-graph-quack/src/lib.rs b/crates/lance-graph-quack/src/lib.rs index e4f894e6e..1d7c09853 100644 --- a/crates/lance-graph-quack/src/lib.rs +++ b/crates/lance-graph-quack/src/lib.rs @@ -146,9 +146,13 @@ //! Filter (`=`/`<>`/`<`/`<=`/`>`/`>=`/ternary match/`IN`, `AND`/`OR`/`NOT`, //! resident planes), the aggregates `COUNT`/`EXISTS`/`ALL`/`SUM`/`MIN`/`MAX`, //! projection ([`Agg::Rows`] keeps the mask, [`Agg::BlendI32`] is the `CASE` -//! shape), a two-phase `GROUP BY` over a categorical key, and the fk join -//! ([`Filter::Semijoin`] / [`Agg::ScatterOrU32`] over -//! `lance-graph-mask-risc`'s [`MaskOp::Gather`]/`Terminal::ScatterOrU32`). +//! shape), a two-phase `GROUP BY` over a categorical key, and TWO fk join +//! shapes: [`Filter::EqU32Via`] reads the foreign predicate THROUGH the fk +//! in one facade pass (`lance-graph-mask-risc`'s [`Pred::EqU32Via`]), no +//! foreign plane and no gathered mask; [`Filter::Semijoin`] lowers to +//! [`MaskOp::Gather`] over a caller-supplied RESIDENT foreign plane — never +//! a mask another `Program` produced — alongside [`Agg::ScatterOrU32`] for +//! the one-to-many hop back (`Terminal::ScatterOrU32`). //! The one-terminal `GROUP BY SUM` is ONE program either way now: //! [`Agg::GroupSumI32`] for a key on THIS table, [`Agg::GroupSumViaI32`] for //! a key resolved through a foreign table's [`ForeignLane`] @@ -180,20 +184,19 @@ pub struct Col(pub u16); pub struct Mask(pub u16); /// A FOREIGN table's kept mask — an index into -/// [`Foreign::planes`](lance_graph_mask_risc::Foreign), never -/// [`Planes::masks`](lance_graph_mask_risc::Planes). A row-space belonging to -/// another table, addressed through [`Filter::semijoin`] rather than read as -/// an ordinary operand — the same distinction [`lance_graph_mask_risc::ir`] -/// draws between [`Mask`] (this table) and this. +/// [`Foreign::planes`](lance_graph_mask_risc::Foreign): a RESIDENT plane of +/// the foreign table, supplied by the caller alongside its `Planes`. See +/// [`Filter::Semijoin`] for the precondition this index carries — it must +/// never be a mask this crate's own [`lower`]/[`lower_fused`] produced for +/// another `Program`. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ForeignMask(pub u16); +pub struct ForeignPlane(pub u16); /// A FOREIGN table's VALUE lane — an index into -/// [`Foreign::lanes`](lance_graph_mask_risc::Foreign), the value-lane twin of -/// [`ForeignMask`]. Addressed through [`Agg::GroupSumViaI32`]: `SUM(l.amount) -/// GROUP BY p.country` names the group key as a foreign lane, resolved -/// through THIS table's own fk column — never a lane this crate reads as an -/// ordinary [`Col`]. +/// [`Foreign::lanes`](lance_graph_mask_risc::Foreign). Addressed through +/// [`Agg::GroupSumViaI32`] (`SUM(l.amount) GROUP BY p.country`) and +/// [`Filter::EqU32Via`] (`p.country = 3` read through `l.partner_id`) — both +/// resolve through THIS table's own fk column, never as an ordinary [`Col`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ForeignLane(pub u16); @@ -308,6 +311,24 @@ pub enum Filter { /// with no `WHERE`: the table is its validity plane, and a query over /// every row of it lowers to zero ops. Plane(Mask), + /// The fk join predicate in FACTORED form — `p.country = 3` read + /// through `l.partner_id`, lowered to [`Pred::EqU32Via`] over the + /// caller-supplied [`Foreign`](lance_graph_mask_risc::Foreign). A LEAF, + /// like [`Filter::Cmp`] / [`Filter::Plane`]: `Pred::EqU32Via` already + /// carries its own `under`, so this lowers exactly the way + /// [`Filter::Cmp(col, Cmp::EqU32(v))`](Cmp::EqU32) lowers to + /// [`Pred::EqU32`] — one `Node::Pred`, one gate, no second emitter arm. + /// No foreign plane is ever built and no mask is ever gathered: the + /// predicate is evaluated once per row, through the fk, in one facade + /// pass. + EqU32Via { + /// The foreign-key column on THIS table. + fk: Col, + /// Which foreign table's value lane the fk resolves into. + key: ForeignLane, + /// The value the resolved lane entry must equal. + v: u32, + }, /// The fk SEMIJOIN — `EXISTS(SELECT 1 FROM foreign f WHERE f.rid = /// this.fk AND )`, lowered to [`MaskOp::Gather`] over the /// caller-supplied [`Foreign`](lance_graph_mask_risc::Foreign). A LEAF, @@ -317,11 +338,23 @@ pub enum Filter { /// the scratch slot its `Gather` wrote (or, when `lower_fused` gives it /// its own slot, `AND`ed with a resident gate as a `BoolExpr::And`, /// exactly what happens to a gated `Cmp`). + /// + /// # Precondition: `foreign` names a RESIDENT plane, never a produced one + /// + /// `foreign` must index a plane the caller supplied in its own right — + /// the foreign table's validity, a focus, a class mask — never a mask + /// another `Program`'s `Keep` terminal just computed. `Gather` may + /// survive as a masking op only when its source is already legitimate + /// resident state, its output is tile-local, and no dynamically-created + /// population mask feeds it; a `Keep`-then-`Gather` pipeline across two + /// programs is exactly the forbidden shape — see [`Filter::EqU32Via`] + /// for the factored form that reads the foreign predicate directly and + /// needs no such mask at all. Semijoin { /// The foreign-key column on THIS table. fk: Col, - /// Which foreign table's kept mask the fk resolves into. - foreign: ForeignMask, + /// Which foreign table's RESIDENT kept plane the fk resolves into. + foreign: ForeignPlane, }, /// Every child must hold. And(Vec), @@ -342,8 +375,14 @@ impl Filter { Filter::Plane(mask) } - /// The fk semijoin leaf — see [`Filter::Semijoin`]. - pub fn semijoin(fk: Col, foreign: ForeignMask) -> Self { + /// The fk join leaf, factored — see [`Filter::EqU32Via`]. + pub fn eq_u32_via(fk: Col, key: ForeignLane, v: u32) -> Self { + Filter::EqU32Via { fk, key, v } + } + + /// The fk semijoin leaf — see [`Filter::Semijoin`], including the + /// precondition on `foreign`. + pub fn semijoin(fk: Col, foreign: ForeignPlane) -> Self { Filter::Semijoin { fk, foreign } } @@ -946,7 +985,10 @@ fn lower_with( /// The filter after the survivor-skip pass, before any slot is assigned. enum Node { - /// A comparison, evaluated under `under` when there is a gate. + /// A comparison, evaluated under `under` when there is a gate. The fk + /// join leaf ([`Filter::EqU32Via`]) lowers here too — `Pred::EqU32Via` + /// is just another `Pred` variant, so it needs no `Node` shape of its + /// own. Pred { pred: Pred, under: Option, @@ -1014,6 +1056,21 @@ fn gate_walk(f: &Filter, gate: Option) -> Result<(Node, bool), LowerError> gate.is_some(), ), Filter::Plane(m) => (Node::Plane(*m), gate == Some(*m)), + // Exactly the `Cmp` arm above: `Pred::EqU32Via` carries its own + // `under`, so this is a single `Node::Pred`, not a second `Node` + // shape — "vanishes with the gate" for the same reason a gated + // `Cmp` does. + Filter::EqU32Via { fk, key, v } => ( + Node::Pred { + pred: Pred::EqU32Via { + fk: fk.0, + key: key.0, + v: *v, + }, + under: gate, + }, + gate.is_some(), + ), // Same "vanishes with the gate" reasoning as `Cmp` above: both // emitters apply `under` as an explicit `AND` on the `Gather` // result, so the emitted value is literally `gather & gate` @@ -1450,9 +1507,10 @@ mod tests { /// the range claim would be untestable. addr: Vec, masks: Vec>, - /// Foreign tables' kept masks, indexed by `ForeignMask.0` — empty for - /// every single-table fixture; populated only by the two-table - /// semijoin tests. + /// Foreign tables' kept RESIDENT planes, indexed by + /// `ForeignPlane.0` — empty for every single-table fixture; + /// populated only by the two-table semijoin test, via a hand-built + /// plane fixture (never a mask this module's own `lower` produced). foreign: Vec>, /// The EXACT row count each `foreign[i]` spans — needed because a /// word-rounded length alone cannot distinguish "row 63 of a 64-row @@ -1460,8 +1518,8 @@ mod tests { foreign_rows: Vec, /// Foreign `u32` VALUE lanes, indexed by [`ForeignLane`] — opt-in and /// EMPTY for every single-table fixture, populated only by the - /// `GroupSumViaI32` test via [`Fx::push_foreign_lane`]. No existing - /// fixture's shape moves. + /// `GroupSumViaI32` and `EqU32Via` tests via [`Fx::push_foreign_lane`]. + /// No existing fixture's shape moves. foreign_lanes: Vec>, } @@ -1494,18 +1552,21 @@ mod tests { } } - /// Register a foreign table's kept mask at the next `ForeignMask` - /// index (`0`, `1`, …, in call order) and return that index. - fn push_foreign(&mut self, bits: Vec, rows: usize) -> ForeignMask { + /// Register a foreign table's kept RESIDENT plane at the next + /// `ForeignPlane` index (`0`, `1`, …, in call order) and return that + /// index. A hand-built fixture, never a mask [`lower`]/[`lower_fused`] + /// produced — that is precisely what [`Filter::Semijoin`]'s + /// precondition forbids. + fn push_foreign(&mut self, bits: Vec, rows: usize) -> ForeignPlane { let idx = self.foreign.len() as u16; self.foreign.push(bits); self.foreign_rows.push(rows); - ForeignMask(idx) + ForeignPlane(idx) } /// Register a foreign `u32` VALUE lane at the next `ForeignLane` - /// index (`0`, `1`, …, in call order) and return that index — the - /// [`Fx::push_foreign`] twin for [`ForeignLane`]/`GroupSumViaI32`. + /// index (`0`, `1`, …, in call order) and return that index — used + /// by both [`Agg::GroupSumViaI32`] and [`Filter::EqU32Via`]. fn push_foreign_lane(&mut self, values: Vec) -> ForeignLane { let idx = self.foreign_lanes.len() as u16; self.foreign_lanes.push(values); @@ -1567,6 +1628,11 @@ mod tests { Cmp::Range { lo, hi } => (lo as usize) <= row && row < (hi as usize), }, Filter::Plane(m) => self.bit(*m, row), + Filter::EqU32Via { fk, key, v } => { + let idx = self.u32_at(*fk, row) as usize; + let lane = &self.foreign_lanes[usize::from(key.0)]; + idx < lane.len() && lane[idx] == *v + } Filter::Semijoin { fk, foreign } => { let idx = self.u32_at(*fk, row) as usize; let bits = &self.foreign[usize::from(foreign.0)]; @@ -2635,12 +2701,78 @@ mod tests { assert_eq!(fx.reference(&fused, None), Value::Count(expected)); } + /// FAILS IF: `Filter::EqU32Via` disagrees with an independent per-row + /// reading (the oracle's `EqU32Via` arm), in EITHER lowering — the fk + /// join in FACTORED form: `CLASS` is the fk into a 4-row foreign VALUE + /// lane (shorter than `CLASS`'s own `0..5` range, so class 4 drops as + /// out-of-range), the resolved value compared against `3`. Anti-vacuity: + /// the bare predicate alone is selective (neither 0 nor every row), + /// composing it with `AND` narrows further still, and the fixture + /// exercises an out-of-range fk. + #[test] + fn eq_u32_via_matches_an_independent_per_row_reading() { + let mut fx = Fx::new(N); + let remap = vec![3u32, 1, 3, 2]; + let foreign_rows = remap.len(); + let foreign_lane = fx.push_foreign_lane(remap); + + let bare = Filter::eq_u32_via(CLASS, foreign_lane, 3); + let bare_expected = fx.rows(&bare).len(); + assert!( + bare_expected > 0 && bare_expected < N, + "the bare eq_u32_via selects {bare_expected}/{N} — not selective" + ); + assert!( + (0..fx.n()).any(|r| fx.classes[r] as usize >= foreign_rows), + "the fixture never exercises an out-of-range fk" + ); + + let f = Filter::and([Filter::cmp(VALS, Cmp::GtI32(0)), bare.clone()]); + let expected: i64 = (0..fx.n()) + .filter(|&r| fx.oracle(&f, r)) + .map(|r| i64::from(fx.alt[r])) + .sum(); + let narrowed = fx.rows(&f).len(); + assert!( + narrowed > 0 && narrowed < bare_expected, + "composing with AND must narrow: {narrowed} vs {bare_expected}" + ); + + let q = Query { + filter: f, + agg: Agg::SumI32(ALT), + }; + let remap = &fx.foreign_lanes[usize::from(foreign_lane.0)]; + let foreign_lanes = [LaneRef::U32(remap)]; + let mask_risc_foreign = lance_graph_mask_risc::Foreign { + planes: &[], + lanes: &foreign_lanes, + }; + for (label, program) in [ + ("in place", lower(&q).expect("lowers in place")), + ("fused", lower_fused(&q).expect("lowers fused")), + ] { + let mut scratch = Scratch::for_program(&program, fx.n()).expect("carves"); + let value = fx.with_planes(&[], |planes| { + lance_graph_mask_risc::execute_into( + &program, + planes, + &mask_risc_foreign, + &mut scratch, + lance_graph_mask_risc::Out::None, + ) + .expect("runs") + }); + assert_eq!(value, Value::SumI64(expected), "{label}: value"); + } + } + /// FAILS IF: `Filter::Semijoin` disagrees with an independent per-row - /// reading (the oracle's new `Semijoin` arm), in EITHER lowering — a - /// two-table shape: `CLASS` is the fk into a 5-row foreign table whose - /// kept mask selects classes `{1, 3}`. Anti-vacuity: the semijoin alone - /// is selective (neither 0 nor every row), and composing it with `AND` - /// narrows further still. + /// reading (the oracle's `Semijoin` arm), in EITHER lowering — a + /// two-table shape: `CLASS` is the fk into a 5-row foreign RESIDENT + /// plane whose kept mask selects classes `{1, 3}`. Anti-vacuity: the + /// semijoin alone is selective (neither 0 nor every row), and composing + /// it with `AND` narrows further still. #[test] fn semijoin_matches_an_independent_per_row_reading() { let mut fx = Fx::new(N); diff --git a/crates/lance-graph-quack/tests/duckdb/fixture.rs b/crates/lance-graph-quack/tests/duckdb/fixture.rs index 257b68394..9c3578221 100644 --- a/crates/lance-graph-quack/tests/duckdb/fixture.rs +++ b/crates/lance-graph-quack/tests/duckdb/fixture.rs @@ -268,17 +268,6 @@ pub mod col { /// same value as [`DOC_ID`], cast; `DOC_ID` stays for `range_docid`'s /// ordered compare. pub const DOC_ID_U32: Col = Col(7); - - /// `partner` table columns, over `partner`'s OWN row space - /// ([`super::PARTNER_ROWS`]) — a separate `Planes` from `line`'s. - /// `pgroup` (lane 1, see [`super::PartnerTable::lanes`]) has no named - /// constant here — no case in this suite filters on it — but the lane - /// itself stays borrowed so a future case can reach it as `Col(1)`. - pub mod partner { - use lance_graph_quack::Col; - - pub const COUNTRY: Col = Col(0); - } } /// The `line` table's lanes, borrowed and held so [`Planes`] can borrow them @@ -308,30 +297,10 @@ impl LineTable { } } -/// The `partner` table's lanes — see [`col::partner`]. -pub struct PartnerLanes<'a> { - lanes: [LaneRef<'a>; 2], -} - -impl PartnerTable { - /// Borrow every lane in the fixed order `col::partner` indexes into. - pub fn lanes(&self) -> PartnerLanes<'_> { - PartnerLanes { - lanes: [LaneRef::U32(&self.country), LaneRef::U32(&self.pgroup)], - } - } -} - -impl<'a> PartnerLanes<'a> { - /// Borrow `PARTNER_ROWS` rows over these lanes, no resident mask planes. - pub fn planes(&self) -> Planes<'_> { - Planes { - n_rows: PARTNER_ROWS, - masks: &[], - lanes: &self.lanes, - } - } -} +// The `partner` table is never run as its own `Planes`: its lanes are read +// THROUGH `line.partner_id` (`Filter::EqU32Via`, `Agg::GroupSumViaI32`) as +// `Foreign::lanes`, so no partner-side program — and no partner-side kept +// mask — ever exists. `pgroup` stays generated for the DuckDB oracle only. impl<'a> LineLanes<'a> { /// Borrow `LINE_ROWS` rows over these lanes, no resident mask planes — diff --git a/crates/lance-graph-quack/tests/duckdb_differential.rs b/crates/lance-graph-quack/tests/duckdb_differential.rs index d21dc56fd..b878789da 100644 --- a/crates/lance-graph-quack/tests/duckdb_differential.rs +++ b/crates/lance-graph-quack/tests/duckdb_differential.rs @@ -14,13 +14,15 @@ //! ONE program reading `partner.country` as a caller-owned //! [`Foreign::lanes`] entry, no two-hop materialisation and no K-program //! spelling. `join_sum_country` and `join_count_docs_with_posted` are the -//! other two join cases: the former lowers a real `Filter::Semijoin` (the -//! fk gather) against a real partner-side `Program`; the latter lowers -//! `Agg::ScatterOrU32` (the one-to-many hop back) into a caller-owned -//! mask, then counts it with a second tiny program. `group_sum_cc` runs -//! the one-terminal `Agg::GroupSumI32` (`Terminal::GroupSumI32`, ONE -//! program) alongside the pre-existing K-program `lower_group_by` reading, -//! printing both METRIC lines so the fold is visible. +//! other two join cases: the former lowers a real `Filter::EqU32Via` — the +//! fk join predicate in FACTORED form, `p.country` read straight through +//! `l.partner_id` in ONE program, no foreign plane, no gathered mask; the +//! latter lowers `Agg::ScatterOrU32` (the one-to-many hop back) into a +//! caller-owned mask, then counts it with a second tiny program. +//! `group_sum_cc` runs the one-terminal `Agg::GroupSumI32` +//! (`Terminal::GroupSumI32`, ONE program) alongside the pre-existing +//! K-program `lower_group_by` reading, printing both METRIC lines so the +//! fold is visible. #[path = "duckdb/fixture.rs"] mod fixture; @@ -30,16 +32,14 @@ use std::path::Path; use std::sync::atomic::{AtomicUsize, Ordering}; use lance_graph_mask_risc::{ - execute_into, materialize_rows, scratch_words_for, words_for, Foreign, ForeignPlane, LaneRef, - Out, Planes, Scratch, Terminal, Value, + execute_into, materialize_rows, scratch_words_for, words_for, Foreign, LaneRef, Out, Planes, + Scratch, Terminal, Value, }; use lance_graph_quack::{ - lower, lower_group_by, Agg, Cmp, Col, Filter, ForeignLane, ForeignMask, GroupBy, Query, + lower, lower_group_by, Agg, Cmp, Col, Filter, ForeignLane, GroupBy, Query, }; -use fixture::col::{ - partner::COUNTRY, AMOUNT, COST_CENTER, DOC_ID, DOC_ID_U32, GL_ACCOUNT, PARTNER_ID, QTY, STATUS, -}; +use fixture::col::{AMOUNT, COST_CENTER, DOC_ID, DOC_ID_U32, GL_ACCOUNT, PARTNER_ID, QTY, STATUS}; use fixture::DOC_ROWS; // --------------------------------------------------------------------- @@ -659,66 +659,37 @@ fn group_sum_cc() { } // --------------------------------------------------------------------- -// The join cases. `join_sum_country` (fk gather), +// The join cases. `join_sum_country` (the fk join predicate, factored), // `join_count_docs_with_posted` (the one-to-many hop back), and // `join_group_sum_country` (the fk-keyed group-sum) are all real, -// one-or-two-program lowerings now — no open seam remains. +// ONE-program lowerings now — no open seam, no forbidden intermediate +// mask remains. // --------------------------------------------------------------------- /// `SELECT SUM(l.amount) FROM line l JOIN partner p ON p.rid=l.partner_id -/// WHERE l.status=1 AND p.country=3` — two programs: the partner-side -/// filter kept as a mask, handed to the line-side program as a -/// [`ForeignPlane`] its `Filter::Semijoin` gathers through. +/// WHERE l.status=1 AND p.country=3` — ONE program: `p.country` is read +/// straight through `l.partner_id` by [`Filter::EqU32Via`] +/// (`Pred::EqU32Via`'s `foreign.lanes[key][fk[i]] == v`), so no partner-side +/// `Program` is ever run, no partner kept-mask is ever materialised, and no +/// `ForeignPlane`/`MaskOp::Gather` exists anywhere in this case — a mask +/// produced only because another fold needed it is forbidden intermediate +/// state. #[test] fn join_sum_country() { let cases = load_cases(); let fx = fixture::generate(); - // Phase 1 (partner side): `country == 3` -> Keep. - let partner_lanes = fx.partner.lanes(); - let partner_planes = partner_lanes.planes(); - let partner_program = lower(&Query { - filter: Filter::cmp(COUNTRY, Cmp::EqU32(3)), - agg: Agg::Rows, - }) - .expect("lowers"); - let mut p_scratch = - Scratch::for_program(&partner_program, partner_planes.n_rows).expect("carves"); - let p_tile_words = p_scratch.words(); - let p_scratch_words = scratch_words_for(p_tile_words, p_scratch.slots()).expect("sized"); - // Phase 1 is a `Terminal::Keep` — its kept bits land in this owned - // `Out::Mask` buffer, never in a scratch slot. - let mut partner_bits = vec![0u64; words_for(partner_planes.n_rows)]; - let before1 = BYTES.load(Ordering::Relaxed); - let p_value = execute_into( - &partner_program, - &partner_planes, - &Foreign::NONE, - &mut p_scratch, - Out::Mask(&mut partner_bits), - ) - .expect("runs"); - let alloc1 = alloc_delta(before1); - match p_value { - Value::Mask(_) => {} - other => panic!("partner-side filter did not Keep a mask: {other:?}"), - } - - let foreign_plane = ForeignPlane { - words: &partner_bits, - rows: fixture::PARTNER_ROWS, - }; + let lanes = fx.line.lanes(); + let planes = lanes.planes(); + let country_lane = [LaneRef::U32(&fx.partner.country)]; let foreign = Foreign { - planes: &[foreign_plane], - lanes: &[], + planes: &[], + lanes: &country_lane, }; - // Phase 2 (line side): `status == 1 AND Semijoin(partner_id, 0)` -> SUM. - let lanes = fx.line.lanes(); - let planes = lanes.planes(); let line_filter = Filter::and([ Filter::cmp(STATUS, Cmp::EqU32(1)), - Filter::semijoin(PARTNER_ID, ForeignMask(0)), + Filter::eq_u32_via(PARTNER_ID, ForeignLane(0), 3), ]); let line_program = lower(&Query { filter: line_filter, @@ -726,11 +697,12 @@ fn join_sum_country() { }) .expect("lowers"); let mut scratch = Scratch::for_program(&line_program, planes.n_rows).expect("carves"); - let scratch_words = scratch_words_for(scratch.words(), scratch.slots()).expect("sized"); - let before2 = BYTES.load(Ordering::Relaxed); + let tile_words = scratch.words(); + let scratch_words = scratch_words_for(tile_words, scratch.slots()).expect("sized"); + let before = BYTES.load(Ordering::Relaxed); let value = execute_into(&line_program, &planes, &foreign, &mut scratch, Out::None).expect("runs"); - let alloc2 = alloc_delta(before2); + let alloc_bytes = alloc_delta(before); let encoded = match value { Value::SumI64(s) => s.to_string(), @@ -738,17 +710,19 @@ fn join_sum_country() { }; let out_bytes = encoded.len(); let m = CaseMetrics { - ops: partner_program.ops.len() + line_program.ops.len(), - scratch_words: p_scratch_words + scratch_words, - tile_words: p_tile_words, - alloc_bytes_exec: alloc1 + alloc2, + ops: line_program.ops.len(), + scratch_words, + tile_words, + alloc_bytes_exec: alloc_bytes, rows_materialized: 0, index_vec_len: 0, out_bytes, - programs: Some(2), + programs: Some(1), // No N×M pair-relation object exists anywhere in this case: the fk - // resolves through one `MaskOp::Gather` read of the foreign plane - // per line row, never a materialized `(line, partner)` pair list. + // resolves through one `Pred::EqU32Via` read of the foreign VALUE + // lane per line row, never a materialized `(line, partner)` pair + // list — and, since the partner-side plane the old two-program + // shape built is gone too, no foreign MASK ever exists either. pair_relation_bytes: 0, }; print_metric("join_sum_country", &m); From 1a8c9e34b2d52f4a31487452ca823208406c05e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 22:00:12 +0000 Subject: [PATCH 09/15] =?UTF-8?q?mask-risc=20+=20quack:=20COUNT=20DISTINCT?= =?UTF-8?q?=20folds=20in=20one=20program=20=E2=80=94=20clustered=20run=20f?= =?UTF-8?q?old,=20or=20the=20minimal=20seen-set?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Terminal::CountKeyRunsU32 { mask, lane }`: on a key-clustered lane the distinct count over selected rows is the number of key runs with a hit, folded tile by tile with a two-word carry (ndarray `masked_key_run_count_u32` / `KeyRunCarry`), `Out::None`, no population state. Oracle arm is an independent first-selected-row-per-run scan. `tests/distinct.rs` carries the pigeonhole falsifier: for every pair of distinct key sets there is a suffix on which the exact counts differ, so any exact fold over an UNCLUSTERED lane separates all 2^K key sets and holds at least one bit per key — `ScatterCountU32`'s accumulator is the minimum, not an avoidable intermediate. The run fold is shown to over-count on such a lane (the precondition is real). Quack: `Agg::CountDistinctU32 { key, universe }` → `ScatterCountU32`, `Agg::CountDistinctClusteredU32 { key }` → `CountKeyRunsU32`. `ScatterOrU32`'s doc now states its survival condition (the target mask is the demanded result, never consumed by a second program). Harness: `join_count_docs_with_posted` is one program on both layouts — the generated (random `doc_id`) fixture through `ScatterCountU32` (`population_state_bytes=64`, reported), and the same lines stored under their doc through `CountKeyRunsU32` (`population_state_bytes=0`) — both against the same DuckDB 511. The doc-bitmap → second-program shape is deleted. METRIC gains `population_state_bytes`. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/lance-graph-mask-risc/src/exec.rs | 19 +- crates/lance-graph-mask-risc/src/ir.rs | 38 ++- crates/lance-graph-mask-risc/src/reference.rs | 22 ++ .../lance-graph-mask-risc/tests/distinct.rs | 268 ++++++++++++++++++ crates/lance-graph-quack/src/lib.rs | 46 ++- .../tests/duckdb_differential.rs | 181 ++++++++---- 6 files changed, 501 insertions(+), 73 deletions(-) create mode 100644 crates/lance-graph-mask-risc/tests/distinct.rs diff --git a/crates/lance-graph-mask-risc/src/exec.rs b/crates/lance-graph-mask-risc/src/exec.rs index 38b774237..45b66367d 100644 --- a/crates/lance-graph-mask-risc/src/exec.rs +++ b/crates/lance-graph-mask-risc/src/exec.rs @@ -28,10 +28,11 @@ use ndarray::simd::{ le_i32_to_mask, le_i32_to_mask_under, lt_i32_to_mask, lt_i32_to_mask_under, mask_all, mask_and, mask_and_assign, mask_andnot, mask_andnot_assign, mask_any, mask_gather_u32, mask_not, mask_not_assign, mask_or, mask_or_assign, mask_scatter_or_u32, mask_set_range, mask_xor, - mask_xor_assign, masked_group_sum_i32, masked_group_sum_i32_via, masked_max_i32, - masked_min_i32, masked_sum_i32, ne_i32_to_mask, ne_i32_to_mask_under, ne_u32_to_mask, - ne_u32_to_mask_under, popcount_batch_u64, ternary_match_u32_to_mask, + mask_xor_assign, masked_group_sum_i32, masked_group_sum_i32_via, masked_key_run_count_u32, + masked_max_i32, masked_min_i32, masked_sum_i32, ne_i32_to_mask, ne_i32_to_mask_under, + ne_u32_to_mask, ne_u32_to_mask_under, popcount_batch_u64, ternary_match_u32_to_mask, ternary_match_u32_to_mask_under, ternary_match_u64_to_mask, ternary_match_u64_to_mask_under, + KeyRunCarry, }; use crate::ir::{ @@ -733,6 +734,10 @@ pub fn execute_into( let mut sum = 0i64; let mut min: Option = None; let mut max: Option = None; + // The whole state of a key-clustered distinct count: the open run's key + // and whether it was hit. Two words, however many rows. + let mut run_carry = KeyRunCarry::default(); + let mut runs = 0usize; let mut w0 = 0usize; while w0 < words { @@ -933,6 +938,13 @@ pub fn execute_into( ); } } + Terminal::CountKeyRunsU32 { mask, lane } => { + runs += masked_key_run_count_u32( + lane_u32(planes, lane, t), + read(planes, &slots, mask, t), + &mut run_carry, + ); + } Terminal::GroupSumI32 { mask, key, val } => { // `validate` already refused a missing or too-small `out`; // the kernel adds into it, tile after tile. @@ -984,6 +996,7 @@ pub fn execute_into( Out::Mask(o) => popcount_batch_u64(o) as usize, _ => 0, }), + Terminal::CountKeyRunsU32 { .. } => Value::Count(runs + run_carry.finish()), Terminal::GroupSumI32 { .. } | Terminal::GroupSumViaI32 { .. } => Value::GroupSummed, Terminal::Keep { mask } => Value::Mask(mask), }) diff --git a/crates/lance-graph-mask-risc/src/ir.rs b/crates/lance-graph-mask-risc/src/ir.rs index 15c6de59c..00ebc3e0e 100644 --- a/crates/lance-graph-mask-risc/src/ir.rs +++ b/crates/lance-graph-mask-risc/src/ir.rs @@ -253,8 +253,17 @@ pub enum Terminal { /// out_rows`. Writes `out[i] |= lane[i]`'s target, union across /// repeats — [`ndarray::simd::mask_scatter_or_u32`]'s contract, out of /// range silently dropped. The caller's buffer must be exactly - /// `words_for(out_rows)` long; a follow-on program then reads it back - /// as a [`ForeignPlane`] or an input [`Planes::masks`] entry. + /// `words_for(out_rows)` long. + /// + /// **Survival condition:** the scattered mask is legal only when it IS + /// the externally demanded result (a `hop` whose answer is the target + /// population's mask). It is never an intermediate: a follow-on program + /// or fold that consumes it is the forbidden + /// projection → population → projection shape. A count over the + /// targets is [`Terminal::ScatterCountU32`] (unclustered key) or + /// [`Terminal::CountKeyRunsU32`] (key-clustered lane); a filter through + /// the targets is [`Pred::EqU32Via`] / [`MaskOp::Gather`] over a + /// RESIDENT plane. ScatterOrU32 { mask: Operand, lane: u16, @@ -269,11 +278,35 @@ pub enum Terminal { /// reads it: `docs WHERE EXISTS line … ` counted, without the doc mask /// becoming an intermediate. (When the mask itself is the demanded /// result — a `hop` — use [`Terminal::ScatterOrU32`].) + /// + /// The accumulator is population-sized (one bit per key of the + /// universe) and that is the MINIMUM for an exact distinct count over an + /// UNCLUSTERED key lane: `tests/distinct.rs`'s pigeonhole falsifier shows + /// two prefixes with different key sets always diverge under some + /// suffix, so any exact fold's state separates all `2^K` key sets. When + /// the key lane is clustered — equal keys contiguous, the address order + /// a projection stores a child population under its parent — use + /// [`Terminal::CountKeyRunsU32`], which needs two words of state. ScatterCountU32 { mask: Operand, lane: u16, out_rows: u32, }, + /// `COUNT(DISTINCT lane[i])` over the rows where `mask` holds, on a + /// KEY-CLUSTERED lane — every run of equal consecutive `lane` values is + /// one key, so the count is the number of runs containing a selected + /// row, folded tile by tile with a two-word carry + /// ([`ndarray::simd::masked_key_run_count_u32`] + `KeyRunCarry`): no + /// population-sized set, no `Out` buffer, `Out::None`. The answer is + /// [`Value::Count`]. + /// + /// Clustering is the CALLER's precondition (the lane is stored in key + /// order — the T0 address projection, or a witnessed ordered lane). On a + /// lane that is not clustered this counts runs, not keys, and + /// over-counts; the executor cannot check clustering without the very + /// seen-set this terminal exists to avoid, so it does not try. For an + /// unclustered lane use [`Terminal::ScatterCountU32`]. + CountKeyRunsU32 { mask: Operand, lane: u16 }, /// The one-terminal `GROUP BY … SUM`: for every row `i` where `mask` /// holds, adds `val[i]` into the caller's `Out::I64` buffer at index /// `key[i]` — provided `key[i] < out.len()` @@ -415,6 +448,7 @@ impl Program { | Terminal::BlendI32 { mask, .. } | Terminal::ScatterOrU32 { mask, .. } | Terminal::ScatterCountU32 { mask, .. } + | Terminal::CountKeyRunsU32 { mask, .. } | Terminal::GroupSumI32 { mask, .. } | Terminal::GroupSumViaI32 { mask, .. } | Terminal::Keep { mask } => touch(mask), diff --git a/crates/lance-graph-mask-risc/src/reference.rs b/crates/lance-graph-mask-risc/src/reference.rs index 65cfb95c8..584a00320 100644 --- a/crates/lance-graph-mask-risc/src/reference.rs +++ b/crates/lance-graph-mask-risc/src/reference.rs @@ -404,6 +404,11 @@ pub(crate) fn validate( check_operand(p, planes, mask)?; written_slots.readable(mask) } + Terminal::CountKeyRunsU32 { mask, lane } => { + check_operand(p, planes, mask)?; + written_slots.readable(mask)?; + check_lane(planes, lane, LaneKind::U32) + } Terminal::Keep { mask } => { check_operand(p, planes, mask)?; written_slots.readable(mask)?; @@ -745,6 +750,23 @@ pub fn reference_execute_into( let survivors = |mask: Operand| (0..n).filter(move |&r| rows.bit(planes, mask, r)); Ok(match p.terminal { Terminal::Count { mask } => Value::Count(survivors(mask).count()), + Terminal::CountKeyRunsU32 { mask, lane } => { + // Independent formulation: a selected row is counted iff it is + // the FIRST selected row of its run (no selected row between the + // run's start and it). Row-at-a-time, no carry object. + let n = planes.n_rows; + let mut count = 0usize; + let mut run_start = 0usize; + for r in 0..n { + if r > 0 && u32_at(planes, lane, r) != u32_at(planes, lane, r - 1) { + run_start = r; + } + if rows.bit(planes, mask, r) && !(run_start..r).any(|q| rows.bit(planes, mask, q)) { + count += 1; + } + } + Value::Count(count) + } Terminal::Any { mask } => Value::Bool(survivors(mask).next().is_some()), Terminal::All { mask } => Value::Bool(survivors(mask).count() == n), Terminal::MaskedSumI32 { mask, lane } => Value::SumI64( diff --git a/crates/lance-graph-mask-risc/tests/distinct.rs b/crates/lance-graph-mask-risc/tests/distinct.rs new file mode 100644 index 000000000..7946dbce3 --- /dev/null +++ b/crates/lance-graph-mask-risc/tests/distinct.rs @@ -0,0 +1,268 @@ +//! `COUNT(DISTINCT key)` without an intermediate population mask. +//! +//! Two terminals answer it, and which one is legal is a property of the KEY +//! LANE'S LAYOUT, not of the query: +//! +//! - `Terminal::CountKeyRunsU32` — a key-clustered lane (equal keys +//! contiguous). Two words of state, tile by tile. Proven here against the +//! oracle and an independent seen-set, across tilings. +//! - `Terminal::ScatterCountU32` — an unclustered lane. Its accumulator is +//! one bit per key of the universe, and the pigeonhole falsifier below +//! shows that is the MINIMUM any exact fold can carry on such a lane, so +//! the accumulator is the fold's own sufficient statistic, not an +//! avoidable intermediate. + +use lance_graph_mask_risc::exec::{execute_into, Scratch}; +use lance_graph_mask_risc::reference::reference_execute_into; +use lance_graph_mask_risc::{ + scratch_words_for, words_for, Foreign, LaneRef, MaskOp, Operand, Out, Planes, Pred, Program, + Terminal, Value, +}; +use std::collections::BTreeSet; + +fn lcg(seed: &mut u64) -> u64 { + *seed = seed + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + *seed >> 11 +} + +const S0: Operand = Operand::Scratch(0); + +/// `WHERE status == 1` then the distinct fold over `key`. +fn program(terminal: fn(Operand) -> Terminal) -> Program { + Program::new( + vec![MaskOp::Pred { + pred: Pred::EqU32 { lane: 0, v: 1 }, + under: None, + dst: 0, + }], + terminal(S0), + ) +} + +fn seen_set(status: &[u32], key: &[u32]) -> usize { + status + .iter() + .zip(key) + .filter(|(s, _)| **s == 1) + .map(|(_, k)| *k) + .collect::>() + .len() +} + +fn run(program: &Program, planes: &Planes<'_>, tile_words: usize) -> usize { + let slots = program.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(tile_words, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, tile_words, slots).expect("carves"); + match execute_into(program, planes, &Foreign::NONE, &mut scratch, Out::None).expect("runs") { + Value::Count(c) => c, + other => panic!("expected a count, got {other:?}"), + } +} + +fn oracle(program: &Program, planes: &Planes<'_>) -> usize { + match reference_execute_into(program, planes, &Foreign::NONE, Out::None).expect("oracle runs") { + Value::Count(c) => c, + other => panic!("expected a count, got {other:?}"), + } +} + +#[test] +fn on_a_clustered_key_lane_the_run_fold_is_the_distinct_count_under_every_tiling() { + let mut seed = 0xD15u64; + for &n in &[1usize, 63, 64, 65, 500, 4096] { + let universe = 97u32; + let mut key: Vec = (0..n) + .map(|_| (lcg(&mut seed) % u64::from(universe)) as u32) + .collect(); + key.sort_unstable(); // clustered: equal keys contiguous + let status: Vec = (0..n).map(|_| (lcg(&mut seed) % 3) as u32).collect(); + let lanes = [LaneRef::U32(&status), LaneRef::U32(&key)]; + let planes = Planes { + n_rows: n, + masks: &[], + lanes: &lanes, + }; + let p = program(|m| Terminal::CountKeyRunsU32 { mask: m, lane: 1 }); + let want = seen_set(&status, &key); + assert_eq!(oracle(&p, &planes), want, "n={n}: oracle vs seen-set"); + for tile_words in [1usize, 2, 8, words_for(n).max(1)] { + assert_eq!( + run(&p, &planes, tile_words), + want, + "n={n} tile_words={tile_words}" + ); + } + } +} + +#[test] +fn a_run_split_across_a_tile_edge_is_counted_once_and_an_unhit_run_never() { + // 130 rows: key 5 spans rows 60..70 (crosses the first 64-row tile + // edge), selected at rows 61 and 68 only; key 6 is never selected. + let n = 130usize; + let key: Vec = (0..n) + .map(|i| match i { + 0..=59 => 1, + 60..=69 => 5, + 70..=99 => 6, + _ => 9, + }) + .collect(); + let mut status = vec![0u32; n]; + status[61] = 1; + status[68] = 1; + status[100] = 1; + let lanes = [LaneRef::U32(&status), LaneRef::U32(&key)]; + let planes = Planes { + n_rows: n, + masks: &[], + lanes: &lanes, + }; + let p = program(|m| Terminal::CountKeyRunsU32 { mask: m, lane: 1 }); + assert_eq!( + run(&p, &planes, 1), + 2, + "keys 5 and 9; key 5 once, key 6 never" + ); + assert_eq!(oracle(&p, &planes), 2); +} + +#[test] +fn on_an_unclustered_lane_the_run_fold_over_counts_and_scatter_count_is_exact() { + let mut seed = 0xBADu64; + let n = 2048usize; + let universe = 64u32; + let key: Vec = (0..n) + .map(|_| (lcg(&mut seed) % u64::from(universe)) as u32) + .collect(); + let status: Vec = (0..n).map(|_| (lcg(&mut seed) % 2) as u32).collect(); + let lanes = [LaneRef::U32(&status), LaneRef::U32(&key)]; + let planes = Planes { + n_rows: n, + masks: &[], + lanes: &lanes, + }; + let want = seen_set(&status, &key); + assert!(want > 2, "fixture must have several distinct keys"); + + let runs = program(|m| Terminal::CountKeyRunsU32 { mask: m, lane: 1 }); + let over = run(&runs, &planes, 8); + assert!( + over > want, + "unclustered: runs ({over}) must exceed keys ({want})" + ); + assert_eq!( + oracle(&runs, &planes), + over, + "executor and oracle agree on the run count" + ); + + let scatter = Program::new( + runs.ops.clone(), + Terminal::ScatterCountU32 { + mask: S0, + lane: 1, + out_rows: universe, + }, + ); + let mut sink = vec![0u64; words_for(universe as usize)]; + let slots = scatter.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(8, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, 8, slots).expect("carves"); + let got = execute_into( + &scatter, + &planes, + &Foreign::NONE, + &mut scratch, + Out::Mask(&mut sink), + ) + .expect("runs"); + assert_eq!(got, Value::Count(want)); +} + +/// The pigeonhole falsifier for "an exact distinct count over an +/// UNCLUSTERED key lane can be folded with less than one bit per key". +/// +/// Take a universe of `K` keys. For every pair of DISTINCT key sets +/// `A ≠ B`, build the prefix streams "one selected row per key of A" and +/// "… of B". One of the two sets is not contained in the other — say +/// `A ⊄ B` — and the suffix "one selected row per key of B" then yields +/// `|A ∪ B| > |B|` after prefix A but exactly `|B|` after prefix B. The +/// exact answers differ, so any fold that is exact on every stream must be +/// in DIFFERENT states after the two prefixes; its state distinguishes all +/// `2^K` key sets, hence carries at least `K` bits — one per key of the +/// universe, exactly `ScatterCountU32`'s accumulator. Row order was +/// arbitrary throughout, so no order-free fold escapes the bound; only a +/// lane whose ORDER already encodes the key set (clustering) does, and that +/// is `CountKeyRunsU32`'s precondition, not a smaller state. +#[test] +fn pigeonhole_every_pair_of_key_sets_diverges_under_some_suffix() { + const K: u32 = 6; + let subsets = 1u32 << K; + let keys_of = |set: u32| -> Vec { (0..K).filter(|k| set >> k & 1 == 1).collect() }; + let exact = |prefix: u32, suffix: u32| -> usize { (prefix | suffix).count_ones() as usize }; + // Run the real terminal on the real streams, not the closure alone: the + // fold under test must reproduce the divergence — and it does, because + // its state IS the key set. + let count_with = |prefix: u32, suffix: u32| -> usize { + let mut key = keys_of(prefix); + key.extend(keys_of(suffix)); + let status = vec![1u32; key.len()]; + let lanes = [LaneRef::U32(&status), LaneRef::U32(&key)]; + let planes = Planes { + n_rows: key.len(), + masks: &[], + lanes: &lanes, + }; + let p = Program::new( + vec![MaskOp::Pred { + pred: Pred::EqU32 { lane: 0, v: 1 }, + under: None, + dst: 0, + }], + Terminal::ScatterCountU32 { + mask: S0, + lane: 1, + out_rows: K, + }, + ); + let mut sink = vec![0u64; 1]; + let mut buf = vec![0u64; scratch_words_for(1, 1).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, 1, 1).expect("carves"); + match execute_into( + &p, + &planes, + &Foreign::NONE, + &mut scratch, + Out::Mask(&mut sink), + ) + .expect("runs") + { + Value::Count(c) => c, + other => panic!("{other:?}"), + } + }; + let mut pairs = 0usize; + for a in 0..subsets { + for b in (a + 1)..subsets { + // The suffix is the WHOLE of whichever set the other does not + // contain — that is the stream on which the two prefixes must + // answer differently. + let suffix = if a & !b != 0 { b } else { a }; + let (ea, eb) = (exact(a, suffix), exact(b, suffix)); + assert_ne!( + ea, eb, + "A={a:06b} B={b:06b} suffix={suffix:06b}: exact counts must differ" + ); + assert_eq!(count_with(a, suffix), ea); + assert_eq!(count_with(b, suffix), eb); + pairs += 1; + } + } + // Every one of the 2^K·(2^K−1)/2 pairs is separated ⇒ ≥ 2^K states ⇒ + // ≥ K bits of state. That is the accumulator width, to the bit. + assert_eq!(pairs, (subsets as usize) * (subsets as usize - 1) / 2); + assert!(words_for(K as usize) * 64 >= K as usize); +} diff --git a/crates/lance-graph-quack/src/lib.rs b/crates/lance-graph-quack/src/lib.rs index 1d7c09853..069b21306 100644 --- a/crates/lance-graph-quack/src/lib.rs +++ b/crates/lance-graph-quack/src/lib.rs @@ -809,11 +809,13 @@ pub enum Agg { /// The lane read where it does not. els: Col, }, - /// The one-to-many hop back: for every surviving row, sets bit `fk[row]` - /// of a caller-supplied `Out::Mask` buffer of `words_for(out_rows)` — - /// `Terminal::ScatterOrU32`. `"docs that have a posted line"` is - /// `Agg::ScatterOrU32 { fk: doc_id, out_rows: DOC_ROWS }` over a - /// `status == 1` filter. + /// The one-to-many hop back AS THE DEMANDED RESULT: for every surviving + /// row, sets bit `fk[row]` of a caller-supplied `Out::Mask` buffer of + /// `words_for(out_rows)` — `Terminal::ScatterOrU32`. Legal only when + /// that target mask IS what the query asks for (a `hop`). A query that + /// asks for a COUNT over the targets is [`Agg::CountDistinctU32`] / + /// [`Agg::CountDistinctClusteredU32`]; handing this mask to a second + /// program is the forbidden population intermediate. ScatterOrU32 { /// The foreign-key column naming the target row on the OTHER table. fk: Col, @@ -821,6 +823,32 @@ pub enum Agg { /// must be exactly `words_for(out_rows)` long. out_rows: u32, }, + /// `COUNT(DISTINCT key)` over the surviving rows on an UNCLUSTERED key + /// lane — `Terminal::ScatterCountU32`. `SELECT COUNT(*) FROM doc WHERE + /// EXISTS(line … doc_id = doc.rid AND status = 1)` is + /// `Agg::CountDistinctU32 { key: doc_id, universe: DOC_ROWS }`: ONE + /// program whose accumulator (the caller's `Out::Mask`, one bit per key + /// of `universe`) never leaves the fold — only its popcount does. That + /// accumulator is the minimum exact state on an unclustered lane + /// (mask-risc `tests/distinct.rs`, the pigeonhole falsifier); a key at or + /// past `universe` is dropped, not an error. + CountDistinctU32 { + /// The key column (`u32`) whose distinct values are counted. + key: Col, + /// The key universe — the accumulator is `words_for(universe)`. + universe: u32, + }, + /// `COUNT(DISTINCT key)` on a KEY-CLUSTERED lane (equal keys contiguous — + /// the address order a projection stores a child population under its + /// parent) — `Terminal::CountKeyRunsU32`, two words of state, `Out::None`. + /// Clustering is the caller's precondition, exactly as + /// [`Filter::prefix_facet`]'s witnessed range carries its row-order + /// obligation: on a lane that is not clustered this counts runs and + /// over-counts. Use [`Agg::CountDistinctU32`] there. + CountDistinctClusteredU32 { + /// The key column (`u32`), stored in key order. + key: Col, + }, /// The one-terminal `GROUP BY key SUM(val)` — `Terminal::GroupSumI32`, /// ONE program instead of `lower_group_by`'s K. The caller supplies an /// `Out::I64` buffer whose length IS the group universe; a `key` at or @@ -1437,6 +1465,12 @@ fn terminal_of(agg: Agg, mask: Operand) -> Terminal { lane: fk.0, out_rows, }, + Agg::CountDistinctU32 { key, universe } => Terminal::ScatterCountU32 { + mask, + lane: key.0, + out_rows: universe, + }, + Agg::CountDistinctClusteredU32 { key } => Terminal::CountKeyRunsU32 { mask, lane: key.0 }, Agg::GroupSumI32 { key, val } => Terminal::GroupSumI32 { mask, key: key.0, @@ -1995,6 +2029,8 @@ mod tests { | Terminal::MaskedMaxI32 { mask, .. } | Terminal::BlendI32 { mask, .. } | Terminal::ScatterOrU32 { mask, .. } + | Terminal::ScatterCountU32 { mask, .. } + | Terminal::CountKeyRunsU32 { mask, .. } | Terminal::GroupSumI32 { mask, .. } | Terminal::GroupSumViaI32 { mask, .. } | Terminal::Keep { mask } if mask == gate diff --git a/crates/lance-graph-quack/tests/duckdb_differential.rs b/crates/lance-graph-quack/tests/duckdb_differential.rs index b878789da..324904baa 100644 --- a/crates/lance-graph-quack/tests/duckdb_differential.rs +++ b/crates/lance-graph-quack/tests/duckdb_differential.rs @@ -152,6 +152,13 @@ struct CaseMetrics { /// row-pair list. Named explicitly (rather than left implicit) so the /// absence is a printed fact, not an inference from a missing field. pair_relation_bytes: usize, + /// Bytes of POPULATION-SIZED state a fold carried that is neither the + /// tile scratch nor the encoded answer — the accumulator of a + /// `ScatterCountU32` (one bit per key of the universe). Printed so the + /// one case that carries such state says so in numbers; every other + /// case is `0`. The law's "no population intermediate" is checked + /// against THIS field, not against `alloc_bytes_exec`. + population_state_bytes: usize, } fn print_metric(id: &str, m: &CaseMetrics) { @@ -160,7 +167,7 @@ fn print_metric(id: &str, m: &CaseMetrics) { Some(k) => eprintln!( "METRIC case={id} ops={} scratch_words={} tile_words={} scratch_bytes={} \ alloc_bytes_exec={} rows_materialized={} index_vec_len={} out_bytes={} \ - pair_relation_bytes={} programs={k}", + pair_relation_bytes={} population_state_bytes={} programs={k}", m.ops, m.scratch_words, m.tile_words, @@ -170,11 +177,12 @@ fn print_metric(id: &str, m: &CaseMetrics) { m.index_vec_len, m.out_bytes, m.pair_relation_bytes, + m.population_state_bytes, ), None => eprintln!( "METRIC case={id} ops={} scratch_words={} tile_words={} scratch_bytes={} \ alloc_bytes_exec={} rows_materialized={} index_vec_len={} out_bytes={} \ - pair_relation_bytes={}", + pair_relation_bytes={} population_state_bytes={}", m.ops, m.scratch_words, m.tile_words, @@ -184,6 +192,7 @@ fn print_metric(id: &str, m: &CaseMetrics) { m.index_vec_len, m.out_bytes, m.pair_relation_bytes, + m.population_state_bytes, ), } } @@ -232,9 +241,7 @@ fn run_query(id: &str, planes: &Planes<'_>, filter: Filter, agg: Agg) -> (String (s, n, n) } Value::Blended => panic!("case {id}: no BlendI32 case in this suite"), - Value::Scattered => panic!( - "case {id}: run_query doesn't handle ScatterOrU32 — see join_count_docs_with_posted" - ), + Value::Scattered => panic!("case {id}: no ScatterOrU32 case in this suite"), Value::GroupSummed => panic!( "case {id}: run_query doesn't handle GroupSumI32 — see group_sum_cc's one-terminal arm" ), @@ -252,6 +259,7 @@ fn run_query(id: &str, planes: &Planes<'_>, filter: Filter, agg: Agg) -> (String out_bytes, programs: None, pair_relation_bytes: 0, + population_state_bytes: 0, }, ) } @@ -346,6 +354,7 @@ fn run_group( out_bytes, programs: Some(plan.groups.len()), pair_relation_bytes: 0, + population_state_bytes: 0, }, ) } @@ -639,6 +648,7 @@ fn group_sum_cc() { out_bytes, programs: Some(1), pair_relation_bytes: 0, + population_state_bytes: 0, }; print_metric("group_sum_cc", &m); assert_case(&cases, "group_sum_cc", &encoded); @@ -724,99 +734,143 @@ fn join_sum_country() { // list — and, since the partner-side plane the old two-program // shape built is gone too, no foreign MASK ever exists either. pair_relation_bytes: 0, + population_state_bytes: 0, }; print_metric("join_sum_country", &m); assert_case(&cases, "join_sum_country", &encoded); } /// `SELECT COUNT(*) FROM doc d WHERE EXISTS(SELECT 1 FROM line l WHERE -/// l.doc_id=d.rid AND l.status=1)` — the one-to-many hop BACK: -/// `Agg::ScatterOrU32` sets bit `doc_id` of an 8-word `Out::Mask` buffer for -/// every posted line, then a tiny doc-side program counts the buffer as a -/// resident plane. +/// l.doc_id=d.rid AND l.status=1)` — `COUNT(DISTINCT doc_id)` over the +/// posted lines, ONE program either way; which terminal is legal is a +/// property of the KEY LANE'S LAYOUT, and both layouts are run here against +/// the same DuckDB answer: +/// +/// - the fixture as generated (`doc_id` random — unclustered): +/// [`Agg::CountDistinctU32`] → `Terminal::ScatterCountU32`. The fold's +/// accumulator is one bit per doc (`population_state_bytes = 64`), the +/// minimum exact state on an unclustered lane (mask-risc +/// `tests/distinct.rs`' pigeonhole falsifier); it never leaves the fold, +/// only its popcount does. +/// - the same lines stored under their doc (`doc_id` clustered — the address +/// order of a child population under its parent): +/// [`Agg::CountDistinctClusteredU32`] → `Terminal::CountKeyRunsU32`, two +/// words of state, `Out::None`, `population_state_bytes = 0`. +/// +/// The old shape — `ScatterOrU32` into a doc bitmap read back by a second +/// `Count` program — is gone: a population mask consumed by the next fold is +/// forbidden intermediate state whatever it was called. #[test] fn join_count_docs_with_posted() { let cases = load_cases(); let fx = fixture::generate(); + + // ── Arm 1: the generated (unclustered) layout — ScatterCountU32. ── let lanes = fx.line.lanes(); let planes = lanes.planes(); - - // Phase 1 (line side): `status == 1` -> ScatterOrU32(doc_id, DOC_ROWS). - let line_program = lower(&Query { + let program = lower(&Query { filter: Filter::cmp(STATUS, Cmp::EqU32(1)), - agg: Agg::ScatterOrU32 { - fk: DOC_ID_U32, - out_rows: DOC_ROWS as u32, + agg: Agg::CountDistinctU32 { + key: DOC_ID_U32, + universe: DOC_ROWS as u32, }, }) .expect("lowers"); - let mut scratch = Scratch::for_program(&line_program, planes.n_rows).expect("carves"); + assert!(matches!(program.terminal, Terminal::ScatterCountU32 { .. })); + let mut scratch = Scratch::for_program(&program, planes.n_rows).expect("carves"); let tile_words = scratch.words(); let scratch_words = scratch_words_for(tile_words, scratch.slots()).expect("sized"); - let mut scattered = vec![0u64; words_for(DOC_ROWS)]; - let before1 = BYTES.load(Ordering::Relaxed); - let value1 = execute_into( - &line_program, + // The fold's own accumulator: one bit per doc. Counted below as + // population state, because that is what it is. + let mut sink = vec![0u64; words_for(DOC_ROWS)]; + let before = BYTES.load(Ordering::Relaxed); + let value = execute_into( + &program, &planes, &Foreign::NONE, &mut scratch, - Out::Mask(&mut scattered), + Out::Mask(&mut sink), ) .expect("runs"); - let alloc1 = alloc_delta(before1); - assert_eq!(value1, Value::Scattered); - // The demanded terminal RESULT of this phase is the 8-word buffer - // itself — count it in bytes here rather than the final scalar answer. - let scattered_bytes = scattered.len() * std::mem::size_of::(); - - // Phase 2 (doc side): a tiny program reading `scattered` as a resident - // plane and counting it — a second `Terminal::Count`, per the module - // doc's "say which": this repo's own executor over a plain `Filter::Plane`, - // not a bespoke `ndarray::simd::popcount_batch_u64` call, so the count - // goes through the SAME validated, differential-tested path as every - // other case in this suite. - let doc_masks: [&[u64]; 1] = [&scattered]; - let doc_planes = Planes { - n_rows: DOC_ROWS, - masks: &doc_masks, - lanes: &[], + let alloc = alloc_delta(before); + let encoded = match value { + Value::Count(c) => c.to_string(), + other => panic!("expected a count, got {other:?}"), }; - let doc_program = lower(&Query { - filter: Filter::plane(lance_graph_quack::Mask(0)), - agg: Agg::Count, + let m = CaseMetrics { + ops: program.ops.len(), + scratch_words, + tile_words, + alloc_bytes_exec: alloc, + rows_materialized: 0, + index_vec_len: 0, + out_bytes: encoded.len(), + programs: Some(1), + pair_relation_bytes: 0, + population_state_bytes: sink.len() * std::mem::size_of::(), + }; + print_metric("join_count_docs_with_posted", &m); + assert_case(&cases, "join_count_docs_with_posted", &encoded); + + // The run fold is NOT legal on this layout, and the harness proves it + // rather than assuming it: on the unclustered lane it counts runs. + let runs = lower(&Query { + filter: Filter::cmp(STATUS, Cmp::EqU32(1)), + agg: Agg::CountDistinctClusteredU32 { key: DOC_ID_U32 }, }) .expect("lowers"); - let mut d_scratch = Scratch::for_program(&doc_program, DOC_ROWS).expect("carves"); - let d_scratch_words = scratch_words_for(d_scratch.words(), d_scratch.slots()).expect("sized"); - let before2 = BYTES.load(Ordering::Relaxed); - let value2 = execute_into( - &doc_program, - &doc_planes, - &Foreign::NONE, - &mut d_scratch, - Out::None, - ) - .expect("runs"); - let alloc2 = alloc_delta(before2); + let mut r_scratch = Scratch::for_program(&runs, planes.n_rows).expect("carves"); + let over = + execute_into(&runs, &planes, &Foreign::NONE, &mut r_scratch, Out::None).expect("runs"); + let want: usize = encoded.parse().expect("a count"); + assert!( + matches!(over, Value::Count(c) if c > want), + "unclustered: the run fold must over-count ({over:?} vs {want})" + ); - let encoded = match value2 { + // ── Arm 2: the clustered layout — CountKeyRunsU32, no population state. ── + let mut order: Vec = (0..fx.line.doc_id_u32.len()).collect(); + order.sort_by_key(|&i| fx.line.doc_id_u32[i]); // stable: runs, not a resort + let by = |v: &[u32]| -> Vec { order.iter().map(|&i| v[i]).collect() }; + let by_i = |v: &[i32]| -> Vec { order.iter().map(|&i| v[i]).collect() }; + let clustered = fixture::LineTable { + doc_id: by_i(&fx.line.doc_id), + doc_id_u32: by(&fx.line.doc_id_u32), + partner_id: by(&fx.line.partner_id), + amount: by_i(&fx.line.amount), + qty: by_i(&fx.line.qty), + status: by(&fx.line.status), + cost_center: by(&fx.line.cost_center), + gl_account: by(&fx.line.gl_account), + }; + let c_lanes = clustered.lanes(); + let c_planes = c_lanes.planes(); + let mut c_scratch = Scratch::for_program(&runs, c_planes.n_rows).expect("carves"); + let c_tile_words = c_scratch.words(); + let c_scratch_words = scratch_words_for(c_tile_words, c_scratch.slots()).expect("sized"); + let before = BYTES.load(Ordering::Relaxed); + let c_value = + execute_into(&runs, &c_planes, &Foreign::NONE, &mut c_scratch, Out::None).expect("runs"); + let c_alloc = alloc_delta(before); + let c_encoded = match c_value { Value::Count(c) => c.to_string(), other => panic!("expected a count, got {other:?}"), }; - let out_bytes = scattered_bytes + encoded.len(); let m = CaseMetrics { - ops: line_program.ops.len() + doc_program.ops.len(), - scratch_words: scratch_words + d_scratch_words, - tile_words, - alloc_bytes_exec: alloc1 + alloc2, + ops: runs.ops.len(), + scratch_words: c_scratch_words, + tile_words: c_tile_words, + alloc_bytes_exec: c_alloc, rows_materialized: 0, index_vec_len: 0, - out_bytes, - programs: Some(2), + out_bytes: c_encoded.len(), + programs: Some(1), pair_relation_bytes: 0, + population_state_bytes: 0, }; - print_metric("join_count_docs_with_posted", &m); - assert_case(&cases, "join_count_docs_with_posted", &encoded); + print_metric("join_count_docs_with_posted_clustered", &m); + // A distinct count is permutation-invariant: same DuckDB row, same answer. + assert_case(&cases, "join_count_docs_with_posted", &c_encoded); } /// `SELECT p.country, SUM(l.amount) FROM line l JOIN partner p ON @@ -886,6 +940,7 @@ fn join_group_sum_country() { // `partner.country` in place — never a materialized `(line, // partner)` pair list or a remapped key lane. pair_relation_bytes: 0, + population_state_bytes: 0, }; print_metric("join_group_sum_country", &m); assert_case(&cases, "join_group_sum_country", &encoded); From d92d55e303acb527e28593c3d885fdb1ac6b1ee5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 22:11:22 +0000 Subject: [PATCH 10/15] quack harness: count the clustered view the test builds as its own materialisation The clustered arm of `join_count_docs_with_posted` proves the terminal is O(1) WHEN GIVEN a key-clustered view; the view is built by the test as a permutation plus reordered lanes. That is materialisation in the harness, so METRIC now prints it as `fixture_view_bytes` beside `population_state_bytes`, and the arm is named `_clustered_view_given`. A resident doc-major projection is the open question, not a proven fact. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- .../tests/duckdb_differential.rs | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/crates/lance-graph-quack/tests/duckdb_differential.rs b/crates/lance-graph-quack/tests/duckdb_differential.rs index 324904baa..74b042df5 100644 --- a/crates/lance-graph-quack/tests/duckdb_differential.rs +++ b/crates/lance-graph-quack/tests/duckdb_differential.rs @@ -159,6 +159,13 @@ struct CaseMetrics { /// case is `0`. The law's "no population intermediate" is checked /// against THIS field, not against `alloc_bytes_exec`. population_state_bytes: usize, + /// Bytes of population-sized state the TEST built to hand the fold a + /// view it does not have resident — a reordered copy of the lanes. This + /// is materialisation in the harness, not in the terminal, and it is + /// printed so a `population_state_bytes=0` next to it cannot be read as + /// a zero-materialisation proof: the terminal is O(1) WHEN GIVEN that + /// view; producing the view is the open question. `0` everywhere else. + fixture_view_bytes: usize, } fn print_metric(id: &str, m: &CaseMetrics) { @@ -167,7 +174,7 @@ fn print_metric(id: &str, m: &CaseMetrics) { Some(k) => eprintln!( "METRIC case={id} ops={} scratch_words={} tile_words={} scratch_bytes={} \ alloc_bytes_exec={} rows_materialized={} index_vec_len={} out_bytes={} \ - pair_relation_bytes={} population_state_bytes={} programs={k}", + pair_relation_bytes={} population_state_bytes={} fixture_view_bytes={} programs={k}", m.ops, m.scratch_words, m.tile_words, @@ -178,11 +185,12 @@ fn print_metric(id: &str, m: &CaseMetrics) { m.out_bytes, m.pair_relation_bytes, m.population_state_bytes, + m.fixture_view_bytes, ), None => eprintln!( "METRIC case={id} ops={} scratch_words={} tile_words={} scratch_bytes={} \ alloc_bytes_exec={} rows_materialized={} index_vec_len={} out_bytes={} \ - pair_relation_bytes={} population_state_bytes={}", + pair_relation_bytes={} population_state_bytes={} fixture_view_bytes={}", m.ops, m.scratch_words, m.tile_words, @@ -193,6 +201,7 @@ fn print_metric(id: &str, m: &CaseMetrics) { m.out_bytes, m.pair_relation_bytes, m.population_state_bytes, + m.fixture_view_bytes, ), } } @@ -260,6 +269,7 @@ fn run_query(id: &str, planes: &Planes<'_>, filter: Filter, agg: Agg) -> (String programs: None, pair_relation_bytes: 0, population_state_bytes: 0, + fixture_view_bytes: 0, }, ) } @@ -355,6 +365,7 @@ fn run_group( programs: Some(plan.groups.len()), pair_relation_bytes: 0, population_state_bytes: 0, + fixture_view_bytes: 0, }, ) } @@ -649,6 +660,7 @@ fn group_sum_cc() { programs: Some(1), pair_relation_bytes: 0, population_state_bytes: 0, + fixture_view_bytes: 0, }; print_metric("group_sum_cc", &m); assert_case(&cases, "group_sum_cc", &encoded); @@ -735,6 +747,7 @@ fn join_sum_country() { // shape built is gone too, no foreign MASK ever exists either. pair_relation_bytes: 0, population_state_bytes: 0, + fixture_view_bytes: 0, }; print_metric("join_sum_country", &m); assert_case(&cases, "join_sum_country", &encoded); @@ -752,10 +765,14 @@ fn join_sum_country() { /// minimum exact state on an unclustered lane (mask-risc /// `tests/distinct.rs`' pigeonhole falsifier); it never leaves the fold, /// only its popcount does. -/// - the same lines stored under their doc (`doc_id` clustered — the address +/// - the same lines GIVEN in doc order (`doc_id` clustered — the address /// order of a child population under its parent): /// [`Agg::CountDistinctClusteredU32`] → `Terminal::CountKeyRunsU32`, two -/// words of state, `Out::None`, `population_state_bytes = 0`. +/// words of state, `Out::None`. This arm proves the TERMINAL is O(1) when +/// handed a clustered view; the view itself is built by the test +/// (`fixture_view_bytes`, a reordered copy) because the fixture has no +/// resident doc-major projection. It is a semantic proof of the fold, not +/// a zero-materialisation proof of the query on this fixture. /// /// The old shape — `ScatterOrU32` into a doc bitmap read back by a second /// `Count` program — is gone: a population mask consumed by the next fold is @@ -808,6 +825,7 @@ fn join_count_docs_with_posted() { programs: Some(1), pair_relation_bytes: 0, population_state_bytes: sink.len() * std::mem::size_of::(), + fixture_view_bytes: 0, }; print_metric("join_count_docs_with_posted", &m); assert_case(&cases, "join_count_docs_with_posted", &encoded); @@ -828,7 +846,8 @@ fn join_count_docs_with_posted() { "unclustered: the run fold must over-count ({over:?} vs {want})" ); - // ── Arm 2: the clustered layout — CountKeyRunsU32, no population state. ── + // ── Arm 2: a clustered VIEW handed to CountKeyRunsU32. The reorder below + // is the test's own materialisation, counted as `fixture_view_bytes`. ── let mut order: Vec = (0..fx.line.doc_id_u32.len()).collect(); order.sort_by_key(|&i| fx.line.doc_id_u32[i]); // stable: runs, not a resort let by = |v: &[u32]| -> Vec { order.iter().map(|&i| v[i]).collect() }; @@ -867,8 +886,12 @@ fn join_count_docs_with_posted() { programs: Some(1), pair_relation_bytes: 0, population_state_bytes: 0, + // What the TEST built to hand the fold a clustered view: the + // permutation plus eight reordered 4-byte lanes. Not the terminal's + // state — and not evidence that a resident doc-major view exists. + fixture_view_bytes: order.len() * (std::mem::size_of::() + 8 * 4), }; - print_metric("join_count_docs_with_posted_clustered", &m); + print_metric("join_count_docs_with_posted_clustered_view_given", &m); // A distinct count is permutation-invariant: same DuckDB row, same answer. assert_case(&cases, "join_count_docs_with_posted", &c_encoded); } @@ -941,6 +964,7 @@ fn join_group_sum_country() { // partner)` pair list or a remapped key lane. pair_relation_bytes: 0, population_state_bytes: 0, + fixture_view_bytes: 0, }; print_metric("join_group_sum_country", &m); assert_case(&cases, "join_group_sum_country", &encoded); From 9967fab475e8b3513ee39bf777c335f50e7e1005 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 22:21:09 +0000 Subject: [PATCH 11/15] mask-risc + quack: a key lane out of order is refused, never folded through a seen-set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Terminal::CountKeyRunsU32` now ENFORCES its precondition: the first key below the open run's key refuses the program with the new `ExecError::LaneNotOrdered { lane }` (executor and oracle alike). Order is the one clustering certificate checkable with O(1) state in the same pass; nothing is ever over-counted. `Terminal::ScatterCountU32` is HELD: it stays as the pigeonhole falsifier's instrument, and Quack no longer emits it — `Agg::CountDistinctU32` is removed. An unordered physical layout is a lowering limitation, not permission to materialise a population seen-set. Harness: `join_count_docs_with_posted` on the generated fixture is REFUSED (`LaneNotOrdered`); the same lines GIVEN in doc order still fold to DuckDB's 511 with two words of state, and the reorder the test built is printed as `fixture_view_bytes`. `tests/rotation.rs`: rotation/transpose as a coordinate map over values stored once — ρ(A&B) = ρ(A)&ρ(B) on the existing IR, reducers invariant under ρ, and a column view as `eq_u32_strided_to_mask` over the same bytes. No `MaskOp::Rotate`, no rotated buffer, no reordered mask. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/lance-graph-mask-risc/src/exec.rs | 10 +- crates/lance-graph-mask-risc/src/ir.rs | 35 +-- crates/lance-graph-mask-risc/src/reference.rs | 3 + crates/lance-graph-mask-risc/src/value.rs | 8 + .../lance-graph-mask-risc/tests/distinct.rs | 99 +++++---- .../lance-graph-mask-risc/tests/rotation.rs | 206 ++++++++++++++++++ crates/lance-graph-quack/src/lib.rs | 38 +--- .../tests/duckdb_differential.rs | 106 +++------ 8 files changed, 346 insertions(+), 159 deletions(-) create mode 100644 crates/lance-graph-mask-risc/tests/rotation.rs diff --git a/crates/lance-graph-mask-risc/src/exec.rs b/crates/lance-graph-mask-risc/src/exec.rs index 45b66367d..16f336d3d 100644 --- a/crates/lance-graph-mask-risc/src/exec.rs +++ b/crates/lance-graph-mask-risc/src/exec.rs @@ -939,11 +939,17 @@ pub fn execute_into( } } Terminal::CountKeyRunsU32 { mask, lane } => { - runs += masked_key_run_count_u32( + // A key below the open run's key means the lane is not in + // key order: refuse, never over-count. Sinks are untouched + // (this terminal has none) and scratch is ALU state. + match masked_key_run_count_u32( lane_u32(planes, lane, t), read(planes, &slots, mask, t), &mut run_carry, - ); + ) { + Some(closed) => runs += closed, + None => return Err(ExecError::LaneNotOrdered { lane }), + } } Terminal::GroupSumI32 { mask, key, val } => { // `validate` already refused a missing or too-small `out`; diff --git a/crates/lance-graph-mask-risc/src/ir.rs b/crates/lance-graph-mask-risc/src/ir.rs index 00ebc3e0e..9c1778960 100644 --- a/crates/lance-graph-mask-risc/src/ir.rs +++ b/crates/lance-graph-mask-risc/src/ir.rs @@ -279,14 +279,17 @@ pub enum Terminal { /// becoming an intermediate. (When the mask itself is the demanded /// result — a `hop` — use [`Terminal::ScatterOrU32`].) /// - /// The accumulator is population-sized (one bit per key of the - /// universe) and that is the MINIMUM for an exact distinct count over an - /// UNCLUSTERED key lane: `tests/distinct.rs`'s pigeonhole falsifier shows - /// two prefixes with different key sets always diverge under some - /// suffix, so any exact fold's state separates all `2^K` key sets. When - /// the key lane is clustered — equal keys contiguous, the address order - /// a projection stores a child population under its parent — use - /// [`Terminal::CountKeyRunsU32`], which needs two words of state. + /// **HELD, not a lowering target.** The accumulator is population-sized + /// (one bit per key of the universe). `tests/distinct.rs`'s pigeonhole + /// falsifier shows that is the minimum for an exact distinct count + /// under ARBITRARY row order — but an arbitrary row order is not a + /// licence to carry it: the law is that an equivalent T0 projection + /// (a lane stored in key order) serves the fold with O(1) state + /// ([`Terminal::CountKeyRunsU32`]), and a lane that lacks one is a + /// lowering limitation, not permission to materialise a seen-set. This + /// terminal survives as the falsifier's instrument and for a caller + /// whose requested computation explicitly asks for a seen-set; no + /// lowering emits it as the automatic fallback for exact DISTINCT. ScatterCountU32 { mask: Operand, lane: u16, @@ -300,12 +303,16 @@ pub enum Terminal { /// population-sized set, no `Out` buffer, `Out::None`. The answer is /// [`Value::Count`]. /// - /// Clustering is the CALLER's precondition (the lane is stored in key - /// order — the T0 address projection, or a witnessed ordered lane). On a - /// lane that is not clustered this counts runs, not keys, and - /// over-counts; the executor cannot check clustering without the very - /// seen-set this terminal exists to avoid, so it does not try. For an - /// unclustered lane use [`Terminal::ScatterCountU32`]. + /// The precondition is a lane stored in KEY ORDER (the T0 address + /// projection that puts a child population under its parent), and it is + /// ENFORCED, not trusted: the first key smaller than the open run's key + /// refuses the program with [`ExecError::LaneNotOrdered`]. Non-decreasing + /// order is the one clustering certificate checkable with O(1) state in + /// the same pass (an exact clustering check would need the seen-set + /// this terminal exists to avoid); a clustered-but-unsorted lane is + /// refused too, deliberately. Nothing is ever over-counted. A lane with + /// no key-ordered projection resident is a lowering limitation, and the + /// refusal is the answer — not [`Terminal::ScatterCountU32`]. CountKeyRunsU32 { mask: Operand, lane: u16 }, /// The one-terminal `GROUP BY … SUM`: for every row `i` where `mask` /// holds, adds `val[i]` into the caller's `Out::I64` buffer at index diff --git a/crates/lance-graph-mask-risc/src/reference.rs b/crates/lance-graph-mask-risc/src/reference.rs index 584a00320..b9fa59a5f 100644 --- a/crates/lance-graph-mask-risc/src/reference.rs +++ b/crates/lance-graph-mask-risc/src/reference.rs @@ -758,6 +758,9 @@ pub fn reference_execute_into( let mut count = 0usize; let mut run_start = 0usize; for r in 0..n { + if r > 0 && u32_at(planes, lane, r) < u32_at(planes, lane, r - 1) { + return Err(ExecError::LaneNotOrdered { lane }); + } if r > 0 && u32_at(planes, lane, r) != u32_at(planes, lane, r - 1) { run_start = r; } diff --git a/crates/lance-graph-mask-risc/src/value.rs b/crates/lance-graph-mask-risc/src/value.rs index 24feb00e9..2b1c101d0 100644 --- a/crates/lance-graph-mask-risc/src/value.rs +++ b/crates/lance-graph-mask-risc/src/value.rs @@ -131,6 +131,14 @@ pub enum ExecError { /// `mask_set_range` a bound past the scratch words (its own assert would /// panic) and the oracle never indexes a row that does not exist. RangeOutOfBounds { lo: u32, hi: u32, n_rows: usize }, + /// [`crate::Terminal::CountKeyRunsU32`] met a key smaller than the open + /// run's key: the lane is not in key order, so a run is not a key and + /// the count would be wrong. Refused at that element, with O(1) state — + /// non-decreasing order is the clustering certificate the fold can + /// check in its own pass. The logical query is fine; THIS lowering + /// needs a lane stored in key order (a T0 address projection), and no + /// such projection is resident for this lane. + LaneNotOrdered { lane: u16 }, /// [`crate::MaskOp::Gather`]'s `foreign` names no entry of the caller's /// [`crate::Foreign::planes`]. ForeignOutOfRange(u16), diff --git a/crates/lance-graph-mask-risc/tests/distinct.rs b/crates/lance-graph-mask-risc/tests/distinct.rs index 7946dbce3..fccb1b137 100644 --- a/crates/lance-graph-mask-risc/tests/distinct.rs +++ b/crates/lance-graph-mask-risc/tests/distinct.rs @@ -6,17 +6,18 @@ //! - `Terminal::CountKeyRunsU32` — a key-clustered lane (equal keys //! contiguous). Two words of state, tile by tile. Proven here against the //! oracle and an independent seen-set, across tilings. -//! - `Terminal::ScatterCountU32` — an unclustered lane. Its accumulator is -//! one bit per key of the universe, and the pigeonhole falsifier below -//! shows that is the MINIMUM any exact fold can carry on such a lane, so -//! the accumulator is the fold's own sufficient statistic, not an -//! avoidable intermediate. +//! - `Terminal::ScatterCountU32` — HELD. Its accumulator is one bit per key +//! of the universe; the pigeonhole falsifier below shows that is the +//! minimum any exact fold can carry under ARBITRARY row order. That bounds +//! the wrong traversal, it does not license it: a lane out of key order is +//! REFUSED (`ExecError::LaneNotOrdered`), never folded through a seen-set +//! as a fallback. The terminal remains as the falsifier's instrument. use lance_graph_mask_risc::exec::{execute_into, Scratch}; use lance_graph_mask_risc::reference::reference_execute_into; use lance_graph_mask_risc::{ - scratch_words_for, words_for, Foreign, LaneRef, MaskOp, Operand, Out, Planes, Pred, Program, - Terminal, Value, + scratch_words_for, words_for, ExecError, Foreign, LaneRef, MaskOp, Operand, Out, Planes, Pred, + Program, Terminal, Value, }; use std::collections::BTreeSet; @@ -69,14 +70,14 @@ fn oracle(program: &Program, planes: &Planes<'_>) -> usize { } #[test] -fn on_a_clustered_key_lane_the_run_fold_is_the_distinct_count_under_every_tiling() { +fn on_a_lane_in_key_order_the_run_fold_is_the_distinct_count_under_every_tiling() { let mut seed = 0xD15u64; for &n in &[1usize, 63, 64, 65, 500, 4096] { let universe = 97u32; let mut key: Vec = (0..n) .map(|_| (lcg(&mut seed) % u64::from(universe)) as u32) .collect(); - key.sort_unstable(); // clustered: equal keys contiguous + key.sort_unstable(); // key order: the T0 address projection, given let status: Vec = (0..n).map(|_| (lcg(&mut seed) % 3) as u32).collect(); let lanes = [LaneRef::U32(&status), LaneRef::U32(&key)]; let planes = Planes { @@ -130,7 +131,7 @@ fn a_run_split_across_a_tile_edge_is_counted_once_and_an_unhit_run_never() { } #[test] -fn on_an_unclustered_lane_the_run_fold_over_counts_and_scatter_count_is_exact() { +fn a_lane_out_of_key_order_is_refused_by_executor_and_oracle_alike() { let mut seed = 0xBADu64; let n = 2048usize; let universe = 64u32; @@ -138,48 +139,66 @@ fn on_an_unclustered_lane_the_run_fold_over_counts_and_scatter_count_is_exact() .map(|_| (lcg(&mut seed) % u64::from(universe)) as u32) .collect(); let status: Vec = (0..n).map(|_| (lcg(&mut seed) % 2) as u32).collect(); + assert!( + key.windows(2).any(|w| w[1] < w[0]), + "fixture must be out of order" + ); let lanes = [LaneRef::U32(&status), LaneRef::U32(&key)]; let planes = Planes { n_rows: n, masks: &[], lanes: &lanes, }; - let want = seen_set(&status, &key); - assert!(want > 2, "fixture must have several distinct keys"); - let runs = program(|m| Terminal::CountKeyRunsU32 { mask: m, lane: 1 }); - let over = run(&runs, &planes, 8); - assert!( - over > want, - "unclustered: runs ({over}) must exceed keys ({want})" + let slots = runs.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(8, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, 8, slots).expect("carves"); + assert_eq!( + execute_into(&runs, &planes, &Foreign::NONE, &mut scratch, Out::None), + Err(ExecError::LaneNotOrdered { lane: 1 }) + ); + assert_eq!( + reference_execute_into(&runs, &planes, &Foreign::NONE, Out::None), + Err(ExecError::LaneNotOrdered { lane: 1 }) ); + // The smallest witness of why: 1,2,1 has three runs and two keys. + let k = [1u32, 2, 1]; + let s1 = [1u32; 3]; + let l = [LaneRef::U32(&s1), LaneRef::U32(&k)]; + let p3 = Planes { + n_rows: 3, + masks: &[], + lanes: &l, + }; + let mut b3 = vec![0u64; scratch_words_for(1, slots).expect("sized")]; + let mut s3 = Scratch::over(&mut b3, 1, slots).expect("carves"); assert_eq!( - oracle(&runs, &planes), - over, - "executor and oracle agree on the run count" + execute_into(&runs, &p3, &Foreign::NONE, &mut s3, Out::None), + Err(ExecError::LaneNotOrdered { lane: 1 }) ); + assert_eq!(seen_set(&s1, &k), 2); +} - let scatter = Program::new( - runs.ops.clone(), - Terminal::ScatterCountU32 { - mask: S0, - lane: 1, - out_rows: universe, - }, +#[test] +fn clustered_but_unsorted_is_refused_by_design() { + // 3 3 1 1: a run count WOULD be exact here, but the certificate the fold + // can check in O(1) is order, and 1 < 3 breaks it. + let k = [3u32, 3, 1, 1]; + let s = [1u32; 4]; + let l = [LaneRef::U32(&s), LaneRef::U32(&k)]; + let planes = Planes { + n_rows: 4, + masks: &[], + lanes: &l, + }; + let p = program(|m| Terminal::CountKeyRunsU32 { mask: m, lane: 1 }); + let slots = p.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(1, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, 1, slots).expect("carves"); + assert_eq!( + execute_into(&p, &planes, &Foreign::NONE, &mut scratch, Out::None), + Err(ExecError::LaneNotOrdered { lane: 1 }) ); - let mut sink = vec![0u64; words_for(universe as usize)]; - let slots = scatter.scratch_slots as usize; - let mut buf = vec![0u64; scratch_words_for(8, slots).expect("sized")]; - let mut scratch = Scratch::over(&mut buf, 8, slots).expect("carves"); - let got = execute_into( - &scatter, - &planes, - &Foreign::NONE, - &mut scratch, - Out::Mask(&mut sink), - ) - .expect("runs"); - assert_eq!(got, Value::Count(want)); } /// The pigeonhole falsifier for "an exact distinct count over an diff --git a/crates/lance-graph-mask-risc/tests/rotation.rs b/crates/lance-graph-mask-risc/tests/rotation.rs new file mode 100644 index 000000000..efc35788f --- /dev/null +++ b/crates/lance-graph-mask-risc/tests/rotation.rs @@ -0,0 +1,206 @@ +//! Rotation is a coordinate system, not a mask op. +//! +//! Toy: values `A B / C D` stored ONCE as one lane in row-major order. A +//! rotation or transpose is an address map ρ over that storage; nothing is +//! copied, no rotated buffer, no reordered mask. Two consequences are +//! proved here, on the existing IR and the existing T1 words only: +//! +//! 1. For any bijection ρ on rows, `ρ(A & B) = ρ(A) & ρ(B)` (likewise `|`, +//! `^`), so predicates and masks are computed ONCE in canonical +//! coordinates and the coordinate transform is absorbed at the consumer: +//! a reducer (`Count`, `MaskedSumI32`) is invariant under ρ and needs no +//! view at all; an order-sensitive sink reads bit `ρ(i)` through the +//! map. `MaskOp::Rotate` does not exist and must not. +//! 2. When the consumer's coordinates are a stride/offset over the same +//! bytes (a column of the 2×2, i.e. the transpose or a 90° rotation), +//! the predicate itself reads through that projection — +//! `ndarray::simd::eq_u32_strided_to_mask` over the canonical storage — +//! and its mask equals the canonical mask read through ρ. Same bytes, +//! different coordinates, fold directly. + +use lance_graph_mask_risc::exec::{execute_into, Scratch}; +use lance_graph_mask_risc::{ + scratch_words_for, Foreign, LaneRef, MaskOp, Operand, Out, Planes, Pred, Program, Terminal, + Value, +}; +use ndarray::simd::eq_u32_strided_to_mask; + +/// `A B / C D` = `[1, 2, 3, 4]`, row-major, the ONLY copy of the values. +const CANON: [i32; 4] = [1, 2, 3, 4]; + +/// Row-major index of cell `(r, c)` in a 2×2. +fn rm(r: usize, c: usize) -> usize { + r * 2 + c +} + +/// 90° clockwise: `A B / C D` → `C A / D B`… the addendum's example is +/// `A B / C D` → `B D / A C` (counter-clockwise): new `(r, c)` reads old +/// `(c, 1 - r)`. +fn rot_ccw(r: usize, c: usize) -> usize { + rm(c, 1 - r) +} + +/// Transpose: `A B / C D` → `A C / B D`: new `(r, c)` reads old `(c, r)`. +fn transpose(r: usize, c: usize) -> usize { + rm(c, r) +} + +/// Read a canonical mask through an address map — the VIEW, computed per +/// bit on demand, never stored. +fn view(mask: u64, map: fn(usize, usize) -> usize) -> u64 { + let mut v = 0u64; + for r in 0..2 { + for c in 0..2 { + if mask >> map(r, c) & 1 == 1 { + v |= 1 << rm(r, c); + } + } + } + v +} + +fn run(program: &Program, planes: &Planes<'_>) -> (u64, Value) { + let slots = program.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(1, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, 1, slots).expect("carves"); + let mut kept = [0u64; 1]; + let out = if matches!(program.terminal, Terminal::Keep { .. }) { + Out::Mask(&mut kept) + } else { + Out::None + }; + let v = execute_into(program, planes, &Foreign::NONE, &mut scratch, out).expect("runs"); + (kept[0], v) +} + +const S0: Operand = Operand::Scratch(0); +const S1: Operand = Operand::Scratch(1); +const S2: Operand = Operand::Scratch(2); + +/// `v > 1` AND `v != 3` over the canonical lane; the kept mask. +fn and_program(terminal: Terminal) -> Program { + Program::new( + vec![ + MaskOp::Pred { + pred: Pred::GtI32 { lane: 0, t: 1 }, + under: None, + dst: 0, + }, + MaskOp::Pred { + pred: Pred::NeI32 { lane: 0, v: 3 }, + under: None, + dst: 1, + }, + MaskOp::And { + a: S0, + b: S1, + dst: 2, + }, + ], + terminal, + ) +} + +#[test] +fn a_rotation_commutes_with_the_mask_algebra_so_operands_are_never_rotated() { + let lanes = [LaneRef::I32(&CANON)]; + let planes = Planes { + n_rows: 4, + masks: &[], + lanes: &lanes, + }; + // The two predicates, each kept, in canonical coordinates — once. + let (a, _) = run( + &Program::new( + vec![MaskOp::Pred { + pred: Pred::GtI32 { lane: 0, t: 1 }, + under: None, + dst: 0, + }], + Terminal::Keep { mask: S0 }, + ), + &planes, + ); + let (b, _) = run( + &Program::new( + vec![MaskOp::Pred { + pred: Pred::NeI32 { lane: 0, v: 3 }, + under: None, + dst: 0, + }], + Terminal::Keep { mask: S0 }, + ), + &planes, + ); + let (ab, _) = run(&and_program(Terminal::Keep { mask: S2 }), &planes); + assert_eq!(a, 0b1110, "v > 1 selects B C D"); + assert_eq!(b, 0b1011, "v != 3 selects A B D"); + assert_eq!(ab, 0b1010, "B D"); + for map in [rot_ccw as fn(usize, usize) -> usize, transpose] { + // ρ(A & B) = ρ(A) & ρ(B): the view of the conjunction is the + // conjunction of the views. No operand was ever rotated. + assert_eq!(view(ab, map), view(a, map) & view(b, map)); + assert_eq!(view(a | b, map), view(a, map) | view(b, map)); + assert_eq!(view(a ^ b, map), view(a, map) ^ view(b, map)); + } + // The rotated matrix is `B D / A C`, so the kept cells B, D sit at + // (0,0) and (0,1): the view reads 0b0011 — and the answer came from the + // canonical mask through the map, not from a rotated buffer. + assert_eq!(view(ab, rot_ccw), 0b0011); + // The transpose is `A C / B D`: B, D are its second row, (1,0) and (1,1). + assert_eq!(view(ab, transpose), 0b1100); + let (_, count) = run(&and_program(Terminal::Count { mask: S2 }), &planes); + let (_, sum) = run( + &and_program(Terminal::MaskedSumI32 { mask: S2, lane: 0 }), + &planes, + ); + // A reducer is invariant under ρ: it consumes the canonical mask + // directly; the coordinate system never reaches it. + assert_eq!(count, Value::Count(2)); + assert_eq!(sum, Value::SumI64(2 + 4)); + assert_eq!(view(ab, rot_ccw).count_ones(), 2); + assert_eq!(view(ab, transpose).count_ones(), 2); +} + +#[test] +fn a_column_view_is_a_strided_read_of_the_same_bytes_not_a_copy() { + // The same four values as LE bytes, stored once. + let vals: [u32; 4] = [1, 2, 3, 4]; + let mut bytes = [0u8; 16]; + for (i, v) in vals.iter().enumerate() { + bytes[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes()); + } + // Canonical: row-major predicate `v == 2` on the contiguous lane. + let mut canon = [0u64; 1]; + eq_u32_strided_to_mask(&bytes, 0, 4, 4, 2, &mut canon); + assert_eq!(canon[0], 0b0010, "B"); + + // Transpose `A C / B D`: its row r IS canonical column r — a strided + // read at offset 4r, stride 8, count 2. Two rows, two reads, no copy. + let mut t = 0u64; + for r in 0..2 { + let mut w = [0u64; 1]; + eq_u32_strided_to_mask(&bytes, 4 * r, 8, 2, 2, &mut w); + t |= w[0] << (2 * r); + } + assert_eq!( + t, + view(canon[0], transpose), + "strided read == canonical mask through ρ" + ); + + // Rotation `B D / A C`: its row r is canonical column 1 - r, top-down — + // the same strided read at offset 4(1 - r). + let mut rot = 0u64; + for r in 0..2 { + let mut w = [0u64; 1]; + eq_u32_strided_to_mask(&bytes, 4 * (1 - r), 8, 2, 2, &mut w); + rot |= w[0] << (2 * r); + } + assert_eq!(rot, view(canon[0], rot_ccw)); + assert_eq!(rot, 0b0001, "B is now at (0,0)"); + // A count over either view equals the canonical count: the fold never + // needed the view. + assert_eq!(t.count_ones(), canon[0].count_ones()); + assert_eq!(rot.count_ones(), canon[0].count_ones()); +} diff --git a/crates/lance-graph-quack/src/lib.rs b/crates/lance-graph-quack/src/lib.rs index 069b21306..6b9ebad7e 100644 --- a/crates/lance-graph-quack/src/lib.rs +++ b/crates/lance-graph-quack/src/lib.rs @@ -823,28 +823,17 @@ pub enum Agg { /// must be exactly `words_for(out_rows)` long. out_rows: u32, }, - /// `COUNT(DISTINCT key)` over the surviving rows on an UNCLUSTERED key - /// lane — `Terminal::ScatterCountU32`. `SELECT COUNT(*) FROM doc WHERE - /// EXISTS(line … doc_id = doc.rid AND status = 1)` is - /// `Agg::CountDistinctU32 { key: doc_id, universe: DOC_ROWS }`: ONE - /// program whose accumulator (the caller's `Out::Mask`, one bit per key - /// of `universe`) never leaves the fold — only its popcount does. That - /// accumulator is the minimum exact state on an unclustered lane - /// (mask-risc `tests/distinct.rs`, the pigeonhole falsifier); a key at or - /// past `universe` is dropped, not an error. - CountDistinctU32 { - /// The key column (`u32`) whose distinct values are counted. - key: Col, - /// The key universe — the accumulator is `words_for(universe)`. - universe: u32, - }, - /// `COUNT(DISTINCT key)` on a KEY-CLUSTERED lane (equal keys contiguous — - /// the address order a projection stores a child population under its - /// parent) — `Terminal::CountKeyRunsU32`, two words of state, `Out::None`. - /// Clustering is the caller's precondition, exactly as - /// [`Filter::prefix_facet`]'s witnessed range carries its row-order - /// obligation: on a lane that is not clustered this counts runs and - /// over-counts. Use [`Agg::CountDistinctU32`] there. + /// `COUNT(DISTINCT key)` — `Terminal::CountKeyRunsU32`, two words of + /// state, `Out::None`. The physical precondition is a key lane stored + /// in KEY ORDER (the address order a projection stores a child + /// population under its parent); the executor ENFORCES it and refuses a + /// lane out of order with `ExecError::LaneNotOrdered`, so a wrong count + /// is impossible. Quack cannot see lanes, so it does not pre-judge: + /// the lowering always succeeds and the refusal is physical. There is + /// deliberately NO other spelling of exact DISTINCT here — an + /// unordered physical layout is a lowering limitation, not permission + /// to fold through a population seen-set (`Terminal::ScatterCountU32` + /// is held, never emitted). CountDistinctClusteredU32 { /// The key column (`u32`), stored in key order. key: Col, @@ -1465,11 +1454,6 @@ fn terminal_of(agg: Agg, mask: Operand) -> Terminal { lane: fk.0, out_rows, }, - Agg::CountDistinctU32 { key, universe } => Terminal::ScatterCountU32 { - mask, - lane: key.0, - out_rows: universe, - }, Agg::CountDistinctClusteredU32 { key } => Terminal::CountKeyRunsU32 { mask, lane: key.0 }, Agg::GroupSumI32 { key, val } => Terminal::GroupSumI32 { mask, diff --git a/crates/lance-graph-quack/tests/duckdb_differential.rs b/crates/lance-graph-quack/tests/duckdb_differential.rs index 74b042df5..10cf78265 100644 --- a/crates/lance-graph-quack/tests/duckdb_differential.rs +++ b/crates/lance-graph-quack/tests/duckdb_differential.rs @@ -32,15 +32,14 @@ use std::path::Path; use std::sync::atomic::{AtomicUsize, Ordering}; use lance_graph_mask_risc::{ - execute_into, materialize_rows, scratch_words_for, words_for, Foreign, LaneRef, Out, Planes, - Scratch, Terminal, Value, + execute_into, materialize_rows, scratch_words_for, words_for, ExecError, Foreign, LaneRef, Out, + Planes, Scratch, Terminal, Value, }; use lance_graph_quack::{ lower, lower_group_by, Agg, Cmp, Col, Filter, ForeignLane, GroupBy, Query, }; use fixture::col::{AMOUNT, COST_CENTER, DOC_ID, DOC_ID_U32, GL_ACCOUNT, PARTNER_ID, QTY, STATUS}; -use fixture::DOC_ROWS; // --------------------------------------------------------------------- // The counting allocator — `no_alloc.rs`'s pattern (lance-graph-mask-risc), @@ -755,95 +754,50 @@ fn join_sum_country() { /// `SELECT COUNT(*) FROM doc d WHERE EXISTS(SELECT 1 FROM line l WHERE /// l.doc_id=d.rid AND l.status=1)` — `COUNT(DISTINCT doc_id)` over the -/// posted lines, ONE program either way; which terminal is legal is a -/// property of the KEY LANE'S LAYOUT, and both layouts are run here against -/// the same DuckDB answer: +/// posted lines. ONE spelling: [`Agg::CountDistinctClusteredU32`] → +/// `Terminal::CountKeyRunsU32`, whose precondition is a key lane in key +/// order — the T0 address projection of lines under their doc. /// -/// - the fixture as generated (`doc_id` random — unclustered): -/// [`Agg::CountDistinctU32`] → `Terminal::ScatterCountU32`. The fold's -/// accumulator is one bit per doc (`population_state_bytes = 64`), the -/// minimum exact state on an unclustered lane (mask-risc -/// `tests/distinct.rs`' pigeonhole falsifier); it never leaves the fold, -/// only its popcount does. -/// - the same lines GIVEN in doc order (`doc_id` clustered — the address -/// order of a child population under its parent): -/// [`Agg::CountDistinctClusteredU32`] → `Terminal::CountKeyRunsU32`, two -/// words of state, `Out::None`. This arm proves the TERMINAL is O(1) when -/// handed a clustered view; the view itself is built by the test -/// (`fixture_view_bytes`, a reordered copy) because the fixture has no -/// resident doc-major projection. It is a semantic proof of the fold, not -/// a zero-materialisation proof of the query on this fixture. +/// - On the fixture AS GENERATED (`doc_id` random, no resident doc-major +/// projection) the executor REFUSES the program +/// (`ExecError::LaneNotOrdered`). That is the ruled outcome: the logical +/// query is valid, this physical lowering is not, and no seen-set is +/// allocated in its place. The `1,2,1` falsifier lives in mask-risc +/// `tests/distinct.rs`. +/// - GIVEN a doc-ordered view of the same lines, the fold answers the same +/// DuckDB 511 with two words of state. The view is built by the test +/// (`fixture_view_bytes`, a reordered copy): a semantic proof of the +/// terminal, not a zero-materialisation proof of the query on this +/// fixture. /// /// The old shape — `ScatterOrU32` into a doc bitmap read back by a second -/// `Count` program — is gone: a population mask consumed by the next fold is -/// forbidden intermediate state whatever it was called. +/// `Count` program — is gone, and so is its one-program successor +/// (`ScatterCountU32`): a population seen-set is not the fallback for a +/// lane that merely happens to be unordered. #[test] fn join_count_docs_with_posted() { let cases = load_cases(); let fx = fixture::generate(); - // ── Arm 1: the generated (unclustered) layout — ScatterCountU32. ── + // ── Arm 1: the generated (unordered) layout is REFUSED. ── let lanes = fx.line.lanes(); let planes = lanes.planes(); - let program = lower(&Query { - filter: Filter::cmp(STATUS, Cmp::EqU32(1)), - agg: Agg::CountDistinctU32 { - key: DOC_ID_U32, - universe: DOC_ROWS as u32, - }, - }) - .expect("lowers"); - assert!(matches!(program.terminal, Terminal::ScatterCountU32 { .. })); - let mut scratch = Scratch::for_program(&program, planes.n_rows).expect("carves"); - let tile_words = scratch.words(); - let scratch_words = scratch_words_for(tile_words, scratch.slots()).expect("sized"); - // The fold's own accumulator: one bit per doc. Counted below as - // population state, because that is what it is. - let mut sink = vec![0u64; words_for(DOC_ROWS)]; - let before = BYTES.load(Ordering::Relaxed); - let value = execute_into( - &program, - &planes, - &Foreign::NONE, - &mut scratch, - Out::Mask(&mut sink), - ) - .expect("runs"); - let alloc = alloc_delta(before); - let encoded = match value { - Value::Count(c) => c.to_string(), - other => panic!("expected a count, got {other:?}"), - }; - let m = CaseMetrics { - ops: program.ops.len(), - scratch_words, - tile_words, - alloc_bytes_exec: alloc, - rows_materialized: 0, - index_vec_len: 0, - out_bytes: encoded.len(), - programs: Some(1), - pair_relation_bytes: 0, - population_state_bytes: sink.len() * std::mem::size_of::(), - fixture_view_bytes: 0, - }; - print_metric("join_count_docs_with_posted", &m); - assert_case(&cases, "join_count_docs_with_posted", &encoded); - - // The run fold is NOT legal on this layout, and the harness proves it - // rather than assuming it: on the unclustered lane it counts runs. let runs = lower(&Query { filter: Filter::cmp(STATUS, Cmp::EqU32(1)), agg: Agg::CountDistinctClusteredU32 { key: DOC_ID_U32 }, }) .expect("lowers"); + assert!(matches!(runs.terminal, Terminal::CountKeyRunsU32 { .. })); let mut r_scratch = Scratch::for_program(&runs, planes.n_rows).expect("carves"); - let over = - execute_into(&runs, &planes, &Foreign::NONE, &mut r_scratch, Out::None).expect("runs"); - let want: usize = encoded.parse().expect("a count"); - assert!( - matches!(over, Value::Count(c) if c > want), - "unclustered: the run fold must over-count ({over:?} vs {want})" + let refused = execute_into(&runs, &planes, &Foreign::NONE, &mut r_scratch, Out::None); + assert_eq!( + refused, + Err(ExecError::LaneNotOrdered { lane: DOC_ID_U32.0 }), + "an unordered key lane must be refused, never folded through a seen-set" + ); + eprintln!( + "REFUSED case=join_count_docs_with_posted terminal=CountKeyRunsU32 \ + reason=LaneNotOrdered population_state_bytes=0" ); // ── Arm 2: a clustered VIEW handed to CountKeyRunsU32. The reorder below From 4d27032ed57261a2914377eeb270fb87725be487 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 22:26:21 +0000 Subject: [PATCH 12/15] mask-risc + quack: the distinct fold's precondition is ORDERED, not clustered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Agg::CountDistinctClusteredU32` → `Agg::CountDistinctOrderedU32`; docs on `CountKeyRunsU32` / `LaneNotOrdered` say non-decreasing key order over EVERY row, selected or not — contiguity of equal keys is not O(1)-checkable, order is. `tests/distinct.rs` pins `keys = 1 2 1` under `selected = 1 0 1`: two runs of 1, refused by executor and oracle, never counted as one. Harness arm renamed `_ordered_view_given`. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/lance-graph-mask-risc/src/exec.rs | 2 +- crates/lance-graph-mask-risc/src/ir.rs | 16 ++++---- crates/lance-graph-mask-risc/src/value.rs | 2 +- .../lance-graph-mask-risc/tests/distinct.rs | 41 ++++++++++++++++--- crates/lance-graph-quack/src/lib.rs | 16 ++++---- .../tests/duckdb_differential.rs | 16 ++++---- 6 files changed, 63 insertions(+), 30 deletions(-) diff --git a/crates/lance-graph-mask-risc/src/exec.rs b/crates/lance-graph-mask-risc/src/exec.rs index 16f336d3d..f8b924d51 100644 --- a/crates/lance-graph-mask-risc/src/exec.rs +++ b/crates/lance-graph-mask-risc/src/exec.rs @@ -734,7 +734,7 @@ pub fn execute_into( let mut sum = 0i64; let mut min: Option = None; let mut max: Option = None; - // The whole state of a key-clustered distinct count: the open run's key + // The whole state of a key-ORDERED distinct count: the open run's key // and whether it was hit. Two words, however many rows. let mut run_carry = KeyRunCarry::default(); let mut runs = 0usize; diff --git a/crates/lance-graph-mask-risc/src/ir.rs b/crates/lance-graph-mask-risc/src/ir.rs index 9c1778960..5bfc8c7d3 100644 --- a/crates/lance-graph-mask-risc/src/ir.rs +++ b/crates/lance-graph-mask-risc/src/ir.rs @@ -260,8 +260,8 @@ pub enum Terminal { /// population's mask). It is never an intermediate: a follow-on program /// or fold that consumes it is the forbidden /// projection → population → projection shape. A count over the - /// targets is [`Terminal::ScatterCountU32`] (unclustered key) or - /// [`Terminal::CountKeyRunsU32`] (key-clustered lane); a filter through + /// targets is [`Terminal::CountKeyRunsU32`] (a key-ORDERED lane; there + /// is no lowering for an unordered one); a filter through /// the targets is [`Pred::EqU32Via`] / [`MaskOp::Gather`] over a /// RESIDENT plane. ScatterOrU32 { @@ -296,7 +296,7 @@ pub enum Terminal { out_rows: u32, }, /// `COUNT(DISTINCT lane[i])` over the rows where `mask` holds, on a - /// KEY-CLUSTERED lane — every run of equal consecutive `lane` values is + /// KEY-ORDERED lane — every run of equal consecutive `lane` values is /// one key, so the count is the number of runs containing a selected /// row, folded tile by tile with a two-word carry /// ([`ndarray::simd::masked_key_run_count_u32`] + `KeyRunCarry`): no @@ -307,10 +307,12 @@ pub enum Terminal { /// projection that puts a child population under its parent), and it is /// ENFORCED, not trusted: the first key smaller than the open run's key /// refuses the program with [`ExecError::LaneNotOrdered`]. Non-decreasing - /// order is the one clustering certificate checkable with O(1) state in - /// the same pass (an exact clustering check would need the seen-set - /// this terminal exists to avoid); a clustered-but-unsorted lane is - /// refused too, deliberately. Nothing is ever over-counted. A lane with + /// order is the one contiguity certificate checkable with O(1) state in + /// the same pass (proving "each key occurs in one run" would need the seen-set + /// this terminal exists to avoid); a contiguous-but-unsorted lane is + /// refused too, deliberately, and the check inspects EVERY key, selected + /// or not (`1 2 1` under `1 0 1` is two runs of `1`, not one). Nothing is + /// ever over-counted. A lane with /// no key-ordered projection resident is a lowering limitation, and the /// refusal is the answer — not [`Terminal::ScatterCountU32`]. CountKeyRunsU32 { mask: Operand, lane: u16 }, diff --git a/crates/lance-graph-mask-risc/src/value.rs b/crates/lance-graph-mask-risc/src/value.rs index 2b1c101d0..330015199 100644 --- a/crates/lance-graph-mask-risc/src/value.rs +++ b/crates/lance-graph-mask-risc/src/value.rs @@ -134,7 +134,7 @@ pub enum ExecError { /// [`crate::Terminal::CountKeyRunsU32`] met a key smaller than the open /// run's key: the lane is not in key order, so a run is not a key and /// the count would be wrong. Refused at that element, with O(1) state — - /// non-decreasing order is the clustering certificate the fold can + /// non-decreasing order is the contiguity certificate the fold can /// check in its own pass. The logical query is fine; THIS lowering /// needs a lane stored in key order (a T0 address projection), and no /// such projection is resident for this lane. diff --git a/crates/lance-graph-mask-risc/tests/distinct.rs b/crates/lance-graph-mask-risc/tests/distinct.rs index fccb1b137..1bfb38603 100644 --- a/crates/lance-graph-mask-risc/tests/distinct.rs +++ b/crates/lance-graph-mask-risc/tests/distinct.rs @@ -3,9 +3,10 @@ //! Two terminals answer it, and which one is legal is a property of the KEY //! LANE'S LAYOUT, not of the query: //! -//! - `Terminal::CountKeyRunsU32` — a key-clustered lane (equal keys -//! contiguous). Two words of state, tile by tile. Proven here against the -//! oracle and an independent seen-set, across tilings. +//! - `Terminal::CountKeyRunsU32` — a key-ORDERED lane (non-decreasing +//! keys, checked over every row in the same pass). Two words of state, +//! tile by tile. Proven here against the oracle and an independent +//! seen-set, across tilings; an unordered lane is refused. //! - `Terminal::ScatterCountU32` — HELD. Its accumulator is one bit per key //! of the universe; the pigeonhole falsifier below shows that is the //! minimum any exact fold can carry under ARBITRARY row order. That bounds @@ -180,9 +181,9 @@ fn a_lane_out_of_key_order_is_refused_by_executor_and_oracle_alike() { } #[test] -fn clustered_but_unsorted_is_refused_by_design() { +fn contiguous_but_unsorted_is_refused_by_design() { // 3 3 1 1: a run count WOULD be exact here, but the certificate the fold - // can check in O(1) is order, and 1 < 3 breaks it. + // can check in O(1) is ORDER, and 1 < 3 breaks it. let k = [3u32, 3, 1, 1]; let s = [1u32; 4]; let l = [LaneRef::U32(&s), LaneRef::U32(&k)]; @@ -201,8 +202,36 @@ fn clustered_but_unsorted_is_refused_by_design() { ); } +#[test] +fn the_order_check_inspects_unselected_rows_too() { + // keys 1 2 1, selected 1 0 1: the SELECTED subsequence reads 1,1, but + // the lane holds two runs of 1 with an unselected 2 between them. A + // fold that validated order over survivors only would count 2 keys + // where the honest answer is a refusal. + let k = [1u32, 2, 1]; + let s = [1u32, 0, 1]; + let l = [LaneRef::U32(&s), LaneRef::U32(&k)]; + let planes = Planes { + n_rows: 3, + masks: &[], + lanes: &l, + }; + let p = program(|m| Terminal::CountKeyRunsU32 { mask: m, lane: 1 }); + let slots = p.scratch_slots as usize; + let mut buf = vec![0u64; scratch_words_for(1, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut buf, 1, slots).expect("carves"); + assert_eq!( + execute_into(&p, &planes, &Foreign::NONE, &mut scratch, Out::None), + Err(ExecError::LaneNotOrdered { lane: 1 }) + ); + assert_eq!( + reference_execute_into(&p, &planes, &Foreign::NONE, Out::None), + Err(ExecError::LaneNotOrdered { lane: 1 }) + ); +} + /// The pigeonhole falsifier for "an exact distinct count over an -/// UNCLUSTERED key lane can be folded with less than one bit per key". +/// UNORDERED key lane can be folded with less than one bit per key". /// /// Take a universe of `K` keys. For every pair of DISTINCT key sets /// `A ≠ B`, build the prefix streams "one selected row per key of A" and diff --git a/crates/lance-graph-quack/src/lib.rs b/crates/lance-graph-quack/src/lib.rs index 6b9ebad7e..0741c0363 100644 --- a/crates/lance-graph-quack/src/lib.rs +++ b/crates/lance-graph-quack/src/lib.rs @@ -814,7 +814,7 @@ pub enum Agg { /// `words_for(out_rows)` — `Terminal::ScatterOrU32`. Legal only when /// that target mask IS what the query asks for (a `hop`). A query that /// asks for a COUNT over the targets is [`Agg::CountDistinctU32`] / - /// [`Agg::CountDistinctClusteredU32`]; handing this mask to a second + /// [`Agg::CountDistinctOrderedU32`]; handing this mask to a second /// program is the forbidden population intermediate. ScatterOrU32 { /// The foreign-key column naming the target row on the OTHER table. @@ -825,16 +825,18 @@ pub enum Agg { }, /// `COUNT(DISTINCT key)` — `Terminal::CountKeyRunsU32`, two words of /// state, `Out::None`. The physical precondition is a key lane stored - /// in KEY ORDER (the address order a projection stores a child - /// population under its parent); the executor ENFORCES it and refuses a - /// lane out of order with `ExecError::LaneNotOrdered`, so a wrong count - /// is impossible. Quack cannot see lanes, so it does not pre-judge: + /// in KEY ORDER — non-decreasing over EVERY row, selected or not (the + /// address order a projection stores a child population under its + /// parent); the executor ENFORCES it in the fold's own pass and refuses + /// a lane out of order with `ExecError::LaneNotOrdered`, so a wrong + /// count is impossible. ORDERED, not merely "clustered": contiguity of + /// equal keys is not O(1)-checkable, order is. Quack cannot see lanes, so it does not pre-judge: /// the lowering always succeeds and the refusal is physical. There is /// deliberately NO other spelling of exact DISTINCT here — an /// unordered physical layout is a lowering limitation, not permission /// to fold through a population seen-set (`Terminal::ScatterCountU32` /// is held, never emitted). - CountDistinctClusteredU32 { + CountDistinctOrderedU32 { /// The key column (`u32`), stored in key order. key: Col, }, @@ -1454,7 +1456,7 @@ fn terminal_of(agg: Agg, mask: Operand) -> Terminal { lane: fk.0, out_rows, }, - Agg::CountDistinctClusteredU32 { key } => Terminal::CountKeyRunsU32 { mask, lane: key.0 }, + Agg::CountDistinctOrderedU32 { key } => Terminal::CountKeyRunsU32 { mask, lane: key.0 }, Agg::GroupSumI32 { key, val } => Terminal::GroupSumI32 { mask, key: key.0, diff --git a/crates/lance-graph-quack/tests/duckdb_differential.rs b/crates/lance-graph-quack/tests/duckdb_differential.rs index 10cf78265..a6c801e00 100644 --- a/crates/lance-graph-quack/tests/duckdb_differential.rs +++ b/crates/lance-graph-quack/tests/duckdb_differential.rs @@ -754,7 +754,7 @@ fn join_sum_country() { /// `SELECT COUNT(*) FROM doc d WHERE EXISTS(SELECT 1 FROM line l WHERE /// l.doc_id=d.rid AND l.status=1)` — `COUNT(DISTINCT doc_id)` over the -/// posted lines. ONE spelling: [`Agg::CountDistinctClusteredU32`] → +/// posted lines. ONE spelling: [`Agg::CountDistinctOrderedU32`] → /// `Terminal::CountKeyRunsU32`, whose precondition is a key lane in key /// order — the T0 address projection of lines under their doc. /// @@ -764,7 +764,7 @@ fn join_sum_country() { /// query is valid, this physical lowering is not, and no seen-set is /// allocated in its place. The `1,2,1` falsifier lives in mask-risc /// `tests/distinct.rs`. -/// - GIVEN a doc-ordered view of the same lines, the fold answers the same +/// - GIVEN a doc-ORDERED view of the same lines, the fold answers the same /// DuckDB 511 with two words of state. The view is built by the test /// (`fixture_view_bytes`, a reordered copy): a semantic proof of the /// terminal, not a zero-materialisation proof of the query on this @@ -784,7 +784,7 @@ fn join_count_docs_with_posted() { let planes = lanes.planes(); let runs = lower(&Query { filter: Filter::cmp(STATUS, Cmp::EqU32(1)), - agg: Agg::CountDistinctClusteredU32 { key: DOC_ID_U32 }, + agg: Agg::CountDistinctOrderedU32 { key: DOC_ID_U32 }, }) .expect("lowers"); assert!(matches!(runs.terminal, Terminal::CountKeyRunsU32 { .. })); @@ -800,13 +800,13 @@ fn join_count_docs_with_posted() { reason=LaneNotOrdered population_state_bytes=0" ); - // ── Arm 2: a clustered VIEW handed to CountKeyRunsU32. The reorder below + // ── Arm 2: an ORDERED view handed to CountKeyRunsU32. The reorder below // is the test's own materialisation, counted as `fixture_view_bytes`. ── let mut order: Vec = (0..fx.line.doc_id_u32.len()).collect(); order.sort_by_key(|&i| fx.line.doc_id_u32[i]); // stable: runs, not a resort let by = |v: &[u32]| -> Vec { order.iter().map(|&i| v[i]).collect() }; let by_i = |v: &[i32]| -> Vec { order.iter().map(|&i| v[i]).collect() }; - let clustered = fixture::LineTable { + let ordered = fixture::LineTable { doc_id: by_i(&fx.line.doc_id), doc_id_u32: by(&fx.line.doc_id_u32), partner_id: by(&fx.line.partner_id), @@ -816,7 +816,7 @@ fn join_count_docs_with_posted() { cost_center: by(&fx.line.cost_center), gl_account: by(&fx.line.gl_account), }; - let c_lanes = clustered.lanes(); + let c_lanes = ordered.lanes(); let c_planes = c_lanes.planes(); let mut c_scratch = Scratch::for_program(&runs, c_planes.n_rows).expect("carves"); let c_tile_words = c_scratch.words(); @@ -840,12 +840,12 @@ fn join_count_docs_with_posted() { programs: Some(1), pair_relation_bytes: 0, population_state_bytes: 0, - // What the TEST built to hand the fold a clustered view: the + // What the TEST built to hand the fold an ordered view: the // permutation plus eight reordered 4-byte lanes. Not the terminal's // state — and not evidence that a resident doc-major view exists. fixture_view_bytes: order.len() * (std::mem::size_of::() + 8 * 4), }; - print_metric("join_count_docs_with_posted_clustered_view_given", &m); + print_metric("join_count_docs_with_posted_ordered_view_given", &m); // A distinct count is permutation-invariant: same DuckDB row, same answer. assert_case(&cases, "join_count_docs_with_posted", &c_encoded); } From 3b7843e3b1d1ebd6851d2e85315e8487c5da64a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 04:21:38 +0000 Subject: [PATCH 13/15] mask-risc: a tiled Keep requires Out::Mask; document the one run-time refusal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Under tiling, `Keep` now refuses every `out` shape but `Out::Mask` (`TerminalNeedsOut`), not only `Out::None`: with `Out::I32` — what the legacy `execute` wrapper passes for every terminal — the call returned `Value::Mask` over a scratch slot holding the last tile alone. Single-tile scratch keeps its old contract. Falsifier `a_tiled_keep_refuses_every_out_shape_but_mask`, disable-verified red. - `execute_into` / `execute` docs: validation errors leave scratch and `out` untouched; `LaneNotOrdered` from `CountKeyRunsU32` is the one run-time refusal — `out` still untouched (no sink), scratch may hold the tiles walked before the descent (ALU state, never a result). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/lance-graph-mask-risc/src/exec.rs | 22 +++- crates/lance-graph-mask-risc/src/reference.rs | 8 +- crates/lance-graph-mask-risc/tests/foreign.rs | 106 ++++++++++++++++++ 3 files changed, 128 insertions(+), 8 deletions(-) diff --git a/crates/lance-graph-mask-risc/src/exec.rs b/crates/lance-graph-mask-risc/src/exec.rs index f8b924d51..791b57e64 100644 --- a/crates/lance-graph-mask-risc/src/exec.rs +++ b/crates/lance-graph-mask-risc/src/exec.rs @@ -626,7 +626,8 @@ fn run_pred<'a>( /// Run `program` over `planes` with the caller's `scratch`; `out` is the /// destination a [`Terminal::BlendI32`] writes. Validation is total and /// happens before any result write, so an `Err` leaves the scratch slots and -/// `out` untouched. +/// `out` untouched (the one run-time refusal, and what it may leave in +/// scratch, is documented on [`execute_into`]). /// /// A thin wrapper over [`execute_into`]: no foreign planes, and `out` widened /// to [`Out::I32`] (or [`Out::None`] for `out: None`) — the shape every @@ -663,7 +664,12 @@ fn fold_opt(acc: Option, tile: Option, f: fn(i32, i32) -> i32) -> Opti /// terminal ignores it — an `Out` of the wrong shape for the terminal that IS /// present is refused by [`validate`], never silently accepted and silently /// not written). Validation is total and happens before any result write, so -/// an `Err` leaves the scratch slots and `out` untouched. +/// a validation `Err` leaves the scratch slots and `out` untouched. ONE +/// refusal is raised at run time instead: [`ExecError::LaneNotOrdered`] from +/// [`Terminal::CountKeyRunsU32`], at the first key descent, mid-walk. That +/// terminal has no sink, so `out` is still untouched; the scratch slots may +/// hold the tiles walked before the descent — scratch is ALU state, never a +/// result, and nothing reads it back as one. /// /// **Execution is tiled.** The program runs once per `scratch.words()`-word /// tile of the population; every op writes at most one tile of its slot, and @@ -714,7 +720,14 @@ pub fn execute_into( out_shape(&out), scratch.written_mut(), )?; - if matches!(program.terminal, Terminal::Keep { .. }) && matches!(out, Out::None) && tw < words { + // Under tiling a `Keep` result exists as a whole ONLY in a demanded + // `Out::Mask`: the scratch slot holds the last tile alone, so any other + // `out` shape (`None`, or the `I32` the legacy `execute` wrapper passes + // for every terminal) would return `Value::Mask` over a partial result. + if matches!(program.terminal, Terminal::Keep { .. }) + && !matches!(out, Out::Mask(_)) + && tw < words + { return Err(ExecError::TerminalNeedsOut { what: "Keep" }); } // The sinks that ACCUMULATE across tiles start from zero here, once. The @@ -941,7 +954,8 @@ pub fn execute_into( Terminal::CountKeyRunsU32 { mask, lane } => { // A key below the open run's key means the lane is not in // key order: refuse, never over-count. Sinks are untouched - // (this terminal has none) and scratch is ALU state. + // (this terminal has none); scratch is ALU state and may + // hold the tiles walked so far (see `execute_into`'s doc). match masked_key_run_count_u32( lane_u32(planes, lane, t), read(planes, &slots, mask, t), diff --git a/crates/lance-graph-mask-risc/src/reference.rs b/crates/lance-graph-mask-risc/src/reference.rs index b9fa59a5f..e7fd476ad 100644 --- a/crates/lance-graph-mask-risc/src/reference.rs +++ b/crates/lance-graph-mask-risc/src/reference.rs @@ -413,10 +413,10 @@ pub(crate) fn validate( check_operand(p, planes, mask)?; written_slots.readable(mask)?; // The kept mask is a DEMANDED sink: `Out::Mask` sized to the - // population, or `Out::None` for a caller whose single-tile - // scratch holds it (the executor refuses `None` under tiling). - // Any other shape is ignored, as `Keep` always ignored `out` - // (the legacy `execute` passes `Out::I32` for every terminal). + // population, or any other shape for a caller whose single-tile + // scratch holds it (the legacy `execute` passes `Out::I32` for + // every terminal). The oracle has no tiles; the executor refuses + // every non-`Mask` shape under tiling (`TerminalNeedsOut`). let want = words_for(n); match out { OutShape::Mask(len) if len != want => Err(ExecError::LenMismatch { diff --git a/crates/lance-graph-mask-risc/tests/foreign.rs b/crates/lance-graph-mask-risc/tests/foreign.rs index 99eec18b1..09e34574f 100644 --- a/crates/lance-graph-mask-risc/tests/foreign.rs +++ b/crates/lance-graph-mask-risc/tests/foreign.rs @@ -889,3 +889,109 @@ fn scatter_count_matches_the_oracle_and_an_independent_distinct_count() { } } } + +/// Under tiling a `Keep` result exists only in a demanded `Out::Mask`; every +/// other `out` shape — `None`, and the `I32` the legacy `execute` wrapper +/// passes for every terminal — must be refused, not answered with a +/// `Value::Mask` whose scratch slot holds the last tile alone. +#[test] +fn a_tiled_keep_refuses_every_out_shape_but_mask() { + let n = 600usize; // words_for(600) == 10 > the 8-word tile below + let lane: Vec = (0..n as u32).collect(); + let lanes = [LaneRef::U32(&lane)]; + let planes = Planes { + n_rows: n, + masks: &[], + lanes: &lanes, + }; + let p = Program::new( + vec![MaskOp::Pred { + pred: Pred::NeU32 { lane: 0, v: 7 }, + under: None, + dst: 0, + }], + Terminal::Keep { mask: S0 }, + ); + let slots = p.scratch_slots as usize; + let tile = 8usize; + let mut buf = vec![0u64; scratch_words_for(tile, slots).expect("sized")]; + + let mut i32_out = [0i32; 1]; + let mut scratch = Scratch::over(&mut buf, tile, slots).expect("carves"); + assert_eq!( + execute_into( + &p, + &planes, + &Foreign::NONE, + &mut scratch, + Out::I32(&mut i32_out) + ), + Err(ExecError::TerminalNeedsOut { what: "Keep" }), + "Out::I32 under tiling" + ); + let mut i64_out = [0i64; 1]; + let mut scratch = Scratch::over(&mut buf, tile, slots).expect("carves"); + assert_eq!( + execute_into( + &p, + &planes, + &Foreign::NONE, + &mut scratch, + Out::I64(&mut i64_out) + ), + Err(ExecError::TerminalNeedsOut { what: "Keep" }), + "Out::I64 under tiling" + ); + let mut scratch = Scratch::over(&mut buf, tile, slots).expect("carves"); + assert_eq!( + execute_into(&p, &planes, &Foreign::NONE, &mut scratch, Out::None), + Err(ExecError::TerminalNeedsOut { what: "Keep" }), + "Out::None under tiling" + ); + // The demanded sink is the one shape that carries a whole result. + let mut mask_out = vec![0u64; words_for(n)]; + let mut scratch = Scratch::over(&mut buf, tile, slots).expect("carves"); + assert_eq!( + execute_into( + &p, + &planes, + &Foreign::NONE, + &mut scratch, + Out::Mask(&mut mask_out) + ), + Ok(Value::Mask(S0)) + ); + assert_eq!( + mask_out + .iter() + .map(|w| w.count_ones() as usize) + .sum::(), + n - 1 + ); + // ...and the legacy single-tile shape keeps its old contract: one tile + // is the whole population, so `Out::I32` is ignored and the slot IS the + // result. + let whole = words_for(n); + let mut whole_buf = vec![0u64; scratch_words_for(whole, slots).expect("sized")]; + let mut scratch = Scratch::over(&mut whole_buf, whole, slots).expect("carves"); + let mut ignored = [0i32; 1]; + assert_eq!( + execute_into( + &p, + &planes, + &Foreign::NONE, + &mut scratch, + Out::I32(&mut ignored) + ), + Ok(Value::Mask(S0)) + ); + assert_eq!( + scratch + .slot(0) + .unwrap() + .iter() + .map(|w| w.count_ones() as usize) + .sum::(), + n - 1 + ); +} From 95a4d744b57ee331b5e186921814c90806582e00 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 04:22:37 +0000 Subject: [PATCH 14/15] docs: fixture doc no longer names a partner Planes; perturbation-sim README states the sibling checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `col::partner` never existed on this branch — the partner table is read only through `line.partner_id` as `Foreign::lanes`. The README note says what the manifest comment already does: the path dep is resolved even with `ndarray-simd` off, so the sibling `../ndarray` checkout is required. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/lance-graph-quack/tests/duckdb/fixture.rs | 8 ++++---- crates/perturbation-sim/README.md | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/lance-graph-quack/tests/duckdb/fixture.rs b/crates/lance-graph-quack/tests/duckdb/fixture.rs index 9c3578221..7e9d95472 100644 --- a/crates/lance-graph-quack/tests/duckdb/fixture.rs +++ b/crates/lance-graph-quack/tests/duckdb/fixture.rs @@ -248,10 +248,10 @@ impl Fixture { /// The `line` table's lanes, in the fixed order the `Col` constants below /// index into. `line` is the table every lowered [`lance_graph_quack::Query`] -/// runs against; `partner` is now ALSO exposed as [`Planes`] (see -/// [`col::partner`]) so the two join cases can lower a real partner-side -/// filter to a real `Program` — `doc` still exists only as CSV rows for the -/// oracle, since no case in this suite filters it directly. +/// runs against. `partner` is never run as its own [`Planes`]: its lanes are +/// read THROUGH `line.partner_id` as `Foreign::lanes` (see the note below the +/// `col` module). `doc` exists only as CSV rows for the oracle, since no case +/// in this suite filters it directly. pub mod col { use lance_graph_quack::Col; diff --git a/crates/perturbation-sim/README.md b/crates/perturbation-sim/README.md index c2f08e0f1..4b610ca78 100644 --- a/crates/perturbation-sim/README.md +++ b/crates/perturbation-sim/README.md @@ -31,6 +31,12 @@ flow-shift field + the trip footprint (which lines tripped, in which round). ## Run it +> The `ndarray-simd` feature resolves `ndarray` from the sibling checkout +> `../ndarray` beside this repository (a path dependency, one Cargo package +> identity for the whole tree). Cargo reads that manifest at resolution even +> with the feature OFF, so clone `AdaWorldAPI/ndarray` next to `lance-graph` +> before either command; CI does the same. + ```sh cargo test --manifest-path crates/perturbation-sim/Cargo.toml cargo run --manifest-path crates/perturbation-sim/Cargo.toml --example simulate From d6d8002b6cfa560b93ef197e98c480edf9f51aa2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 04:24:31 +0000 Subject: [PATCH 15/15] mask-risc: the one-materialiser guard reads whole signatures A `pub fn` whose parameters wrap carries its `->` on a later line, so the line-based read missed `reference_scratch_with_foreign` (returns `Vec>`). The guard now parses each declaration up to its body; the oracle's second arena-copy entry is named in the exemption list. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG --- crates/lance-graph-mask-risc/src/exec.rs | 33 +++++++++++++++--------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/lance-graph-mask-risc/src/exec.rs b/crates/lance-graph-mask-risc/src/exec.rs index 791b57e64..9bf9ff9ca 100644 --- a/crates/lance-graph-mask-risc/src/exec.rs +++ b/crates/lance-graph-mask-risc/src/exec.rs @@ -1437,27 +1437,36 @@ mod tests { let mut found = Vec::new(); for src in CRATE_SOURCES { let production = src.split("#[cfg(test)]").next().unwrap_or(""); - for line in production.lines() { - let t = line.trim_start(); + // Read each signature WHOLE, up to its body: a `pub fn` whose + // parameters wrap onto several lines carries its `->` on a later + // line, and a guard that reads the first line alone cannot see + // it (`reference_scratch_with_foreign` escaped exactly so). + for decl in production.split("pub fn ").skip(1) { + let sig = decl.split('{').next().unwrap_or(""); // ANY owning collection RETURN, not just `Vec`: the // oracle's arena copy is a `Vec>`, and a guard that // greps the one shape it knows cannot see the next one. Split // on the arrow so a `Vec` PARAMETER is not read as a return. - let returns = t.split("->").nth(1).unwrap_or(""); - if t.starts_with("pub fn ") && returns.contains("Vec<") { - let name = t - .strip_prefix("pub fn ") - .and_then(|r| r.split('(').next()) - .unwrap_or(t); + let returns = sig.split("->").nth(1).unwrap_or(""); + if returns.contains("Vec<") { + let name = sig.split('(').next().unwrap_or(sig).trim(); found.push(name.to_string()); } } } - // `reference_scratch` is the ONE written-down exemption: the oracle - // must hold its own unpacked reading of the arena, because an oracle + // `reference_scratch` / `reference_scratch_with_foreign` are the ONE + // written-down exemption in its two entry shapes: the oracle must + // hold its own unpacked reading of the arena, because an oracle // sharing the executor's bit packing could not falsify a packing bug - // (law L4). Named here so the guard SEES it. - assert_eq!(found, ["materialize_rows", "reference_scratch"]); + // (law L4). Named here so the guard SEES them. + assert_eq!( + found, + [ + "materialize_rows", + "reference_scratch", + "reference_scratch_with_foreign" + ] + ); } /// FAILS IF: `materialize_rows` reads phantom bits past `n_rows` or