Skip to content

Commit 4650ae5

Browse files
perf: errors build only when raised, recursion keeps its caches, and calls skip the name scan
1 parent 74ec329 commit 4650ae5

22 files changed

Lines changed: 5340 additions & 5262 deletions

File tree

‎bench/.snapshot‎

Lines changed: 5192 additions & 5187 deletions
Large diffs are not rendered by default.

‎docs/04-implementation/01-design.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ Each `Instruction` is 4 bytes. That is a 1-byte `OpCode` (`#[repr(u8)]`), a 2-by
3030

3131
## Dispatch shape
3232

33-
The hot loop reads `cache.fused_ref()[ip]`, a snapshot of the instruction stream with `LoadAttr` + `Call` sites fused into `CallMethod` + `CallMethodArgs`. Fusion happens after first dispatch when every argument is a single-push load, up to 8 arguments, never across a jump target. The arg loads shift down one slot so the pair sits adjacent at the call. Fusion runs once per chunk and is cached.
33+
The hot loop reads `cache.fused_ref()[ip]`, a snapshot of the instruction stream with `LoadAttr` + `Call` sites fused into `CallMethod` + `CallMethodArgs`. Fusion happens after first dispatch when every argument is a single-push load, up to 8 arguments, never across a jump target. The arg loads shift down one slot so the pair sits adjacent at the call. Fusion runs once per cache, and a recursion holds one cache per depth.
3434

3535
A build with the `coverage` feature marks every instruction the loop dispatches, and `vm.ran(chunk, ip)` says whether `chunk.instructions[ip]` ran, still after a run that raised. Fusion stays off in that build, so each ip is the one the compiler wrote, and a chunk answers under the same pointer the caller built, an imported module under its entry in `chunk.imports`.
3636

37-
For arith and compare opcodes the loop checks `cache.get_fast(ip)`. A present `FastOp` runs inline without a function call.
37+
For arith and compare opcodes `exec_arith_or_compare` checks `cache.get_fast(ip)` first. A present `FastOp` runs inside that call and skips the generic handler.
3838

3939
`LoadConst` reads a pre-materialised `Vec<Val>` built on first dispatch. Inline-range ints stay inline. Ints from 2⁴⁷ to 2¹²⁷ allocate a `HeapObj::LongInt` slot. Literals beyond ±2¹²⁷ are rejected at parse time.
4040

‎src/parser/types.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ impl SSAChunk {
230230
i
231231
}
232232

233+
/* The slot of `n` in this chunk, None when the chunk never named it. */
234+
pub(crate) fn slot_of(&self, n: &str) -> Option<u16> { self.name_index.get(n).copied() }
235+
233236
/* Builds `prev_slots`, coalesces SSA versions to canonical root, rewrites operands, builds `phi_map`. */
234237
pub fn finalize_prev_slots(&mut self) {
235238
let n = self.names.len();

‎src/vm/cache.rs‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,37 @@ struct CacheSlot {
4040
inst: Option<InstanceCache>,
4141
}
4242

43+
/* One chunk's caches, each running frame holds one and returns it here. */
44+
#[derive(Default)]
45+
pub struct CachePool {
46+
active: u32,
47+
main: Option<OpcodeCache>,
48+
/* Only a recursion fills it, so a plain call never allocates here. */
49+
spare: Vec<OpcodeCache>,
50+
}
51+
52+
impl CachePool {
53+
pub fn take(&mut self, chunk: &SSAChunk) -> OpcodeCache {
54+
self.active += 1;
55+
self.main.take().or_else(|| self.spare.pop()).unwrap_or_else(|| OpcodeCache::new(chunk))
56+
}
57+
58+
/* The outermost frame returning keeps one cache and frees the rest. */
59+
pub fn put(&mut self, cache: OpcodeCache) {
60+
self.active -= 1;
61+
if self.active == 0 {
62+
self.spare.clear();
63+
self.main = Some(cache);
64+
} else if self.main.is_none() {
65+
self.main = Some(cache);
66+
} else {
67+
self.spare.push(cache);
68+
}
69+
}
70+
71+
pub fn caches(&self) -> impl Iterator<Item = &OpcodeCache> { self.main.iter().chain(&self.spare) }
72+
}
73+
4374
pub struct OpcodeCache {
4475
slots: Vec<CacheSlot>,
4576
fused: Option<Vec<Instruction>>,

‎src/vm/dispatch.rs‎

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,11 @@ impl<'a> VM<'a> {
155155
let delta_base = self.pending.delta_save.len();
156156
let key = chunk as *const _;
157157

158-
let mut cache = self.opcode_caches.remove(&key).unwrap_or_else(|| OpcodeCache::new(chunk));
158+
let mut cache = self.opcode_caches.entry(key).or_default().take(chunk);
159159
cache.ensure_fused(chunk);
160160
// Pre-materialise the constant pool here (not in OpcodeCache::new) because Str allocates into the live HeapPool.
161161
if let Err(e) = cache.ensure_const_vals(chunk, &mut self.heap) {
162-
self.opcode_caches.insert(key, cache);
162+
self.park_cache(key, cache);
163163
return Err(e);
164164
}
165165

@@ -290,10 +290,15 @@ impl<'a> VM<'a> {
290290
self.active_const_pools.pop();
291291
self.active_slots.pop();
292292
self.frame_safe = outer_safe;
293-
self.opcode_caches.insert(key, cache);
293+
self.park_cache(key, cache);
294294
result
295295
}
296296

297+
/* Hands a frame's cache back to its chunk's pool. */
298+
fn park_cache(&mut self, key: *const SSAChunk, cache: OpcodeCache) {
299+
if let Some(pool) = self.opcode_caches.get_mut(&key) { pool.put(cache); }
300+
}
301+
297302
pub(crate) fn exec_from(&mut self, chunk: &SSAChunk, slots: &mut [Val], start_ip: usize) -> Result<Val, VmErr> {
298303
self.resume_ip = start_ip;
299304
self.exec(chunk, slots)
@@ -386,12 +391,12 @@ impl<'a> VM<'a> {
386391
match ins.opcode {
387392
// Short-circuit jumps, instance `__bool__` / `__len__` may run via `truthy_op`.
388393
OpCode::JumpIfFalseOrPop => {
389-
let v = *self.stack.last().ok_or(cold_runtime("stack underflow"))?;
394+
let v = *self.stack.last().ok_or_else(|| cold_runtime("stack underflow"))?;
390395
if !self.truthy_op(v, chunk, slots)? { *ip = op as usize; }
391396
else { self.pop()?; }
392397
}
393398
OpCode::JumpIfTrueOrPop => {
394-
let v = *self.stack.last().ok_or(cold_runtime("stack underflow"))?;
399+
let v = *self.stack.last().ok_or_else(|| cold_runtime("stack underflow"))?;
395400
if self.truthy_op(v, chunk, slots)? { *ip = op as usize; }
396401
else { self.pop()?; }
397402
}
@@ -450,23 +455,26 @@ impl<'a> VM<'a> {
450455
}
451456
}
452457
OpCode::LoadGlobal => {
453-
let name = chunk.names.get(op as usize).ok_or(cold_runtime("LoadGlobal: name index out of bounds"))?;
458+
let name = chunk.names.get(op as usize).ok_or_else(|| cold_runtime("LoadGlobal: name index out of bounds"))?;
454459
let v = self.module_state.get(name.as_str()).copied().or_else(|| self.global(name)).filter(|v| !v.is_undef());
455460
self.push(v.ok_or_else(|| VmErr::Name(name.clone()))?);
456461
}
457462
OpCode::StoreGlobal => {
458463
let v = self.pop()?;
459-
let name = chunk.names.get(op as usize).ok_or(cold_runtime("StoreGlobal: name index out of bounds"))?;
464+
let name = chunk.names.get(op as usize).ok_or_else(|| cold_runtime("StoreGlobal: name index out of bounds"))?;
460465
// A `global` store rebinds a name some cached result may have read.
461466
self.templates.clear();
462467
self.note_builtin_binding(name);
463468
self.globals_written = true;
464-
self.module_state.insert(name.clone(), v);
469+
match self.module_state.get_mut(name.as_str()) {
470+
Some(s) => *s = v,
471+
None => { self.module_state.insert(name.clone(), v); }
472+
}
465473
}
466474
OpCode::LoadConst => {
467475
// Constants are pre-materialised at exec entry, so this is a single bounds-checked index instead of a Value->Val conversion.
468476
let v = *consts.get(op as usize)
469-
.ok_or(cold_runtime("constant index out of bounds"))?;
477+
.ok_or_else(|| cold_runtime("constant index out of bounds"))?;
470478
self.push(v);
471479
}
472480

@@ -593,8 +601,8 @@ impl<'a> VM<'a> {
593601
OpCode::StoreAttr => self.exec_store_attr(op, chunk, slots)?,
594602

595603
OpCode::LoadModule => {
596-
let entry = chunk.imports.get(op as usize).ok_or(cold_runtime("LoadModule: import index out of range"))?;
597-
let v = *self.module_table.get(&entry.spec).ok_or(cold_runtime("LoadModule: module not initialised"))?;
604+
let entry = chunk.imports.get(op as usize).ok_or_else(|| cold_runtime("LoadModule: import index out of range"))?;
605+
let v = *self.module_table.get(&entry.spec).ok_or_else(|| cold_runtime("LoadModule: module not initialised"))?;
598606
self.push(v);
599607
}
600608

@@ -611,12 +619,12 @@ impl<'a> VM<'a> {
611619
OpCode::MatMul => self.handle_matmul(operand, chunk, slots)?,
612620
OpCode::MakeTypeAlias => {
613621
let value = self.pop()?;
614-
let name = chunk.names.get(operand as usize).ok_or(cold_runtime("MakeTypeAlias: bad name index"))?.clone();
622+
let name = chunk.names.get(operand as usize).ok_or_else(|| cold_runtime("MakeTypeAlias: bad name index"))?.clone();
615623
let alias = self.heap.alloc(HeapObj::TypeAlias(name, value))?;
616624
self.push(alias);
617625
}
618626
OpCode::MakeTypeVar => {
619-
let name = chunk.names.get(operand as usize).ok_or(cold_runtime("MakeTypeVar: bad name index"))?.clone();
627+
let name = chunk.names.get(operand as usize).ok_or_else(|| cold_runtime("MakeTypeVar: bad name index"))?.clone();
620628
let v = self.heap.alloc(HeapObj::TypeVar(name))?;
621629
self.push(v);
622630
}
@@ -641,7 +649,7 @@ impl<'a> VM<'a> {
641649
self.push(v);
642650
}
643651
OpCode::Dup => {
644-
let v = *self.stack.last().ok_or(cold_runtime("stack underflow"))?;
652+
let v = *self.stack.last().ok_or_else(|| cold_runtime("stack underflow"))?;
645653
self.push(v);
646654
}
647655
OpCode::MatchSeq => {
@@ -697,7 +705,7 @@ impl<'a> VM<'a> {
697705
OpCode::BeginFinally => self.unwind_stack.push(Unwind::Normal),
698706
// Stages `__exit__` for the plain Call behind it, parking-safe.
699707
OpCode::WithExit => {
700-
let cm = self.with_stack.pop().ok_or(cold_runtime("WithExit without matching WithEnter"))?;
708+
let cm = self.with_stack.pop().ok_or_else(|| cold_runtime("WithExit without matching WithEnter"))?;
701709
let (exit_fn, class) = self.with_dunder(cm, "__exit__")?;
702710
// Reraise selects `__exit__(type, exc, None)`, other exits pass three Nones.
703711
let (exc_type, exc) = if matches!(self.unwind_stack.last(), Some(Unwind::Reraise(..))) {
@@ -1009,7 +1017,7 @@ impl<'a> VM<'a> {
10091017
fn exec_store_attr(&mut self, op: u16, chunk: &SSAChunk, slots: &mut [Val]) -> Result<(), VmErr> {
10101018
let value = self.pop()?;
10111019
let obj = self.pop()?;
1012-
let name = chunk.names.get(op as usize).ok_or(cold_runtime("StoreAttr: bad name index"))?;
1020+
let name = chunk.names.get(op as usize).ok_or_else(|| cold_runtime("StoreAttr: bad name index"))?;
10131021
self.store_attr(obj, name, value, chunk, slots)
10141022
}
10151023

‎src/vm/gc.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ impl<'a> VM<'a> {
5252
let heap = &mut self.heap; // split borrow, lets closures take &mut heap while iterating other fields
5353
for frame in &self.iter_stack { frame.for_each_val(&mut |v| heap.mark(v)); }
5454
for sf in &self.pending_sync_frames { sf.for_each_val(&mut |v| heap.mark(v)); }
55-
for cache in self.opcode_caches.values() {
55+
for cache in self.opcode_caches.values().flat_map(|p| p.caches()) {
5656
if let Some(consts) = cache.const_vals_opt() {
5757
for &v in consts { self.heap.mark(v); }
5858
}

‎src/vm/globals/async_ops.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ impl<'a> VM<'a> {
470470
/* Suspend until `s` real seconds elapse. */
471471
pub fn call_sleep(&mut self) -> Result<(), VmErr> {
472472
let n = self.pop()?;
473-
let secs = num_as_f64(n, &self.heap).ok_or(cold_type("sleep() requires a number"))?.max(0.0);
473+
let secs = num_as_f64(n, &self.heap).ok_or_else(|| cold_type("sleep() requires a number"))?.max(0.0);
474474
let until = self.now_ns().saturating_add((secs * 1_000_000_000.0) as u64);
475475
self.pending.sleep_until_ns = Some(until);
476476
// Push None as the yield value, the scheduler ignores it.
@@ -500,7 +500,7 @@ impl<'a> VM<'a> {
500500
if !matches!(self.heap.try_get(coro), Some(HeapObj::Coroutine(..))) {
501501
return Err(cold_type("with_timeout() requires a coroutine"));
502502
}
503-
let secs = num_as_f64(secs_v, &self.heap).ok_or(cold_type("with_timeout() seconds must be a number"))?;
503+
let secs = num_as_f64(secs_v, &self.heap).ok_or_else(|| cold_type("with_timeout() seconds must be a number"))?;
504504
let deadline_ns = self.now_ns().saturating_add((secs.max(0.0) * 1_000_000_000.0) as u64);
505505
self.schedule(&[coro]);
506506
self.park_on(vec![coro], WaitKind::Timeout { deadline_ns, target: coro });

‎src/vm/globals/attr.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ impl<'a> VM<'a> {
5656
/* `del obj.attr` opcode, pop the object, remove the named attribute in place. */
5757
pub fn exec_del_attr(&mut self, op: u16, chunk: &crate::parser::SSAChunk) -> Result<(), VmErr> {
5858
let obj = self.pop()?;
59-
let name = chunk.names.get(op as usize).ok_or(cold_runtime("DelAttr: bad name index"))?.clone();
59+
let name = chunk.names.get(op as usize).ok_or_else(|| cold_runtime("DelAttr: bad name index"))?.clone();
6060
self.delete_attr_named(obj, &name)
6161
}
6262

‎src/vm/globals/numeric.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ impl<'a> VM<'a> {
215215
pub fn call_chr(&mut self) -> Result<(), VmErr> {
216216
let o = self.pop()?;
217217
if !o.is_int() { return Err(cold_type("chr() requires an integer")); }
218-
let c = char::from_u32(o.as_int() as u32).ok_or(cold_value("chr() arg out of range"))?;
218+
let c = char::from_u32(o.as_int() as u32).ok_or_else(|| cold_value("chr() arg out of range"))?;
219219
let mut s = String::with_capacity(4);
220220
s.push(c);
221221
self.alloc_and_push_str(s)
@@ -262,7 +262,7 @@ impl<'a> VM<'a> {
262262
}
263263
// Ints/bools round to themselves, negative ndigits round to tens/hundreds.
264264
(Some(o), n) if o.is_bool() || o.is_int() || (o.is_heap() && matches!(self.heap.get(o), HeapObj::LongInt(_))) => {
265-
let i = if o.is_bool() { o.as_bool() as i128 } else { self.as_i128(o).ok_or(cold_type("round() requires a number"))? };
265+
let i = if o.is_bool() { o.as_bool() as i128 } else { self.as_i128(o).ok_or_else(|| cold_type("round() requires a number"))? };
266266
let nd = match n { Some(n) if n.is_int() => n.as_int(), _ => 0 };
267267
let r = if nd < 0 { round_int_banker(i, (-nd) as u32) } else { i };
268268
self.int_to_val(Some(r))?
@@ -396,7 +396,7 @@ impl<'a> VM<'a> {
396396
// Modular exponentiation (a ** b) % c on i128.
397397
let (Some(base), Some(modulus)) = (self.as_i128(args[0]), self.as_i128(args[2]))
398398
else { return Err(cold_type("pow() with 3 args requires integers")); };
399-
let exp = self.as_i128(args[1]).ok_or(cold_type("pow() with 3 args requires integer exponent"))?;
399+
let exp = self.as_i128(args[1]).ok_or_else(|| cold_type("pow() with 3 args requires integer exponent"))?;
400400
if exp < 0 { return Err(cold_value("pow() exponent must be non-negative")); }
401401
if modulus == 0 { return Err(VmErr::ZeroDiv); }
402402

@@ -438,11 +438,11 @@ impl<'a> VM<'a> {
438438
let mut e = exp;
439439
while e > 0 {
440440
if e & 1 == 1 {
441-
result = result.checked_mul(base).ok_or(cold_overflow())?;
441+
result = result.checked_mul(base).ok_or_else(cold_overflow)?;
442442
}
443443
e >>= 1;
444444
if e > 0 {
445-
base = base.checked_mul(base).ok_or(cold_overflow())?;
445+
base = base.checked_mul(base).ok_or_else(cold_overflow)?;
446446
}
447447
}
448448
return self.int_to_val(Some(result));

‎src/vm/helpers.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,13 @@ impl<'a> VM<'a> {
8080
#[inline] pub(crate) fn push(&mut self, v: Val) { self.stack.push(v); }
8181

8282
#[inline] pub(crate) fn pop(&mut self) -> Result<Val, VmErr> {
83-
self.stack.pop().ok_or(cold_runtime("stack underflow"))
83+
self.stack.pop().ok_or_else(|| cold_runtime("stack underflow"))
8484
}
8585
#[inline] pub(crate) fn pop2(&mut self) -> Result<(Val, Val), VmErr> {
8686
let b = self.pop()?; let a = self.pop()?; Ok((a, b))
8787
}
8888
#[inline] pub(crate) fn pop_n(&mut self, n: usize) -> Result<Vec<Val>, VmErr> {
89-
let at = self.stack.len().checked_sub(n).ok_or(cold_runtime("stack underflow"))?;
89+
let at = self.stack.len().checked_sub(n).ok_or_else(|| cold_runtime("stack underflow"))?;
9090
Ok(self.stack.split_off(at))
9191
}
9292

0 commit comments

Comments
 (0)