Skip to content

Commit a811005

Browse files
ahrzbclaude
authored andcommitted
feat(specializer): allocation-free steady state on both backends — AC #4 (TASK-45 stretch 4)
Counting-allocator tests now pin zero heap allocation per warm run for the interpreter AND cranelift, over the probe/arith fixture and a string-heavy program (case, trim, substr, concat, itos/ftos, parse, compare). What had to change: substr and trim become pure sub-span arithmetic (byte ranges into the input span — no copy at all), case mapping streams char-at-a-time into the arena (Arena::case_map), number->text formats straight into the arena (Arena::push_fmt) with DuckF64 rendering to a stack buffer, string compare/parse read the arena immutably, and h_probe emits values without the per-call Vec + ScalarVal clones. All shared between backends; 500-seed differential and the full 612-test pytest suite stay green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 4b410ff commit a811005

4 files changed

Lines changed: 318 additions & 118 deletions

File tree

src/specializer/exec/cranelift.rs

Lines changed: 80 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ use super::super::ir::{
3636
BinOp, CmpPred, Inst, Lit, NumOp1, Program, RoundMode, StaticTy, StrOp1, Term, TrimSide, Ty,
3737
};
3838
use super::interp::{
39-
self, apply_ord, col_valid, substr_range_ok, substr_window, CompileError, DuckF64, InterpFn,
40-
PreparedStatic,
39+
self, apply_ord, col_valid, substr_range_ok, substr_window, trim_bounds, CompileError, DuckF64,
40+
InterpFn, PreparedStatic,
4141
};
4242
use super::{casemap, Arena, Batch, ColData, KeyBits, OutCol, RunState, ScalarVal, StrRef, Trap};
4343

@@ -72,16 +72,6 @@ impl Cx {
7272
fn statics(&self) -> &[PreparedStatic] {
7373
unsafe { std::slice::from_raw_parts(self.statics, self.statics_len) }
7474
}
75-
fn get(&mut self, off: i64, len: i64) -> String {
76-
// Owned copy: several helpers read one span and push another; a
77-
// borrow across arena mutation is not expressible here.
78-
self.arena()
79-
.get(StrRef {
80-
off: off as usize,
81-
len: len as usize,
82-
})
83-
.to_string()
84-
}
8575
fn set_trap(&mut self, msg: String) {
8676
self.trap_flag = 1;
8777
self.trap = Some(Trap(msg));
@@ -218,8 +208,18 @@ extern "C" fn h_fcmp(a: f64, b: f64, pred: i64) -> u8 {
218208

219209
extern "C" fn h_scmp(p: *mut Cx, ao: i64, al: i64, bo: i64, bl: i64, pred: i64) -> u8 {
220210
let c = unsafe { cx(p) };
221-
let (a, b) = (c.get(ao, al), c.get(bo, bl));
222-
apply_ord(decode_pred(pred), a.as_str().cmp(b.as_str())) as u8
211+
// Comparison only reads — both spans borrow the arena immutably.
212+
let arena = unsafe { &*c.arena };
213+
let a = arena.get(span(ao, al));
214+
let b = arena.get(span(bo, bl));
215+
apply_ord(decode_pred(pred), a.cmp(b)) as u8
216+
}
217+
218+
fn span(off: i64, len: i64) -> StrRef {
219+
StrRef {
220+
off: off as usize,
221+
len: len as usize,
222+
}
223223
}
224224

225225
fn decode_pred(p: i64) -> CmpPred {
@@ -257,21 +257,21 @@ extern "C" fn h_ftoi(p: *mut Cx, x: f64, round: i64) -> i64 {
257257

258258
extern "C" fn h_itos(p: *mut Cx, v: i64, len_out: *mut i64) -> i64 {
259259
let c = unsafe { cx(p) };
260-
let r = c.arena().push_str(&format!("{v}"));
260+
let r = c.arena().push_fmt(format_args!("{v}"));
261261
unsafe { *len_out = r.len as i64 };
262262
r.off as i64
263263
}
264264

265265
extern "C" fn h_ftos(p: *mut Cx, v: f64, len_out: *mut i64) -> i64 {
266266
let c = unsafe { cx(p) };
267-
let r = c.arena().push_str(&format!("{}", DuckF64(v)));
267+
let r = c.arena().push_fmt(format_args!("{}", DuckF64(v)));
268268
unsafe { *len_out = r.len as i64 };
269269
r.off as i64
270270
}
271271

272272
extern "C" fn h_stoi(p: *mut Cx, off: i64, len: i64, valid_out: *mut u8) -> i64 {
273273
let c = unsafe { cx(p) };
274-
let s = c.get(off, len);
274+
let s = unsafe { &*c.arena }.get(span(off, len));
275275
match s.trim_ascii().parse::<i64>() {
276276
Ok(v) => {
277277
unsafe { *valid_out = 1 };
@@ -286,7 +286,7 @@ extern "C" fn h_stoi(p: *mut Cx, off: i64, len: i64, valid_out: *mut u8) -> i64
286286

287287
extern "C" fn h_stof(p: *mut Cx, off: i64, len: i64, valid_out: *mut u8) -> f64 {
288288
let c = unsafe { cx(p) };
289-
let s = c.get(off, len);
289+
let s = unsafe { &*c.arena }.get(span(off, len));
290290
match s.trim_ascii().parse::<f64>() {
291291
Ok(v) => {
292292
unsafe { *valid_out = 1 };
@@ -317,13 +317,12 @@ extern "C" fn h_sconcat(p: *mut Cx, ao: i64, al: i64, bo: i64, bl: i64, len_out:
317317

318318
extern "C" fn h_scase(p: *mut Cx, off: i64, len: i64, upper: i64, len_out: *mut i64) -> i64 {
319319
let c = unsafe { cx(p) };
320-
let s = c.get(off, len);
321-
let out: String = if upper != 0 {
322-
s.chars().map(casemap::simple_upper).collect()
320+
let map: fn(char) -> char = if upper != 0 {
321+
casemap::simple_upper
323322
} else {
324-
s.chars().map(casemap::simple_lower).collect()
323+
casemap::simple_lower
325324
};
326-
let r = c.arena().push_str(&out);
325+
let r = c.arena().case_map(span(off, len), map);
327326
unsafe { *len_out = r.len as i64 };
328327
r.off as i64
329328
}
@@ -338,18 +337,16 @@ extern "C" fn h_strim(
338337
len_out: *mut i64,
339338
) -> i64 {
340339
let c = unsafe { cx(p) };
341-
let (s, set_s) = (c.get(ao, al), c.get(co, cl));
342-
let set: Vec<char> = set_s.chars().collect();
343-
let hit = |ch: char| set.contains(&ch);
344-
let t = match side {
345-
0 => s.trim_matches(hit),
346-
1 => s.trim_start_matches(hit),
347-
_ => s.trim_end_matches(hit),
348-
}
349-
.to_string();
350-
let r = c.arena().push_str(&t);
351-
unsafe { *len_out = r.len as i64 };
352-
r.off as i64
340+
let arena = unsafe { &*c.arena };
341+
let side = match side {
342+
0 => TrimSide::Both,
343+
1 => TrimSide::Lead,
344+
_ => TrimSide::Trail,
345+
};
346+
let rng = trim_bounds(arena.get(span(ao, al)), arena.get(span(co, cl)), side);
347+
// The trimmed value is a subview of the input span — no copy.
348+
unsafe { *len_out = (rng.end - rng.start) as i64 };
349+
(ao as usize + rng.start) as i64
353350
}
354351

355352
extern "C" fn h_ssubstr(
@@ -372,11 +369,15 @@ extern "C" fn h_ssubstr(
372369
unsafe { *len_out = 0 };
373370
return 0;
374371
}
375-
let s = c.get(ao, al);
376-
let out = substr_window(&s, start, (has_len != 0).then_some(len));
377-
let r = c.arena().push_str(&out);
378-
unsafe { *len_out = r.len as i64 };
379-
r.off as i64
372+
let arena = unsafe { &*c.arena };
373+
let rng = substr_window(
374+
arena.get(span(ao, al)),
375+
start,
376+
(has_len != 0).then_some(len),
377+
);
378+
// The window is a subview of the input span — no copy.
379+
unsafe { *len_out = (rng.end - rng.start) as i64 };
380+
(ao as usize + rng.start) as i64
380381
}
381382

382383
macro_rules! store_h {
@@ -449,25 +450,26 @@ extern "C" fn h_probe(
449450
let c = unsafe { cx(p) };
450451
let desc = unsafe { &*desc };
451452
let keys = unsafe { std::slice::from_raw_parts(keys, desc.key_tys.len()) };
452-
let PreparedStatic::Map { entries } = &c.statics()[desc.static_id] else {
453+
// SAFETY: statics and arena are disjoint allocations behind separate
454+
// raw pointers; independent references let str values append to the
455+
// arena while the matched entry stays borrowed — no clones, no
456+
// per-call Vec (the steady state is allocation-free).
457+
let statics = unsafe { std::slice::from_raw_parts(c.statics, c.statics_len) };
458+
let arena = unsafe { &mut *c.arena };
459+
let PreparedStatic::Map { entries } = &statics[desc.static_id] else {
453460
unreachable!("static kind checked at compile");
454461
};
455462

456463
// Mirrors interp::cmp_key: canonical f64 bits, arena strings by byte
457-
// order. The arena is only read here, never written, so the borrow is
458-
// fine to hold across the search.
459-
let arena = unsafe { &*c.arena };
464+
// order. The search only reads the arena.
460465
let cmp = |stored: &[KeyBits]| -> std::cmp::Ordering {
461466
for (kb, cell) in stored.iter().zip(keys.iter()) {
462467
let ord = match kb {
463468
KeyBits::I1(s) => s.cmp(&(cell[0] != 0)),
464469
KeyBits::I64(s) => s.cmp(&(cell[0] as i64)),
465470
KeyBits::F64(s) => s.cmp(&super::canon_f64_bits(f64::from_bits(cell[0]))),
466471
KeyBits::Str(s) => {
467-
let v = arena.get(StrRef {
468-
off: cell[0] as usize,
469-
len: cell[1] as usize,
470-
});
472+
let v = arena.get(span(cell[0] as i64, cell[1] as i64));
471473
s.as_str().cmp(v)
472474
}
473475
};
@@ -477,35 +479,39 @@ extern "C" fn h_probe(
477479
}
478480
std::cmp::Ordering::Equal
479481
};
480-
481482
let found = entries.binary_search_by(|(k, _)| cmp(k)).ok();
482-
let hit = found.is_some();
483-
let values: Vec<ScalarVal> = match found {
484-
Some(idx) => entries[idx].1.clone(),
485-
None => desc
486-
.val_tys
487-
.iter()
488-
.map(|ty| match ty {
489-
Ty::I1 => ScalarVal::I1(false),
490-
Ty::I64 => ScalarVal::I64(0),
491-
Ty::F64 => ScalarVal::F64(0.0),
492-
Ty::Str => ScalarVal::Str(String::new()),
493-
})
494-
.collect(),
495-
};
496-
for (i, v) in values.into_iter().enumerate() {
497-
let cell = match v {
498-
ScalarVal::I1(b) => [b as u64, 0],
499-
ScalarVal::I64(x) => [x as u64, 0],
500-
ScalarVal::F64(f) => [f.to_bits(), 0],
501-
ScalarVal::Str(s) => {
502-
let r = c.arena().push_str(&s);
503-
[r.off as u64, r.len as u64]
483+
484+
match found {
485+
Some(idx) => {
486+
for (i, v) in entries[idx].1.iter().enumerate() {
487+
let cell: Cell = match v {
488+
ScalarVal::I1(b) => [*b as u64, 0],
489+
ScalarVal::I64(x) => [*x as u64, 0],
490+
ScalarVal::F64(f) => [f.to_bits(), 0],
491+
ScalarVal::Str(s) => {
492+
let r = arena.push_str(s);
493+
[r.off as u64, r.len as u64]
494+
}
495+
};
496+
unsafe { *outs.add(i) = cell };
504497
}
505-
};
506-
unsafe { *outs.add(i) = cell };
498+
1
499+
}
500+
None => {
501+
for (i, ty) in desc.val_tys.iter().enumerate() {
502+
let cell: Cell = match ty {
503+
Ty::I1 | Ty::I64 => [0, 0],
504+
Ty::F64 => [0f64.to_bits(), 0],
505+
Ty::Str => {
506+
let r = arena.push_str("");
507+
[r.off as u64, r.len as u64]
508+
}
509+
};
510+
unsafe { *outs.add(i) = cell };
511+
}
512+
0
513+
}
507514
}
508-
hit as u8
509515
}
510516

511517
// ------------------------------------------------------------ the JIT'd fn --

0 commit comments

Comments
 (0)