;;; Guile Knots ;;; Copyright © 2026 Christopher Baines ;;; ;;; This file is part of Guile Knots. ;;; ;;; The Guile Knots is free software; you can redistribute it and/or ;;; modify it under the terms of the GNU General Public License as ;;; published by the Free Software Foundation; either version 3 of the ;;; License, or (at your option) any later version. ;;; ;;; The Guile Knots is distributed in the hope that it will be useful, ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;;; General Public License for more details. ;;; ;;; You should have received a copy of the GNU General Public License ;;; along with the guix-data-service. If not, see ;;; . (define-module (knots backtraces) #:use-module (srfi srfi-1) #:use-module (srfi srfi-43) #:use-module (ice-9 match) #:use-module (ice-9 format) #:use-module (system repl debug) #:use-module (system vm frame) #:use-module ((knots) #:select (display/knots simple-format/knots format/knots)) #:export (&knots-exception make-knots-exception knots-exception? knots-exception-stack print-backtrace-and-exception/knots)) (define &knots-exception (make-exception-type '&knots-exception &exception '(stack))) (define make-knots-exception (record-constructor &knots-exception)) (set-procedure-property! make-knots-exception 'documentation "Construct a @code{&knots-exception} with the given stack.") (define knots-exception? (exception-predicate &knots-exception)) (set-procedure-property! knots-exception? 'documentation "Return @code{#t} if OBJ is a @code{&knots-exception}.") (define knots-exception-stack (exception-accessor &knots-exception (record-accessor &knots-exception 'stack))) (set-procedure-property! knots-exception-stack 'documentation "Return the stack from a @code{&knots-exception}.") (define (backtrace-debug-mode?) (let ((val (getenv "KNOTS_BACKTRACE_DEBUG"))) (and val (not (string=? val "")) (not (string=? val "0"))))) (define (debug-print-stack port label full-vec included-vec) (simple-format/knots port "[KNOTS DEBUG] ~A\n" label) (if (vector-empty? full-vec) (simple-format/knots port " (empty)\n") (vector-fold-right (lambda (i _ frame) (let ((marker (if (vector-index (lambda (f) (eq? f frame)) included-vec) ">" " ")) (name (symbol->string (or (frame-procedure-name frame) '_)))) (match (frame-source frame) (#f (format/knots port " ~a ~3d unknown ~a~%" marker i name)) ((_ file line . col) (format/knots port " ~a ~3d ~a:~a:~a ~a~%" marker i file (1+ line) col name))))) #f full-vec)) (force-output port)) (define (internal-file? file) (or (string-prefix? "ice-9/" file) (string-prefix? "system/" file) (string-prefix? "srfi/" file) (string=? file "knots.scm") (string-prefix? "knots/" file) (string=? file "fibers.scm") (string-prefix? "fibers/" file))) (define (frame-file frame) (let ((src (frame-source frame))) (and src (cadr src)))) (define (user-frame? frame) (let ((file (frame-file frame))) (and (string? file) (not (internal-file? file))))) (define (raise-machinery-frame? frame) ;; Return #t for frames that are part of the raise/unwind machinery ;; and should be skipped when looking for the raise site. ;; Specifically: C/unknown frames (no source file) and ;; ice-9/boot-9.scm frames. Other internal frames such as ;; ice-9/vlist.scm are part of the actual call path and should be ;; preserved. (let ((file (frame-file frame))) (or (not file) (string=? file "ice-9/boot-9.scm")))) (define (fibers-frame? frame) ;; Return #t if FRAME belongs to the fibers library. (let ((file (frame-file frame))) (and (string? file) (or (string=? file "fibers.scm") (string-prefix? "fibers/" file))))) ;; The number of frames in Guile's eval-machinery tail appended to every ;; top-level script stack: ;; ;; [n-6] ice-9/boot-9.scm _ ;; [n-5] ice-9/boot-9.scm save-module-excursion ;; [n-4] ice-9/eval.scm _ ;; [n-3] ice-9/boot-9.scm call-with-prompt ;; [n-2] C/unknown apply-smob/0 ;; [n-1] ice-9/boot-9.scm with-exception-handler (define script-eval-tail-length 6) (define (classify-stack-situation stack-vector) (cond ((vector-any fibers-frame? stack-vector) 'run-fibers) ((let ((len (vector-length stack-vector))) (and (>= len script-eval-tail-length) (equal? (frame-file (vector-ref stack-vector (- len 1))) "ice-9/boot-9.scm") (eq? (frame-procedure-name (vector-ref stack-vector (- len 3))) 'call-with-prompt) (not (vector-any (lambda (frame) (eq? (frame-procedure-name frame) '%start-stack)) stack-vector)))) 'script) (else 'unknown))) (define (filter-knots-stack-vector vector) ;; Extract user frames from a pre-captured knots stack. The bottom 3 frames ;; are always fixed overhead: make-stack (C), the handler body frame at the ;; make-stack call site (exactly 1 Scheme frame), and raise-exception ;; (boot-9). User frames start at index 3. (let ((last-user (vector-index-right user-frame? vector))) (if (or (not last-user) (< last-user 3)) #() (vector-copy vector 3 (+ last-user 1))))) (define (filter-stack-vector vector) ;; Return the slice of VECTOR containing the frames relevant for ;; display. Skips the fixed 2-frame overhead (make-stack + call ;; site) and any raise machinery to find after-raise, then bounds at ;; the eval-machinery tail (script) or the first fibers scheduler ;; frame (run-fibers/unknown). (define (skip-handler-and-raise vector start) ;; Scan forward from START in VECTOR, first past any user frames ;; (the handler body), then past raise-machinery frames (C/unknown ;; and ice-9/boot-9.scm). Returns the index of the first ;; remaining frame — the raise site or context. Other internal ;; frames such as ice-9/vlist.scm are preserved because they are ;; part of the actual call path. (let* ((len (vector-length vector)) (after-handler (let loop ((i start)) (if (or (>= i len) (not (user-frame? (vector-ref vector i)))) i (loop (+ i 1)))))) (let loop ((i after-handler)) (cond ((>= i len) i) ((raise-machinery-frame? (vector-ref vector i)) (loop (+ i 1))) (else i))))) (let* ((len (vector-length vector)) (situation (classify-stack-situation vector)) (after-raise (skip-handler-and-raise vector (min 2 len))) (end (if (and (eq? situation 'script) (> (- len script-eval-tail-length) after-raise)) (- len script-eval-tail-length) (let loop ((i after-raise)) (cond ((>= i len) i) ((fibers-frame? (vector-ref vector i)) i) (else (loop (+ i 1)))))))) (if (>= after-raise end) #() (vector-copy vector after-raise end)))) ;; Based on print-frame from (system repl debug), but without the ;; frame indexes (define* (print-frame/no-index frame #:optional (port (current-output-port)) #:key (width (terminal-width)) (last-source #f) (innermost? #f)) (define (source-file src) (match src (#f "unknown file") ((_ #f . _) "current input") ((_ file . _) file))) (let* ((source (frame-source frame)) (file (source-file source))) (when (not (equal? file (source-file last-source))) (format port "~&In ~a:~&" file)) (format port "~9@a ~v:@y~%" (match source (#f "") ((_ _ line . col) (simple-format #f "~A:~A" (1+ line) col))) width (frame-call-representation frame #:top-frame? innermost?)))) (define* (print-backtrace-and-exception/knots exn #:key (port (current-error-port))) "Print the backtrace and exception information from EXN to PORT. This procedure captures the stack, so should be run before the stack is unwound, so using @code{with-exception-handler} without @code{#:unwind? #t}, the exception may need to then be re-raised and handled in an outer exception handler. @example (with-exception-handler (lambda (exn) ;; Recover from the exception #f) (lambda () (with-exception-handler (lambda (exn) (print-backtrace-and-exception/knots exn) (raise-exception exn)) (lambda () (do-things)))) #:unwind? #t) @end example It's important to use @code{print-backtrace-and-exception/knots} for displaying backtraces involving functionality from Guile Knots, since the stack involved is potentially split across several fibers. The stacks involved are attached to the exception, and this procedure extracts this information out and assembles a backtrace including all the code involved. " (define (get-string out stack) (let* ((stack-vector (stack->vector stack)) (knots-stack-vectors (map (lambda (exn) (stack->vector (knots-exception-stack exn))) (reverse (filter knots-exception? (simple-exceptions exn))))) (filtered-stack-vector (filter-stack-vector stack-vector)) (filtered-knots-stack-vectors (map filter-knots-stack-vector knots-stack-vectors))) (when (backtrace-debug-mode?) (let ((debug-port (current-error-port)) (situation (classify-stack-situation stack-vector))) (simple-format/knots debug-port "[KNOTS DEBUG] situation: ~A\n" situation) (debug-print-stack debug-port "stack" stack-vector filtered-stack-vector) (let ((stack-count (length knots-stack-vectors))) (for-each (lambda (knots-vec user-vec index) (debug-print-stack debug-port (format #f "knots stack ~a/~a" index stack-count) knots-vec user-vec)) knots-stack-vectors filtered-knots-stack-vectors (iota stack-count 1))) (display/knots "\n" debug-port) (force-output debug-port))) (for-each (lambda (vec) (vector-fold-right (lambda (i last-source frame) (print-frame/no-index frame out #:innermost? (= i 0) #:last-source last-source) (frame-source frame)) #f vec)) (cons filtered-stack-vector filtered-knots-stack-vectors)) (print-exception out #f '%exception (list (if (backtrace-debug-mode?) exn (apply make-exception (remove knots-exception? (simple-exceptions exn)))))))) (let* ((stack (match (fluid-ref %stacks) ((stack-tag . prompt-tag) (make-stack #t 0 prompt-tag 0 (and prompt-tag 1))) (_ (make-stack #t)))) (string-port (open-output-string)) (output (with-exception-handler (lambda (output-exn) (display/knots (get-output-string string-port) port) (close-output-port string-port) (display/knots "\n\n" port) (let* ((stack (make-stack #t)) (backtrace (call-with-output-string (lambda (port) (display-backtrace stack port) (newline port))))) (display/knots backtrace port)) (simple-format/knots port "\nexception in print-backtrace-and-exception/knots: ~A\n" output-exn) (raise-exception output-exn)) (lambda () (get-string string-port stack) (let ((str (get-output-string string-port))) (close-output-port string-port) str))))) (display/knots output port)))