Skip to content

Commit 0362b37

Browse files
ahrzbclaude
andcommitted
feat(specializer): cranelift backend — full IR coverage, 500-seed differential green, wired behind DuckDBInferFn (TASK-44 stretches 1-3)
One JIT'd function per program, called per row: extern "C" fn(*mut Cx) -> i64 (0 emit / 1 skip / 2 helper trap / 3+k term trap). Coverage-first: float arith, int compares, logic, select, itof, fabs, and consts map inline to CLIF; everything nontrivial — checked int arith, all string ops, loads/stores, statics, probes — calls extern helpers that delegate to the interpreter's own semantic functions (casemap, substr_window, duck_fcmp, DuckF64, the arena), so the backends cannot drift where they share code. Strings travel as (offset, length) i64 pairs; trap_flag at Cx offset 0 is checked after every fallible helper call. CraneliftFn owns a compiled InterpFn for input/state checks, prepared statics, and the always-available fallback. The 500-seed random-IR differential (fuzz_cranelift_agrees_with_ interpreter) demands byte-identical outputs and traps — it caught two real bugs before landing: load.opt and sload.opt must NORMALIZE payloads to type defaults under a false flag, exactly as the interpreter documents. DuckDBInferFn now compiles cranelift-first with interpreter fallback (AC #2) and exposes .backend; the whole pytest suite — 32 duck_check differentials and the 678-case corpus replay at 0 FAILs — now exercises the JIT. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6fd137b commit 0362b37

5 files changed

Lines changed: 1484 additions & 18 deletions

File tree

src/duckdb/mod.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ use pyo3::types::PyDict;
1414

1515
use crate::error::InterpError;
1616
use crate::schema;
17+
use crate::specializer::exec::cranelift::{self, CraneliftFn};
1718
use crate::specializer::exec::interp::{compile, InterpFn};
1819
use crate::specializer::exec::{Batch, ColData, KeyBits, OutCol, ScalarVal, StaticData};
20+
use crate::specializer::exec::{RunState, Trap};
1921
use crate::specializer::ir::{Col, ColTy, StaticTy, Ty};
2022
use crate::specializer::plan::StaticTable;
2123
use crate::specializer::{prepare, StaticSpec};
@@ -169,9 +171,38 @@ fn eval_static_only(
169171
Ok((rows, fields))
170172
}
171173

174+
/// The execution backend: cranelift when it compiles, the interpreter as
175+
/// the always-available fallback (AC #2 — an uncovered op must not fail
176+
/// prepare). Both agree byte-for-byte by the 500-seed differential.
177+
enum Backend {
178+
Cranelift(CraneliftFn),
179+
Interp(InterpFn),
180+
}
181+
182+
impl Backend {
183+
fn name(&self) -> &'static str {
184+
match self {
185+
Backend::Cranelift(_) => "cranelift",
186+
Backend::Interp(_) => "interpreter",
187+
}
188+
}
189+
fn new_state(&self) -> RunState {
190+
match self {
191+
Backend::Cranelift(f) => f.new_state(),
192+
Backend::Interp(f) => f.new_state(),
193+
}
194+
}
195+
fn run(&self, input: &Batch, st: &mut RunState) -> Result<(), Trap> {
196+
match self {
197+
Backend::Cranelift(f) => f.run(input, st),
198+
Backend::Interp(f) => f.run(input, st),
199+
}
200+
}
201+
}
202+
172203
enum Engine {
173204
Compiled {
174-
fun: InterpFn,
205+
fun: Backend,
175206
in_cols: Vec<Col>,
176207
out_cols: Vec<Col>,
177208
},
@@ -293,7 +324,26 @@ impl DuckDBInferFn {
293324
data.push(materialize_map(py, table, spec, keys, values)?);
294325
}
295326

296-
let fun = compile(&prepared.program, data).map_err(|e| build_err(e.to_string()))?;
327+
let fun = match cranelift::compile(&prepared.program, data) {
328+
Ok(f) => Backend::Cranelift(f),
329+
// The failed attempt consumed the static data; rematerialize on
330+
// this cold path and fall back to the interpreter.
331+
Err(_) => {
332+
let mut data = Vec::with_capacity(prepared.statics.len());
333+
for (spec, sty) in prepared.statics.iter().zip(&prepared.program.statics) {
334+
let StaticTy::Map { keys, values } = sty else {
335+
return Err(build_err("internal: v0 lowering emits only map statics"));
336+
};
337+
let table = static_tables
338+
.get(&spec.table)
339+
.expect("spec names come from the catalog");
340+
data.push(materialize_map(py, table, spec, keys, values)?);
341+
}
342+
Backend::Interp(
343+
compile(&prepared.program, data).map_err(|e| build_err(e.to_string()))?,
344+
)
345+
}
346+
};
297347
let output_model = match output_model {
298348
// Supplied models are trusted as-is in v0 (no shape validation).
299349
Some(m) => m,
@@ -310,6 +360,15 @@ impl DuckDBInferFn {
310360
})
311361
}
312362

363+
/// Which engine executes: "cranelift", "interpreter", or "constant".
364+
#[getter]
365+
fn backend(&self) -> &'static str {
366+
match &self.engine {
367+
Engine::Compiled { fun, .. } => fun.name(),
368+
Engine::Constant { .. } => "constant",
369+
}
370+
}
371+
313372
#[pyo3(signature = (tables=None, **kwargs))]
314373
fn infer(
315374
&self,

0 commit comments

Comments
 (0)