Skip to content

Commit 75b7380

Browse files
ahrzbclaude
andcommitted
feat: extern (UDF) calls in the IR and both backends (DRAFT-22 step 2, engine half)
New 'extern @n' program header + 'ecall' instruction: args are a (validity, payload) pair per declared param; dsts are a whole-call validity (NULL list vs list-of-NULLs at the width-k boundary) plus a (validity, payload) pair per declared return. Both backends route through one shared call_extern that enforces the declared return shape — wrong length/type or a raised error is a named trap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 886f167 commit 75b7380

11 files changed

Lines changed: 789 additions & 7 deletions

File tree

packages/confit/src/specializer/exec/cranelift.rs

Lines changed: 163 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ struct Cx {
5959
statics_len: usize,
6060
regexes: *const CompiledRe,
6161
regexes_len: usize,
62+
externs: *const super::ExternImpl,
63+
externs_len: usize,
6264
trap: Option<Trap>,
6365
}
6466

@@ -96,6 +98,14 @@ struct ProbeDesc {
9698
val_tys: Vec<Ty>,
9799
}
98100

101+
/// Per-ecall-site descriptor owned by the `CraneliftFn`; the JIT'd code
102+
/// passes its absolute address to `h_extern`.
103+
struct ExternDesc {
104+
ext_id: usize,
105+
param_tys: Vec<Ty>,
106+
ret_tys: Vec<Ty>,
107+
}
108+
99109
/// One 16-byte scratch cell: scalar payload in `[0]` (f64 as bits, bool as
100110
/// 0/1), strings as (offset, length).
101111
type Cell = [u64; 2];
@@ -901,6 +911,57 @@ extern "C" fn h_probe(
901911
}
902912
}
903913

914+
/// Extern (UDF) call: gather `Option<ScalarVal>` args from the arg cells,
915+
/// delegate to the SAME [`interp::call_extern`] the interpreter uses, and
916+
/// write the whole-call validity plus (validity, payload) pairs to the out
917+
/// cells. A callable error or declaration violation sets the trap flag.
918+
extern "C" fn h_extern(p: *mut Cx, desc: *const ExternDesc, args: *const Cell, outs: *mut Cell) {
919+
let c = unsafe { cx(p) };
920+
let desc = unsafe { &*desc };
921+
let args = unsafe { std::slice::from_raw_parts(args, 2 * desc.param_tys.len()) };
922+
let mut a = Vec::with_capacity(desc.param_tys.len());
923+
{
924+
let arena = unsafe { &*c.arena };
925+
for (j, &ty) in desc.param_tys.iter().enumerate() {
926+
let valid = args[2 * j][0] != 0;
927+
let cell = &args[2 * j + 1];
928+
a.push(if valid {
929+
Some(match ty {
930+
Ty::I1 => ScalarVal::I1(cell[0] != 0),
931+
Ty::I64 => ScalarVal::I64(cell[0] as i64),
932+
Ty::F64 => ScalarVal::F64(f64::from_bits(cell[0])),
933+
Ty::Str => ScalarVal::Str(
934+
arena.get(span(cell[0] as i64, cell[1] as i64)).to_string(),
935+
),
936+
})
937+
} else {
938+
None
939+
});
940+
}
941+
}
942+
let imps = unsafe { std::slice::from_raw_parts(c.externs, c.externs_len) };
943+
match interp::call_extern(&imps[desc.ext_id], &desc.ret_tys, &a) {
944+
Err(t) => c.set_trap(t.0),
945+
Ok((whole, comps)) => {
946+
unsafe { *outs = [whole as u64, 0] };
947+
let arena = c.arena();
948+
for (j, (f, v)) in comps.iter().enumerate() {
949+
unsafe { *outs.add(1 + 2 * j) = [*f as u64, 0] };
950+
let cell: Cell = match v {
951+
ScalarVal::I1(x) => [*x as u64, 0],
952+
ScalarVal::I64(x) => [*x as u64, 0],
953+
ScalarVal::F64(x) => [x.to_bits(), 0],
954+
ScalarVal::Str(s) => {
955+
let r = arena.push_str(s);
956+
[r.off as u64, r.len as u64]
957+
}
958+
};
959+
unsafe { *outs.add(2 + 2 * j) = cell };
960+
}
961+
}
962+
}
963+
}
964+
904965
// ------------------------------------------------------------ the JIT'd fn --
905966

906967
type RowFn = extern "C" fn(*mut Cx) -> i64;
@@ -916,6 +977,7 @@ pub struct CraneliftFn {
916977
/// Owned backing for absolute addresses baked into the code.
917978
_const_strs: Vec<Box<str>>,
918979
_probe_descs: Vec<Box<ProbeDesc>>,
980+
_extern_descs: Vec<Box<ExternDesc>>,
919981
}
920982

921983
/// A JIT'd value: scalars are one CLIF value, strings are (offset, length).
@@ -950,6 +1012,16 @@ fn clif_ty(ty: Ty) -> types::Type {
9501012
}
9511013

9521014
pub fn compile(p: &Program, statics: Vec<super::StaticData>) -> Result<CraneliftFn, CompileError> {
1015+
compile_ext(p, statics, Vec::new())
1016+
}
1017+
1018+
/// [`compile`] plus the extern (UDF) implementations — one per program
1019+
/// `extern @N`, validated by the interpreter compile below.
1020+
pub fn compile_ext(
1021+
p: &Program,
1022+
statics: Vec<super::StaticData>,
1023+
externs: Vec<super::ExternImpl>,
1024+
) -> Result<CraneliftFn, CompileError> {
9531025
// Stage-B multiplicity programs (EmitTo loops / multimap probes) are
9541026
// interpreter-only for now: rejecting here makes the caller's existing
9551027
// interp fallback the documented 'many' path. The match arms below can
@@ -970,8 +1042,8 @@ pub fn compile(p: &Program, statics: Vec<super::StaticData>) -> Result<Cranelift
9701042
));
9711043
}
9721044
// The interpreter compile also runs verify + prepare_statics; its
973-
// prepared statics are the ones the helpers read.
974-
let interp = interp::compile(p, statics)?;
1045+
// prepared statics (and extern impls) are the ones the helpers read.
1046+
let interp = interp::compile_ext(p, statics, externs)?;
9751047

9761048
let mut flags = settings::builder();
9771049
flags.set("use_colocated_libcalls", "false").unwrap();
@@ -1006,14 +1078,24 @@ pub fn compile(p: &Program, statics: Vec<super::StaticData>) -> Result<Cranelift
10061078

10071079
let mut const_strs: Vec<Box<str>> = Vec::new();
10081080
let mut probe_descs: Vec<Box<ProbeDesc>> = Vec::new();
1081+
let mut extern_descs: Vec<Box<ExternDesc>> = Vec::new();
10091082
let mut trap_msgs: Vec<String> = Vec::new();
10101083

10111084
{
10121085
let mut fbc = FunctionBuilderContext::new();
10131086
let mut b = FunctionBuilder::new(&mut ctx.func, &mut fbc);
10141087

10151088
// Scratch stack slots: one 16-byte out-cell for helper out-params,
1016-
// plus room for the widest probe's keys and values.
1089+
// plus room for the widest probe's keys and values — and the widest
1090+
// ecall's arg cells (2 per param) and out cells (1 + 2 per return),
1091+
// which share the same scratch slots.
1092+
let ext_args = p.externs.iter().map(|e| 2 * e.params.len()).max().unwrap_or(0);
1093+
let ext_outs = p
1094+
.externs
1095+
.iter()
1096+
.map(|e| 1 + 2 * e.rets.len())
1097+
.max()
1098+
.unwrap_or(0);
10171099
let max_keys = p
10181100
.statics
10191101
.iter()
@@ -1023,6 +1105,7 @@ pub fn compile(p: &Program, statics: Vec<super::StaticData>) -> Result<Cranelift
10231105
})
10241106
.max()
10251107
.unwrap_or(1)
1108+
.max(ext_args)
10261109
.max(1);
10271110
let max_vals = p
10281111
.statics
@@ -1033,6 +1116,7 @@ pub fn compile(p: &Program, statics: Vec<super::StaticData>) -> Result<Cranelift
10331116
})
10341117
.max()
10351118
.unwrap_or(1)
1119+
.max(ext_outs)
10361120
.max(1);
10371121
let slot_out =
10381122
b.create_sized_stack_slot(StackSlotData::new(StackSlotKind::ExplicitSlot, 16, 3));
@@ -1104,6 +1188,7 @@ pub fn compile(p: &Program, statics: Vec<super::StaticData>) -> Result<Cranelift
11041188
slot_vals,
11051189
&mut const_strs,
11061190
&mut probe_descs,
1191+
&mut extern_descs,
11071192
trap_exit,
11081193
);
11091194
}
@@ -1200,6 +1285,7 @@ pub fn compile(p: &Program, statics: Vec<super::StaticData>) -> Result<Cranelift
12001285
regexes,
12011286
_const_strs: const_strs,
12021287
_probe_descs: probe_descs,
1288+
_extern_descs: extern_descs,
12031289
})
12041290
}
12051291

@@ -1220,6 +1306,7 @@ impl CraneliftFn {
12201306
interp::reserve_out(&mut st.out, input.rows);
12211307

12221308
let statics = self.interp.statics();
1309+
let externs = self.interp.externs();
12231310
let mut emitted = 0usize;
12241311
for row in 0..input.rows {
12251312
let mut cx = Cx {
@@ -1233,6 +1320,8 @@ impl CraneliftFn {
12331320
statics_len: statics.len(),
12341321
regexes: self.regexes.as_ptr(),
12351322
regexes_len: self.regexes.len(),
1323+
externs: externs.as_ptr(),
1324+
externs_len: externs.len(),
12361325
trap: None,
12371326
};
12381327
match (self.row_fn)(&mut cx) {
@@ -1263,6 +1352,7 @@ fn translate_inst(
12631352
slot_vals: cranelift_codegen::ir::StackSlot,
12641353
const_strs: &mut Vec<Box<str>>,
12651354
probe_descs: &mut Vec<Box<ProbeDesc>>,
1355+
extern_descs: &mut Vec<Box<ExternDesc>>,
12661356
trap_exit: cranelift_codegen::ir::Block,
12671357
) {
12681358
// After any fallible helper: check trap_flag (offset 0 of Cx) and bail.
@@ -1278,6 +1368,74 @@ fn translate_inst(
12781368
Inst::ProbeRange { .. } | Inst::ProbeRead { .. } => {
12791369
unreachable!("multiplicity programs are rejected before codegen")
12801370
}
1371+
Inst::ExternCall { ext, dsts, args } => {
1372+
let spec = &p.externs[*ext as usize];
1373+
// Write the arg cells: (flag, payload) per param, one cell each.
1374+
// Payloads under a false flag are never read by the helper.
1375+
for (i, a) in args.iter().enumerate() {
1376+
let base = (16 * i) as i32;
1377+
match vals[&a.0] {
1378+
V::S(v) => {
1379+
let is_flag = i % 2 == 0;
1380+
let as64 = if is_flag {
1381+
b.ins().uextend(types::I64, v)
1382+
} else {
1383+
match spec.params[i / 2] {
1384+
Ty::I1 => b.ins().uextend(types::I64, v),
1385+
Ty::I64 => v,
1386+
Ty::F64 => b.ins().bitcast(types::I64, MemFlags::new(), v),
1387+
Ty::Str => unreachable!("str payload is a two-i64 V::Str"),
1388+
}
1389+
};
1390+
b.ins().stack_store(as64, slot_keys, base);
1391+
}
1392+
V::Str(o, l) => {
1393+
b.ins().stack_store(o, slot_keys, base);
1394+
b.ins().stack_store(l, slot_keys, base + 8);
1395+
}
1396+
}
1397+
}
1398+
let desc = Box::new(ExternDesc {
1399+
ext_id: *ext as usize,
1400+
param_tys: spec.params.clone(),
1401+
ret_tys: spec.rets.clone(),
1402+
});
1403+
let desc_ptr = &*desc as *const ExternDesc as i64;
1404+
extern_descs.push(desc);
1405+
let dp = icon(b, desc_ptr);
1406+
let ap = b.ins().stack_addr(types::I64, slot_keys, 0);
1407+
let op = b.ins().stack_addr(types::I64, slot_vals, 0);
1408+
call_h(b, module, "h_extern", &[cxp, dp, ap, op]);
1409+
trap_check(b);
1410+
// Read the out cells: whole-call validity, then (flag, payload)
1411+
// per return.
1412+
for (i, d) in dsts.iter().enumerate() {
1413+
let base = (16 * i) as i32;
1414+
let is_flag = i == 0 || i % 2 == 1;
1415+
let v = if is_flag {
1416+
let x = b.ins().stack_load(types::I64, slot_vals, base);
1417+
V::S(b.ins().ireduce(types::I8, x))
1418+
} else {
1419+
match spec.rets[i / 2 - 1] {
1420+
Ty::I1 => {
1421+
let x = b.ins().stack_load(types::I64, slot_vals, base);
1422+
V::S(b.ins().ireduce(types::I8, x))
1423+
}
1424+
Ty::I64 => V::S(b.ins().stack_load(types::I64, slot_vals, base)),
1425+
Ty::F64 => {
1426+
let x = b.ins().stack_load(types::I64, slot_vals, base);
1427+
V::S(b.ins().bitcast(types::F64, MemFlags::new(), x))
1428+
}
1429+
Ty::Str => {
1430+
let o = b.ins().stack_load(types::I64, slot_vals, base);
1431+
let l = b.ins().stack_load(types::I64, slot_vals, base + 8);
1432+
V::Str(o, l)
1433+
}
1434+
}
1435+
};
1436+
vals.insert(d.0, v);
1437+
}
1438+
}
12811439
Inst::Const { dst, lit } => {
12821440
let v = match lit {
12831441
Lit::I1(x) => V::S(b.ins().iconst(types::I8, *x as i64)),
@@ -1968,6 +2126,7 @@ const HELPERS: &[(&str, *const u8)] = &[
19682126
("h_store_str", h_store_str as *const u8),
19692127
("h_sload", h_sload as *const u8),
19702128
("h_probe", h_probe as *const u8),
2129+
("h_extern", h_extern as *const u8),
19712130
("h_ln", h_ln as *const u8),
19722131
("h_log2", h_log2 as *const u8),
19732132
("h_log10", h_log10 as *const u8),
@@ -2059,6 +2218,7 @@ fn helper_sig(name: &str, sig: &mut cranelift_codegen::ir::Signature, ptr: types
20592218
"h_store_str" => (&[ptr, I64, I8, I64, I64], None),
20602219
"h_sload" => (&[ptr, I64, I64, I64], None),
20612220
"h_probe" => (&[ptr, I64, I64, I64], Some(I8)),
2221+
"h_extern" => (&[ptr, I64, I64, I64], None),
20622222
_ => unreachable!("unknown helper {name}"),
20632223
};
20642224
for p in params {

0 commit comments

Comments
 (0)