Skip to content

Commit f040685

Browse files
committed
py: Only store the exception instance on Py stack in bytecode try block.
When an exception is raised and is to be handled by the VM, it is stored on the Python value stack so the bytecode can access it. CPython stores 3 objects on the stack for each exception: exc type, exc instance and traceback. uPy followed this approach, but it turns out not to be necessary. Instead, it is enough to store just the exception instance on the Python value stack. The only place where the 3 values are needed explicitly is for the __exit__ handler of a with-statement context, but for these cases the 3 values can be extracted from the single exception instance. This patch removes the need to store 3 values on the stack, and instead just stores the exception instance. Code size is reduced by about 50-100 bytes, the compiler and VM are slightly simpler, generate bytecode is smaller (by 2 bytes for each try block), and the Python value stack is reduced in size for functions that handle exceptions.
1 parent 67d52d8 commit f040685

3 files changed

Lines changed: 30 additions & 44 deletions

File tree

py/compile.c

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,8 @@ STATIC void compile_try_except(compiler_t *comp, mp_parse_node_t pn_body, int n_
14951495
EMIT_ARG(label_assign, l1); // start of exception handler
14961496
EMIT(start_except_handler);
14971497

1498+
// at this point the top of the stack contains the exception instance that was raised
1499+
14981500
uint l2 = comp_next_label(comp);
14991501

15001502
for (int i = 0; i < n_except; i++) {
@@ -1528,16 +1530,13 @@ STATIC void compile_try_except(compiler_t *comp, mp_parse_node_t pn_body, int n_
15281530
EMIT_ARG(pop_jump_if, false, end_finally_label);
15291531
}
15301532

1531-
EMIT(pop_top);
1532-
1533+
// either discard or store the exception instance
15331534
if (qstr_exception_local == 0) {
15341535
EMIT(pop_top);
15351536
} else {
15361537
compile_store_id(comp, qstr_exception_local);
15371538
}
15381539

1539-
EMIT(pop_top);
1540-
15411540
uint l3 = 0;
15421541
if (qstr_exception_local != 0) {
15431542
l3 = comp_next_label(comp);
@@ -1561,7 +1560,7 @@ STATIC void compile_try_except(compiler_t *comp, mp_parse_node_t pn_body, int n_
15611560
}
15621561
EMIT_ARG(jump, l2);
15631562
EMIT_ARG(label_assign, end_finally_label);
1564-
EMIT_ARG(adjust_stack_size, 3); // stack adjust for the 3 exception items
1563+
EMIT_ARG(adjust_stack_size, 1); // stack adjust for the exception instance
15651564
}
15661565

15671566
compile_decrease_except_level(comp);

py/emitbc.c

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -751,19 +751,19 @@ void mp_emit_bc_unwind_jump(emit_t *emit, mp_uint_t label, mp_uint_t except_dept
751751
}
752752

753753
void mp_emit_bc_setup_with(emit_t *emit, mp_uint_t label) {
754-
// TODO We can probably optimise the amount of needed stack space, since
755-
// we don't actually need 4 slots during the entire with block, only in
756-
// the cleanup handler in certain cases. It needs some thinking.
757-
emit_bc_pre(emit, 4);
754+
// The SETUP_WITH opcode pops ctx_mgr from the top of the stack
755+
// and then pushes 3 entries: __exit__, ctx_mgr, as_value.
756+
emit_bc_pre(emit, 2);
758757
emit_write_bytecode_byte_unsigned_label(emit, MP_BC_SETUP_WITH, label);
759758
}
760759

761760
void mp_emit_bc_with_cleanup(emit_t *emit, mp_uint_t label) {
762761
mp_emit_bc_pop_block(emit);
763762
mp_emit_bc_load_const_tok(emit, MP_TOKEN_KW_NONE);
764763
mp_emit_bc_label_assign(emit, label);
765-
emit_bc_pre(emit, -4);
764+
emit_bc_pre(emit, 2); // ensure we have enough stack space to call the __exit__ method
766765
emit_write_bytecode_byte(emit, MP_BC_WITH_CLEANUP);
766+
emit_bc_pre(emit, -4); // cancel the 2 above, plus the 2 from mp_emit_bc_setup_with
767767
}
768768

769769
void mp_emit_bc_setup_except(emit_t *emit, mp_uint_t label) {
@@ -955,11 +955,11 @@ void mp_emit_bc_yield_from(emit_t *emit) {
955955
}
956956

957957
void mp_emit_bc_start_except_handler(emit_t *emit) {
958-
mp_emit_bc_adjust_stack_size(emit, 6); // stack adjust for the 3 exception items, +3 for possible UNWIND_JUMP state
958+
mp_emit_bc_adjust_stack_size(emit, 4); // stack adjust for the exception instance, +3 for possible UNWIND_JUMP state
959959
}
960960

961961
void mp_emit_bc_end_except_handler(emit_t *emit) {
962-
mp_emit_bc_adjust_stack_size(emit, -5); // stack adjust
962+
mp_emit_bc_adjust_stack_size(emit, -3); // stack adjust
963963
}
964964

965965
#if MICROPY_EMIT_NATIVE

py/vm.c

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,8 @@ run_code_state: ;
587587
// and __exit__ method (with self) underneath it. Bytecode calls __exit__,
588588
// and "deletes" it off stack, shifting "exception control block"
589589
// to its place.
590+
// The bytecode emitter ensures that there is enough space on the Python
591+
// value stack to hold the __exit__ method plus an additional 4 entries.
590592
if (TOP() == mp_const_none) {
591593
// stack: (..., __exit__, ctx_mgr, None)
592594
sp[1] = mp_const_none;
@@ -620,31 +622,26 @@ run_code_state: ;
620622
}
621623
sp -= 2; // we removed (__exit__, ctx_mgr)
622624
} else {
623-
assert(mp_obj_is_exception_type(TOP()));
624-
// stack: (..., __exit__, ctx_mgr, traceback, exc_val, exc_type)
625-
// Need to pass (sp[0], sp[-1], sp[-2]) as arguments so must reverse the
626-
// order of these on the value stack (don't want to create a temporary
627-
// array because it increases stack footprint of the VM).
628-
mp_obj_t obj = sp[-2];
629-
sp[-2] = sp[0];
630-
sp[0] = obj;
631-
mp_obj_t ret_value = mp_call_method_n_kw(3, 0, sp - 4);
625+
assert(mp_obj_is_exception_instance(TOP()));
626+
// stack: (..., __exit__, ctx_mgr, exc_instance)
627+
// Need to pass (exc_type, exc_instance, None) as arguments to __exit__.
628+
sp[1] = sp[0];
629+
sp[0] = mp_obj_get_type(sp[0]);
630+
sp[2] = mp_const_none;
631+
sp -= 2;
632+
mp_obj_t ret_value = mp_call_method_n_kw(3, 0, sp);
632633
if (mp_obj_is_true(ret_value)) {
633634
// We need to silence/swallow the exception. This is done
634635
// by popping the exception and the __exit__ handler and
635636
// replacing it with None, which signals END_FINALLY to just
636637
// execute the finally handler normally.
637-
sp -= 4;
638638
SET_TOP(mp_const_none);
639639
assert(exc_sp >= exc_stack);
640640
POP_EXC_BLOCK();
641641
} else {
642642
// We need to re-raise the exception. We pop __exit__ handler
643-
// and copy the 3 exception values down (remembering that they
644-
// are reversed due to above code).
645-
sp[-4] = sp[0];
646-
sp[-3] = sp[-1];
647-
sp -= 2;
643+
// by copying the exception instance down to the new top-of-stack.
644+
sp[0] = sp[3];
648645
}
649646
}
650647
DISPATCH();
@@ -698,18 +695,12 @@ unwind_jump:;
698695

699696
ENTRY(MP_BC_END_FINALLY):
700697
MARK_EXC_IP_SELECTIVE();
701-
// not fully implemented
702-
// if TOS is an exception, reraises the exception (3 values on TOS)
703698
// if TOS is None, just pops it and continues
704-
// if TOS is an integer, does something else
705-
// else error
706-
if (mp_obj_is_exception_type(TOP())) {
707-
RAISE(sp[-1]);
708-
}
699+
// if TOS is an integer, finishes coroutine and returns control to caller
700+
// if TOS is an exception, reraises the exception
709701
if (TOP() == mp_const_none) {
710702
sp--;
711-
} else {
712-
assert(MP_OBJ_IS_SMALL_INT(TOP()));
703+
} else if (MP_OBJ_IS_SMALL_INT(TOP())) {
713704
// We finished "finally" coroutine and now dispatch back
714705
// to our caller, based on TOS value
715706
mp_unwind_reason_t reason = MP_OBJ_SMALL_INT_VALUE(POP());
@@ -719,6 +710,9 @@ unwind_jump:;
719710
assert(reason == UNWIND_JUMP);
720711
goto unwind_jump;
721712
}
713+
} else {
714+
assert(mp_obj_is_exception_instance(TOP()));
715+
RAISE(TOP());
722716
}
723717
DISPATCH();
724718

@@ -751,14 +745,9 @@ unwind_jump:;
751745

752746
// matched against: SETUP_EXCEPT
753747
ENTRY(MP_BC_POP_EXCEPT):
754-
// TODO need to work out how blocks work etc
755-
// pops block, checks it's an exception block, and restores the stack, saving the 3 exception values to local threadstate
756748
assert(exc_sp >= exc_stack);
757749
assert(currently_in_except_block);
758-
//sp = (mp_obj_t*)(*exc_sp--);
759-
//exc_sp--; // discard ip
760750
POP_EXC_BLOCK();
761-
//sp -= 3; // pop 3 exception values
762751
DISPATCH();
763752

764753
ENTRY(MP_BC_BUILD_TUPLE): {
@@ -1359,10 +1348,8 @@ unwind_jump:;
13591348
mp_obj_t *sp = MP_TAGPTR_PTR(exc_sp->val_sp);
13601349
// save this exception in the stack so it can be used in a reraise, if needed
13611350
exc_sp->prev_exc = nlr.ret_val;
1362-
// push(traceback, exc-val, exc-type)
1363-
PUSH(mp_const_none);
1351+
// push exception object so it can be handled by bytecode
13641352
PUSH(MP_OBJ_FROM_PTR(nlr.ret_val));
1365-
PUSH(MP_OBJ_FROM_PTR(((mp_obj_base_t*)nlr.ret_val)->type));
13661353
code_state->sp = sp;
13671354

13681355
#if MICROPY_STACKLESS

0 commit comments

Comments
 (0)