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