Strange behavior when using try/catch to return a value from a Class method

James and all,

Just another idea: make the “don’t use nonlocal return inside handlers” rule visible while you type.

A linter could indicate the error with a wavy underline under the ^ the moment you write it inside a guarded block.

How it would work:

  • open a .sc file
  • Type: {}.try { ^42 }

You should see a colorful wavy underline under the ^ (Flymake warning: ^ inside try block)

The parser can’t reliably forbid ^ there, but we can lint the typical case: a block immediately following one of those selectors that contains a nonlocal return.

;;; sclang-return-guard-simple.el --- Proof-of-concept/simple linter -*- lexical-binding: t; -*-

;; usage:
;; (require 'sclang-return-guard-simple)
(require 'flymake)

(defgroup sclang-return-guard nil
  "Lint for nonlocal returns."
  :group 'languages)

(defcustom sclang-return-guard-selectors '("try" "protect" "use")
  "Selectors to check."
  :type '(repeat string)
  :group 'sclang-return-guard)

(defun sclang-return-guard-check-buffer ()
  "Check buffer for problematic returns."
  (let ((diagnostics '()))
    (save-excursion
      (goto-char (point-min))
      (while (re-search-forward "\\(try\\|protect\\|use\\)[[:space:]]*{" nil t)
        (let ((method-name (match-string 1))
              (block-start (1- (point))))  
          (goto-char block-start)
          (when (eq (char-after) ?{)
            (condition-case nil
                (progn
                  (forward-sexp)  ; Jump to matching }
                  (let ((block-end (point)))
                    (goto-char (1+ block-start))
                    (while (search-forward "^" block-end t)
                      (let ((caret-pos (1- (point))))
                        (unless (save-excursion
                                  (goto-char caret-pos)
                                  (or (nth 3 (syntax-ppss))  ; in string
                                      (nth 4 (syntax-ppss)))) ; in comment
                          (push (flymake-make-diagnostic
                                 (current-buffer)
                                 caret-pos
                                 (1+ caret-pos)
                                 :warning
                                 (format "^ inside %s block" method-name))
                                diagnostics))))))
              (error nil))))))
    diagnostics))

(defun sclang-return-guard-backend (report-fn &rest _args)
  "Flymake backend."
  (funcall report-fn (sclang-return-guard-check-buffer)))


;;;###autoload
(define-minor-mode sclang-return-guard-mode
  "Simple return guard."
  :lighter " ^guard"
  (if sclang-return-guard-mode
      (progn
        (add-hook 'flymake-diagnostic-functions
                  #'sclang-return-guard-backend nil t)
        (when (fboundp 'flymake-mode)
          (flymake-mode 1)))
    (remove-hook 'flymake-diagnostic-functions
                 #'sclang-return-guard-backend t)))

;;;###autoload
(defun sclang-return-guard-auto-enable ()
  "Auto-enable for SuperCollider files."
  (when (and (stringp buffer-file-name)
             (string-match "\\.\\(scd\\|sc\\)\\'" buffer-file-name))
    (sclang-return-guard-mode 1)))

(add-hook 'find-file-hook #'sclang-return-guard-auto-enable)

(provide 'sclang-return-guard-simple)
;;; sclang-return-guard-simple.el ends here

Future

  • “Code action” (“convert to return-at-method-level”): turn

    try { ^compute() } { |e| ^fallback }
    

    into

    var ret;
    try { ret = compute() } { |e| ret = fallback };
    ^ret
    

NOTE: A tree‑sitter would do the same, but safely and precisely. For our return‑inside‑handler issue, it turns a weak text search into a precise AST rule—and that’s the difference between “works in my demo” and “always works while you type.”