Re: Nested 'break'
Olivier Dion <[email protected]> Fri, 19 Jun 2026 10:57:17 -0400
| Newsgroups | gmane.lisp.guile.user |
|---|---|
| Message-ID | <87cxxmo9wy.fsf@laura> |
On Fri, 19 Jun 2026, Reuben Thomas via General Guile related discussions <[email protected]> wrote: > After a little more thought I have a much more minimal failing example: > > (while #t > (while #t (break))) Congratulation. You have found a compiler bug! I will debug this "live" with you. I indeed get the same error as you, but if I force evaluation: scheme@(guile-user)> ,option interp #t then it works as expected. Furthermore, you can see the bug in action: scheme@(guile-user)> ,option interp #f scheme@(guile-user)> ,expand (while #t (while #t (break))) (let ((break-tag ((@@ (guile) make-prompt-tag) "break")) (continue-tag ((@@ (guile) make-prompt-tag) "continue"))) ((@@ (guile) call-with-prompt) break-tag (lambda () (let lp () ((@@ (guile) call-with-prompt) continue-tag (lambda () (let loop () (if ((@@ (guile) not) #t) (begin (if #f #f) #f) (begin (let ((break-tag ((@@ (guile) make-prompt-tag) "break")) (continue-tag ((@@ (guile) make-prompt-tag) "continue"))) (pk 'before-prompt break-tag) ;<= adding debug output ((@@ (guile) call-with-prompt) break-tag (lambda () (pk 'inside-prompt break-tag) ;<= adding debug output (let lp () ((@@ (guile) call-with-prompt) continue-tag (lambda () (let loop () (if ((@@ (guile) not) #t) (begin (if #f #f) #f) (begin ((@@ (guile) abort-to-prompt) break-tag) (loop))))) (lambda (k) (lp))))) (lambda (k . args) (if ((@@ (guile) null?) args) #t ((@@ (guile) apply) (@@ (guile) values) args))))) (loop))))) (lambda (k) (lp))))) (lambda (k . args) (if ((@@ (guile) null?) args) #t ((@@ (guile) apply) (@@ (guile) values) args))))) and if we evaluate this: ;;; (before-prompt ("break")) ;;; (inside-prompt ("break")) ;;; (before-prompt ("break")) ;;; (inside-prompt #<procedure values _>) ice-9/boot-9.scm:1705:22: In procedure raise-exception: In procedure abort: Abort to unknown prompt Oh well, something is overwriting the prompt tag once we are inside it the second time. This looks to me like a register liveness bug. Furtheremore, we can actually reproduce the issue with a single while: (let lp ((x #t)) (lp (while #t (break)))) And indeed, I got the same error. Time to look at the assembly: scheme@(guile-user)> ,x (lambda () (let lp () (while #t (break)) (lp))) 0 (instrument-entry 261) 2 (assert-nargs-ee/locals 1 11) ;; 12 slots (0 args) 3 (make-non-immediate 11 218) ;; "break" 5 (static-ref 10 224) ;; #f 7 (immediate-tag=? 10 7 0) ;; heap-object? 9 (je 7) ;; -> L1 10 (call-scm<-scmn-scmn 10 231 235 113);; lookup-bound-private 14 (static-set! 10 215) ;; #f L1: 16 (scm-ref/immediate 10 10 1) 17 (mov 6 10) 18 (mov 5 11) 19 (handle-interrupts) 20 (call 5 2) 22 (receive 2 5 12) 24 (make-non-immediate 8 229) ;; "continue" 26 (mov 4 10) 27 (mov 3 8) 28 (handle-interrupts) 29 (call 7 2) 31 (receive-values 7 #t 1) 33 (reset-frame 12) ;; 12 slots 34 (prompt 9 #t 4 4) ;; H -> H2 37 (j 56) ;; -> L8 H2: 38 (receive-values 4 #t 1) 40 (bind-rest 5) ;; 6 slots 41 (reset-frame 12) ;; 12 slots 42 (j 1) ;; -> L3 L3: 43 (immediate-tag=? 6 3583 260) ;; null? 45 (je 8) ;; -> L4 46 (builtin-ref 3 1) ;; values 47 (builtin-ref 4 0) ;; apply 48 (mov 2 6) 49 (handle-interrupts) 50 (call 7 3) 52 (reset-frame 12) ;; 12 slots L4: 53 (builtin-ref 9 1) ;; values <= Actually overwriting the break tag 54 (builtin-ref 7 0) ;; apply L5: 55 (instrument-loop 206) 57 (handle-interrupts) 58 (mov 3 10) 59 (mov 2 11) 60 (handle-interrupts) 61 (call 8 2) 63 (receive 5 8 12) 65 (mov 2 10) 66 (mov 1 8) 67 (handle-interrupts) 68 (call 9 2) 70 (receive-values 9 #t 1) 72 (reset-frame 12) ;; 12 slots 73 (prompt 6 #t 5 4) ;; H -> H6 76 (j 17) ;; -> L8 H6: 77 (receive-values 5 #t 1) 79 (bind-rest 6) ;; 7 slots 80 (reset-frame 12) ;; 12 slots 81 (j 1) ;; -> L7 L7: 82 (immediate-tag=? 5 3583 260) ;; null? 84 (je -29) ;; -> L5 85 (mov 3 7) 86 (mov 2 9) 87 (mov 1 5) 88 (handle-interrupts) 89 (call 8 3) 91 (reset-frame 12) ;; 12 slots 92 (j -37) ;; -> L5 L8: 93 (mov 6 9) ;;<= Read break tag for abort 94 (builtin-ref 5 2) ;; abort-to-prompt L9: 95 (instrument-loop 166) 97 (handle-interrupts) 98 (mov 1 5) 99 (mov 0 6) 100 (handle-interrupts) 101 (call 10 2) 103 (reset-frame 12) ;; 12 slots 104 (j -9) ;; -> L9 So it seems that the compiler got confused and overwrite the tag with the procedure `values', inside the handler. Later, when we try to read the tag back to call `abort-to-prompt' we read that value instead of the original tag and we try to abort to it. What's very interesting is that the installation of the prompt itself it not failing. It seems to be using a different stack slot than the abort-to-prompt, so we can actually setup a prompt with the correct tag, but the abort is using the wrong one. Finally, here's a trimmed down reproducer: (let lp () (let ((break-tag (make-prompt-tag 'break))) (call-with-prompt break-tag (lambda () (let loop () (abort-to-prompt break-tag) (loop))) (lambda (k . args) (const 1)))) (lp)) If you could open a bug report on Codeberg, I will track this issue down further later. Thanks, Olivier -- Olivier Dion