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

Reply via email to