Skip to content

Commit d8a2194

Browse files
ahrzbclaude
andcommitted
feat(specializer): stage-B live-stack loop lowering + corpus harness retry — 529 -> 546, 0 FAIL (TASK-59 stage B-3)
Predicate/expression emission may SPLIT blocks (CASE machinery), so the multiplicity loop now rides its state and probe lanes on the LIVE stack (auto-rebound across splits; lanes are always the trailing entries) and re-seeds the per-block probe cache before every emission that can reference join columns — fixes the three corpus binder failures. The keyless path needed nothing: wave-4's empty-key binding already covers cross joins, inequality ON, and constant ON under 'many' (test added). Corpus replay retries 'duplicate map key' rejections with shape='many' (the default's rejection stays proven); fn.shape now reports 'many'. Corpus: 546 match / 132 clean-unsupported / 0 FAIL of 678 (was 529 — the full dup-key + cross/inequality bucket). Self-joins are B-4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 9089e5e commit d8a2194

4 files changed

Lines changed: 218 additions & 99 deletions

File tree

src/duckdb/mod.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,8 @@ pub struct DuckDBInferFn {
495495
#[pyo3(get)]
496496
output_model: Py<PyAny>,
497497
output_dicts: bool,
498-
shape_map: bool,
498+
/// 0 = filter, 1 = map, 2 = many (the declared row-shape contract).
499+
shape_kind: u8,
499500
}
500501

501502
#[pymethods]
@@ -517,6 +518,12 @@ impl DuckDBInferFn {
517518
// join multiplicity (stage B) and is the only shape under which
518519
// those constructs will ever build.
519520
let many = shape.as_deref() == Some("many");
521+
let shape_kind: u8 = match shape.as_deref() {
522+
None | Some("filter") => 0,
523+
Some("map") => 1,
524+
Some("many") => 2,
525+
Some(_) => 0, // rejected below
526+
};
520527
let strict_map = match shape.as_deref() {
521528
None | Some("filter") => false,
522529
Some("map") => true,
@@ -689,7 +696,7 @@ impl DuckDBInferFn {
689696
row_table,
690697
output_model,
691698
output_dicts,
692-
shape_map: false,
699+
shape_kind,
693700
});
694701
}
695702
Err(_) => return Err(build_err(e.to_string())),
@@ -781,17 +788,17 @@ impl DuckDBInferFn {
781788
row_table,
782789
output_model,
783790
output_dicts,
784-
shape_map: strict_map,
791+
shape_kind,
785792
})
786793
}
787794

788-
/// The row-shape contract: "map" (proven exactly-1) or "filter" (0..1).
795+
/// The declared row-shape contract: "map", "filter", or "many".
789796
#[getter]
790797
fn shape(&self) -> &'static str {
791-
if self.shape_map {
792-
"map"
793-
} else {
794-
"filter"
798+
match self.shape_kind {
799+
1 => "map",
800+
2 => "many",
801+
_ => "filter",
795802
}
796803
}
797804

src/specializer/lower.rs

Lines changed: 118 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,16 +1050,19 @@ impl<'a> FB<'a> {
10501050
/// entry: key exprs (trap per input row), ProbeRange -> [lo, hi)
10511051
/// (NULL keys force an EMPTY range — a NULL never matches),
10521052
/// jump header(lo, hi, any=false)
1053-
/// header(i, end, any): i < end ? body(i, end, any) : after(any)
1054-
/// body: ProbeRead lanes; residual AND WHERE (3VL) gate BEFORE any
1055-
/// store (the continue path must be store-free); pass ->
1056-
/// store_blk(lanes.., i+1, end) -> stores -> emit.to header
1057-
/// (any=true); fail -> header(i+1, end, any)
1053+
/// header(i, end, any): i < end ? body : after(any)
1054+
/// body: ProbeRead lanes; the RESIDUAL gates match-ness (and the
1055+
/// `any` flag); WHERE only gates emission (DuckDB joins
1056+
/// first, filters second); pass -> stores -> emit.to header
1057+
/// (any=true); fail -> continue.
10581058
/// after: INNER -> skip. LEFT -> any ? skip : null-extension row
10591059
/// (default lanes, hit=false) gated by WHERE, then emit.
10601060
///
1061-
/// The engine's documented emission order falls out: probe rows in
1062-
/// input order, matches contiguous in build INSERTION order.
1061+
/// Predicate/expression emission may SPLIT blocks (CASE machinery), so
1062+
/// the loop state and probe lanes ride the LIVE stack (auto-rebound
1063+
/// across splits) with the invariant: the probe lanes are always the
1064+
/// LAST `nd` live entries; the per-block probe cache is re-seeded from
1065+
/// them before every emission that can reference join columns.
10631066
fn lower_many_loop(
10641067
&mut self,
10651068
exprs: &[(String, SExpr)],
@@ -1071,9 +1074,10 @@ impl<'a> FB<'a> {
10711074
let residual = self.joins[0].residual.clone();
10721075
let keys_expr = self.joins[0].keys.clone();
10731076
let dst_tys: Vec<Ty> = self.val_flat_tys(j);
1077+
let nd = dst_tys.len();
10741078

10751079
// entry: keys + range.
1076-
let mut live = Vec::new();
1080+
let mut live: Live = Vec::new();
10771081
let mut keys_valid: Option<Value> = None;
10781082
let mut key_vals = Vec::with_capacity(keys_expr.len());
10791083
for key in &keys_expr {
@@ -1140,93 +1144,99 @@ impl<'a> FB<'a> {
11401144
else_args: vec![h_any],
11411145
});
11421146

1143-
// body: lanes + gate.
1147+
// body: lanes + the two gates, everything riding live as
1148+
// [inext, end, any, dsts...].
11441149
self.switch(body);
11451150
let (b_i, b_end, b_any) = (bp[0], bp[1], bp[2]);
1146-
let dsts: Vec<Value> = (0..dst_tys.len()).map(|_| self.fresh()).collect();
1151+
let dsts: Vec<Value> = (0..nd).map(|_| self.fresh()).collect();
11471152
self.inst(Inst::ProbeRead {
11481153
static_id: j,
11491154
idx: b_i,
11501155
dsts: dsts.clone(),
11511156
});
1152-
let t1 = self.const_i1(true);
1153-
self.blocks[self.cur].probes.insert(j, (t1, dsts.clone()));
11541157
let one = self.const_lit(Lit::I64(1));
11551158
let inext = self.bin(BinOp::Iadd, b_i, one);
1156-
// Gate 1 — the RESIDUAL decides match-ness (and therefore `any`);
1157-
// a residual-failed candidate continues with `any` UNCHANGED.
1158-
let mut match_tys = dst_tys.clone();
1159-
match_tys.push(Ty::I64); // inext
1160-
match_tys.push(Ty::I64); // end
1159+
let mut live: Live = Vec::new();
1160+
live.push((Lane { flag: None, val: inext }, Ty::I64));
1161+
live.push((Lane { flag: None, val: b_end }, Ty::I64));
1162+
live.push((Lane { flag: None, val: b_any }, Ty::I1));
1163+
for (&d, &ty) in dsts.iter().zip(dst_tys.iter()) {
1164+
live.push((Lane { flag: None, val: d }, ty));
1165+
}
1166+
let rv = match &residual {
1167+
None => None,
1168+
Some(res) => {
1169+
self.reseed_many(j, true, &live, nd);
1170+
let rl = self.emit(res, &mut live)?;
1171+
Some(self.truthy(rl))
1172+
}
1173+
};
1174+
let vals: Vec<Value> = live.iter().map(|(l, _)| l.val).collect();
1175+
let (r_inext, r_end, r_any) = (vals[0], vals[1], vals[2]);
1176+
let r_dsts: Vec<Value> = vals[3..].to_vec();
1177+
live.clear();
1178+
let mut match_tys = vec![Ty::I64, Ty::I64];
1179+
match_tys.extend(dst_tys.iter().copied());
11611180
let (matched_blk, mp) = self.create_block(&match_tys);
1162-
let mut match_args: Vec<Value> = dsts.clone();
1163-
match_args.push(inext);
1164-
match_args.push(b_end);
1165-
match &residual {
1181+
let mut match_args = vec![r_inext, r_end];
1182+
match_args.extend(r_dsts.iter().copied());
1183+
match rv {
11661184
None => self.term(Term::Jump {
11671185
to: BlockId(matched_blk as u32),
11681186
args: match_args,
11691187
}),
1170-
Some(res) => {
1171-
let mut live = Vec::new();
1172-
let rl = self.emit(res, &mut live)?;
1173-
let rv = self.truthy(rl);
1174-
self.term(Term::Brif {
1175-
cond: rv,
1176-
then_to: BlockId(matched_blk as u32),
1177-
then_args: match_args,
1178-
else_to: BlockId(header as u32),
1179-
else_args: vec![inext, b_end, b_any],
1180-
});
1181-
}
1188+
Some(cond) => self.term(Term::Brif {
1189+
cond,
1190+
then_to: BlockId(matched_blk as u32),
1191+
then_args: match_args,
1192+
else_to: BlockId(header as u32),
1193+
else_args: vec![r_inext, r_end, r_any],
1194+
}),
11821195
}
11831196

1184-
// matched_blk: `any` is TRUE from here on, WHERE only gates the
1185-
// emission (DuckDB joins first, filters second — a match killed by
1186-
// WHERE still suppresses the LEFT null-extension).
1197+
// matched_blk: `any` is TRUE from here on; WHERE gates emission
1198+
// only. live = [inext, end, dsts...].
11871199
self.switch(matched_blk);
1188-
let m_dsts: Vec<Value> = mp[..dst_tys.len()].to_vec();
1189-
let (m_inext, m_end) = (mp[dst_tys.len()], mp[dst_tys.len() + 1]);
1190-
let tm = self.const_i1(true);
1191-
self.blocks[self.cur].probes.insert(j, (tm, m_dsts.clone()));
1192-
let mut store_tys = dst_tys.clone();
1193-
store_tys.push(Ty::I64); // inext
1194-
store_tys.push(Ty::I64); // end
1195-
let (store_blk, sp) = self.create_block(&store_tys);
1196-
let mut store_args: Vec<Value> = m_dsts.clone();
1197-
store_args.push(m_inext);
1198-
store_args.push(m_end);
1199-
match filter_pred {
1200-
None => self.term(Term::Jump {
1201-
to: BlockId(store_blk as u32),
1202-
args: store_args,
1203-
}),
1204-
Some(pred) => {
1205-
let mut live = Vec::new();
1206-
let pl = self.emit(pred, &mut live)?;
1207-
let pv = self.truthy(pl);
1208-
let tt = self.const_i1(true);
1209-
self.term(Term::Brif {
1210-
cond: pv,
1211-
then_to: BlockId(store_blk as u32),
1212-
then_args: store_args,
1213-
else_to: BlockId(header as u32),
1214-
else_args: vec![m_inext, m_end, tt],
1215-
});
1200+
let mut live: Live = Vec::new();
1201+
live.push((Lane { flag: None, val: mp[0] }, Ty::I64));
1202+
live.push((Lane { flag: None, val: mp[1] }, Ty::I64));
1203+
for (&d, &ty) in mp[2..].iter().zip(dst_tys.iter()) {
1204+
live.push((Lane { flag: None, val: d }, ty));
1205+
}
1206+
if let Some(pred) = filter_pred {
1207+
self.reseed_many(j, true, &live, nd);
1208+
let pl = self.emit(pred, &mut live)?;
1209+
let pv = self.truthy(pl);
1210+
let vals: Vec<Value> = live.iter().map(|(l, _)| l.val).collect();
1211+
let (keep, kp) = {
1212+
let mut tys = vec![Ty::I64, Ty::I64];
1213+
tys.extend(dst_tys.iter().copied());
1214+
self.create_block(&tys)
1215+
};
1216+
let mut keep_args = vec![vals[0], vals[1]];
1217+
keep_args.extend(vals[2..].iter().copied());
1218+
let tt = self.const_i1(true);
1219+
self.term(Term::Brif {
1220+
cond: pv,
1221+
then_to: BlockId(keep as u32),
1222+
then_args: keep_args,
1223+
else_to: BlockId(header as u32),
1224+
else_args: vec![vals[0], vals[1], tt],
1225+
});
1226+
self.switch(keep);
1227+
live.clear();
1228+
live.push((Lane { flag: None, val: kp[0] }, Ty::I64));
1229+
live.push((Lane { flag: None, val: kp[1] }, Ty::I64));
1230+
for (&d, &ty) in kp[2..].iter().zip(dst_tys.iter()) {
1231+
live.push((Lane { flag: None, val: d }, ty));
12161232
}
12171233
}
1218-
1219-
// store_blk: seeded lanes -> stores -> emit.to header(any=true).
1220-
self.switch(store_blk);
1221-
let s_dsts: Vec<Value> = sp[..dst_tys.len()].to_vec();
1222-
let (s_inext, s_end) = (sp[dst_tys.len()], sp[dst_tys.len() + 1]);
1223-
let t2 = self.const_i1(true);
1224-
self.blocks[self.cur].probes.insert(j, (t2, s_dsts));
1225-
self.store_out_row(exprs, out_cols)?;
1234+
self.store_out_row(exprs, out_cols, Some((j, true)), &mut live, nd)?;
1235+
let vals: Vec<Value> = live.iter().map(|(l, _)| l.val).collect();
12261236
let t3 = self.const_i1(true);
12271237
self.term(Term::EmitTo {
12281238
to: BlockId(header as u32),
1229-
args: vec![s_inext, s_end, t3],
1239+
args: vec![vals[0], vals[1], t3],
12301240
});
12311241

12321242
// after: LEFT null-extension or skip.
@@ -1243,32 +1253,36 @@ impl<'a> FB<'a> {
12431253
else_args: vec![],
12441254
});
12451255
self.switch(miss);
1256+
// live = [dsts(defaults)...] only.
1257+
let mut live: Live = Vec::new();
1258+
for &ty in dst_tys.iter() {
1259+
let d = self.default_of(ty);
1260+
live.push((Lane { flag: None, val: d }, ty));
1261+
}
12461262
if let Some(pred) = filter_pred {
12471263
// WHERE sees the null-extended row too (measured).
1248-
let fmiss = self.const_i1(false);
1249-
let defaults: Vec<Value> =
1250-
dst_tys.iter().map(|&ty| self.default_of(ty)).collect();
1251-
self.blocks[self.cur].probes.insert(j, (fmiss, defaults));
1252-
let mut live = Vec::new();
1264+
self.reseed_many(j, false, &live, nd);
12531265
let pl = self.emit(pred, &mut live)?;
12541266
let pv = self.truthy(pl);
1255-
let (keep, _) = self.create_block(&[]);
1267+
let vals: Vec<Value> = live.iter().map(|(l, _)| l.val).collect();
1268+
let (keep, kp) = self.create_block(&dst_tys);
12561269
let (drop, _) = self.create_block(&[]);
12571270
self.term(Term::Brif {
12581271
cond: pv,
12591272
then_to: BlockId(keep as u32),
1260-
then_args: vec![],
1273+
then_args: vals,
12611274
else_to: BlockId(drop as u32),
12621275
else_args: vec![],
12631276
});
12641277
self.switch(drop);
12651278
self.term(Term::Skip);
12661279
self.switch(keep);
1280+
live.clear();
1281+
for (&d, &ty) in kp.iter().zip(dst_tys.iter()) {
1282+
live.push((Lane { flag: None, val: d }, ty));
1283+
}
12671284
}
1268-
let fmiss2 = self.const_i1(false);
1269-
let defaults2: Vec<Value> = dst_tys.iter().map(|&ty| self.default_of(ty)).collect();
1270-
self.blocks[self.cur].probes.insert(j, (fmiss2, defaults2));
1271-
self.store_out_row(exprs, out_cols)?;
1285+
self.store_out_row(exprs, out_cols, Some((j, false)), &mut live, nd)?;
12721286
self.term(Term::Emit);
12731287
self.switch(done);
12741288
self.term(Term::Skip);
@@ -1278,17 +1292,32 @@ impl<'a> FB<'a> {
12781292
Ok(())
12791293
}
12801294

1281-
/// Emit every output expression and store it (the shared tail of both
1282-
/// lowerings) — the current block's caches must already be seeded.
1295+
/// Re-seed the CURRENT block's probe cache for many-join `j` from the
1296+
/// live stack's trailing `nd` lanes (the invariant of the loop
1297+
/// lowering) with a fresh hit constant.
1298+
fn reseed_many(&mut self, j: u32, hit: bool, live: &Live, nd: usize) {
1299+
let h = self.const_i1(hit);
1300+
let ds: Vec<Value> = live[live.len() - nd..].iter().map(|(l, _)| l.val).collect();
1301+
self.blocks[self.cur].probes.insert(j, (h, ds));
1302+
}
1303+
1304+
/// Emit every output expression and store it. Under a many-join
1305+
/// (`seed` present) the probe cache is re-seeded from `live`'s
1306+
/// trailing `nd` lanes before EVERY expression — emission may split
1307+
/// blocks, and each new block starts with an empty cache.
12831308
fn store_out_row(
12841309
&mut self,
12851310
exprs: &[(String, SExpr)],
12861311
out_cols: &[Col],
1312+
seed: Option<(u32, bool)>,
1313+
live: &mut Live,
1314+
nd: usize,
12871315
) -> Result<(), PrepareError> {
1288-
let mut live = Vec::new();
12891316
for (ci, (_, e)) in exprs.iter().enumerate() {
1290-
debug_assert!(live.is_empty());
1291-
let lane = self.emit(e, &mut live)?;
1317+
if let Some((j, hit)) = seed {
1318+
self.reseed_many(j, hit, live, nd);
1319+
}
1320+
let lane = self.emit(e, live)?;
12921321
let col = ci as u32;
12931322
if out_cols[ci].ty.nullable {
12941323
let flag = match lane.flag {

0 commit comments

Comments
 (0)