greghendershott / racket-mode

Emacs major and minor modes for Racket: edit, REPL, check-syntax, debug, profile, and more.

Home Page:https://www.racket-mode.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Images do not display in the REPL with the HTDP languages

a-wagner opened this issue · comments

It appears that inline images do not work with the HTDP teaching languages. For example,

#lang htdp/bsl

(require 2htdp/image)

(circle 5 "solid" "red")

produces #<image> in the REPL (both with racket-run and racket-run-module-at-point). Using #lang racket instead, the image displays correctly. This seems related to #430.

Thanks to @camoy for helping me isolate the issue.

In case it's relevant, I'm on macOS Ventura 13.5.2 using Emacs 29 built with imagemagick support and configured using Doom. But Cameron was able to reproduce the issue on his Linux setup, so I don't think it's a platform issue.

((alist-get 'racket-mode package-alist))
((emacs-version "29.1")
 (system-type darwin)
 (x-gtk-use-system-tooltips UNDEFINED)
 (major-mode help-mode)
 (racket--el-source-dir "/Users/ahwagner/.emacs.d/.local/straight/build-29.1/racket-mode/")
 (racket--rkt-source-dir "/Users/ahwagner/.emacs.d/.local/straight/build-29.1/racket-mode/racket/")
 (racket-program "racket")
 (racket-command-timeout 10)
 (racket-path-from-emacs-to-racket-function UNDEFINED)
 (racket-path-from-racket-to-emacs-function UNDEFINED)
 (racket-browse-url-function racket-browse-url-using-temporary-file)
 (racket-documentation-search-location "https://docs.racket-lang.org/search/index.html?q=%s")
 (racket-xp-after-change-refresh-delay 1)
 (racket-xp-mode-lighter
  (:eval
   (racket--xp-mode-lighter)))
 (racket-xp-highlight-unused-regexp "^[^_]")
 (racket-repl-buffer-name-function nil)
 (racket-submodules-to-run
  ((test)
   (main)))
 (racket-memory-limit 2048)
 (racket-error-context medium)
 (racket-repl-history-directory "~/.emacs.d/.local/cache/racket-mode/")
 (racket-history-filter-regexp "\\`\\s *\\'")
 (racket-images-inline t)
 (racket-imagemagick-props nil)
 (racket-images-keep-last 100)
 (racket-images-system-viewer "display")
 (racket-pretty-print t)
 (racket-use-repl-submit-predicate nil)
 (racket-pretty-print t)
 (racket-indent-curly-as-sequence t)
 (racket-indent-sequence-depth 0)
 (racket-pretty-lambda nil)
 (racket-smart-open-bracket-enable nil)
 (racket-module-forms "\\s(\\(?:module[*+]?\\|library\\)")
 (racket-logger-config
  ((cm-accomplice . warning)
   (GC . info)
   (module-prefetch . warning)
   (optimizer . info)
   (racket/contract . error)
   (racket-mode-debugger . info)
   (sequence-specialization . info)
   (* . fatal)))
 (racket-show-functions
  (racket-show-pseudo-tooltip)))
(enabled-minor-modes
 (+popup-mode)
 (TeX-PDF-mode)
 (TeX-source-correlate-mode)
 (anzu-mode)
 (auto-composition-mode)
 (auto-compression-mode)
 (auto-encryption-mode)
 (auto-fill-mode)
 (auto-save-mode)
 (better-jumper-local-mode)
 (better-jumper-mode)
 (buffer-read-only)
 (column-number-mode)
 (delete-selection-mode)
 (doom-modeline-mode)
 (electric-indent-mode)
 (eros-mode)
 (evil-escape-mode)
 (evil-goggles-mode)
 (evil-local-mode)
 (evil-mode)
 (evil-snipe-local-mode)
 (evil-snipe-mode)
 (evil-snipe-override-local-mode)
 (evil-snipe-override-mode)
 (evil-surround-mode)
 (evil-traces-mode)
 (file-name-shadow-mode)
 (font-lock-mode)
 (format-all-mode)
 (gcmh-mode)
 (general-override-mode)
 (global-anzu-mode)
 (global-company-mode)
 (global-eldoc-mode)
 (global-evil-surround-mode)
 (global-flycheck-mode)
 (global-font-lock-mode)
 (global-git-commit-mode)
 (global-hl-line-mode)
 (global-so-long-mode)
 (hl-line-mode)
 (isearch-fold-quotes-mode)
 (line-number-mode)
 (marginalia-mode)
 (mouse-wheel-mode)
 (ns-auto-titlebar-mode)
 (override-global-mode)
 (pdf-occur-global-minor-mode)
 (persp-mode)
 (projectile-mode)
 (recentf-mode)
 (save-place-mode)
 (savehist-mode)
 (semantic-minor-modes-format)
 (shell-dirtrack-mode)
 (show-paren-mode)
 (size-indication-mode)
 (smartparens-global-mode)
 (solaire-global-mode)
 (solaire-mode)
 (transient-mark-mode)
 (treemacs-filewatch-mode)
 (treemacs-follow-mode)
 (treemacs-fringe-indicator-mode)
 (treemacs-git-mode)
 (undo-fu-mode)
 (undo-fu-session-global-mode)
 (vertico-mode)
 (which-key-mode)
 (windmove-mode)
 (window-divider-mode)
 (winner-mode)
 (ws-butler-global-mode)
 (yas-global-mode)
 (yas-minor-mode))
(disabled-minor-modes
 (+emacs-lisp-ert-mode)
 (+emacs-lisp-non-package-mode)
 (+javascript-gulp-mode)
 (+javascript-npm-mode)
 (+lsp-optimization-mode)
 (+org-pretty-mode)
 (+popup-buffer-mode)
 (+web-angularjs-mode)
 (+web-django-mode)
 (+web-jekyll-mode)
 (+web-phaser-mode)
 (+web-react-mode)
 (+web-wordpress-mode)
 (LaTeX-math-mode)
 (TeX-Omega-mode)
 (TeX-interactive-mode)
 (abbrev-mode)
 (adaptive-wrap-prefix-mode)
 (auto-fill-function)
 (auto-revert-mode)
 (auto-revert-tail-mode)
 (auto-save-visited-mode)
 (blink-cursor-mode)
 (buffer-face-mode)
 (button-mode)
 (cl-old-struct-compat-mode)
 (comint-fontify-input-mode)
 (company-mode)
 (company-search-mode)
 (compilation-minor-mode)
 (compilation-shell-minor-mode)
 (completion-in-region-mode)
 (consult-preview-at-point-mode)
 (context-menu-mode)
 (cursor-face-highlight-mode)
 (cursor-intangible-mode)
 (cursor-sensor-mode)
 (dap-auto-configure-mode)
 (dap-mode)
 (dash-fontify-mode)
 (defining-kbd-macro)
 (diff-auto-refine-mode)
 (diff-minor-mode)
 (dired-hide-details-mode)
 (display-line-numbers-mode)
 (dtrt-indent-global-mode)
 (dtrt-indent-mode)
 (edit-indirect--overlay)
 (eldoc-mode)
 (electric-layout-mode)
 (electric-quote-mode)
 (evil-collection-magit-toggle-text-minor-mode)
 (evil-tex-mode)
 (flycheck-mode)
 (flycheck-popup-tip-mode)
 (flymake-mode)
 (general-override-local-mode)
 (git-commit-mode)
 (git-gutter-mode)
 (global-auto-revert-mode)
 (global-dash-fontify-mode)
 (global-display-line-numbers-mode)
 (global-git-gutter-mode)
 (global-hide-mode-line-mode)
 (global-hl-todo-mode)
 (global-prettify-symbols-mode)
 (global-reveal-mode)
 (global-semantic-highlight-edits-mode)
 (global-semantic-highlight-func-mode)
 (global-semantic-show-parser-state-mode)
 (global-semantic-show-unmatched-syntax-mode)
 (global-semantic-stickyfunc-mode)
 (global-vi-tilde-fringe-mode)
 (global-visual-line-mode)
 (global-whitespace-mode)
 (global-whitespace-newline-mode)
 (header-line-indent-mode)
 (hide-mode-line-mode)
 (highlight-numbers-mode)
 (highlight-quoted-mode)
 (hl-todo-mode)
 (horizontal-scroll-bar-mode)
 (hs-minor-mode)
 (html-autoview-mode)
 (ibuffer-auto-mode)
 (image-minor-mode)
 (indent-tabs-mode)
 (isearch-mode)
 (ispell-minor-mode)
 (jit-lock-debug-mode)
 (latex-electric-env-pair-mode)
 (lock-file-mode)
 (lost-selection-mode)
 (lsp-completion-mode)
 (lsp-inlay-hints-mode)
 (lsp-installation-buffer-mode)
 (lsp-kotlin-lens-mode)
 (lsp-managed-mode)
 (lsp-mode)
 (lsp-semantic-tokens-mode)
 (lsp-signature-mode)
 (lsp-terraform-modules-mode)
 (lsp-treemacs-deps-list-mode)
 (lsp-treemacs-error-list-mode)
 (lsp-treemacs-generic-mode)
 (lsp-treemacs-sync-mode)
 (magit-auto-revert-mode)
 (magit-blame-mode)
 (magit-blame-read-only-mode)
 (magit-blob-mode)
 (magit-todos-mode)
 (magit-wip-after-apply-mode)
 (magit-wip-after-save-local-mode)
 (magit-wip-after-save-mode)
 (magit-wip-before-change-mode)
 (magit-wip-initial-backup-mode)
 (magit-wip-mode)
 (mail-abbrevs-mode)
 (markdown-live-preview-mode)
 (menu-bar-mode)
 (mml-mode)
 (next-error-follow-minor-mode)
 (org-capture-mode)
 (org-cdlatex-mode)
 (org-list-checkbox-radio-mode)
 (org-src-mode)
 (org-table-follow-field-mode)
 (org-table-header-line-mode)
 (orgtbl-mode)
 (outline-minor-mode)
 (overwrite-mode)
 (paragraph-indent-minor-mode)
 (pcre-mode)
 (pdf-annot-edit-contents-minor-mode)
 (pdf-annot-list-follow-minor-mode)
 (pdf-annot-minor-mode)
 (pdf-cache-prefetch-minor-mode)
 (pdf-history-minor-mode)
 (pdf-isearch-active-mode)
 (pdf-isearch-batch-mode)
 (pdf-isearch-minor-mode)
 (pdf-links-minor-mode)
 (pdf-misc-context-menu-minor-mode)
 (pdf-misc-menu-bar-minor-mode)
 (pdf-misc-minor-mode)
 (pdf-misc-size-indication-minor-mode)
 (pdf-occur-dired-minor-mode)
 (pdf-occur-ibuffer-minor-mode)
 (pdf-outline-follow-mode)
 (pdf-outline-minor-mode)
 (pdf-sync-backward-debug-minor-mode)
 (pdf-sync-minor-mode)
 (pdf-view-auto-slice-minor-mode)
 (pdf-view-dark-minor-mode)
 (pdf-view-midnight-minor-mode)
 (pdf-view-printer-minor-mode)
 (pdf-view-themed-minor-mode)
 (prettify-symbols-mode)
 (racket-smart-open-bracket-mode)
 (racket-xp-mode)
 (rainbow-delimiters-mode)
 (read-extended-command-mode)
 (rectangle-mark-mode)
 (reftex-mode)
 (reveal-mode)
 (rxt--read-pcre-mode)
 (rxt-global-mode)
 (rxt-mode)
 (semantic-highlight-edits-mode)
 (semantic-highlight-func-mode)
 (semantic-mode)
 (semantic-show-parser-state-mode)
 (semantic-show-unmatched-syntax-mode)
 (semantic-stickyfunc-mode)
 (server-mode)
 (sgml-electric-tag-pair-mode)
 (sh-electric-here-document-mode)
 (shell-command-with-editor-mode)
 (shell-highlight-undef-mode)
 (show-smartparens-global-mode)
 (show-smartparens-mode)
 (smartparens-global-strict-mode)
 (smartparens-mode)
 (smartparens-strict-mode)
 (smerge-mode)
 (so-long-minor-mode)
 (tab-bar-history-mode)
 (tab-bar-mode)
 (tablist-edit-column-minor-mode)
 (tablist-minor-mode)
 (temp-buffer-resize-mode)
 (text-scale-mode)
 (tool-bar-mode)
 (tooltip-mode)
 (transient-resume-mode)
 (treemacs-hide-gitignored-files-mode)
 (treemacs-indent-guide-mode)
 (treemacs-indicate-top-scroll-mode)
 (treesit-explore-mode)
 (treesit-inspect-mode)
 (undelete-frame-mode)
 (undo-fu-session-mode)
 (url-handler-mode)
 (use-hard-newlines)
 (vc-dir-git-mode)
 (vi-tilde-fringe-mode)
 (view-mode)
 (visible-mode)
 (visual-line-mode)
 (which-function-mode)
 (whitespace-mode)
 (whitespace-newline-mode)
 (with-editor-mode)
 (ws-butler-mode)
 (xref-etags-mode))

Executive summary: Display of images relies on Racket Mode's print handler getting a chance to see the thing being printed. "Normal" langs like Racket wraps each module-level expression in a print. Whereas htdp/bsl does... something else.

racket

This:

#lang racket/base
(require 2htdp/image)
(circle 5 "solid" "red")

fully expands to:

(module issue-667 racket/base
  (#%module-begin
   (module configure-runtime '#%kernel
     (#%module-begin (#%require racket/runtime-config) (#%app configure '#f)))
   (#%require 2htdp/image)
   (#%app
    call-with-values
    (lambda ()
      (#%app
       (let-values (((circle) (lambda argv (#%app apply circle argv)))) circle)
       '5
       '"solid"
       '"red"))
    print-values)))

i.e. Each module-level expression is applied to print-values. So Racket Mode's print handler sees it (as well as explicit calls to print). And can see if it's file/convertible to an image.

bsl

But this:

#lang htdp/bsl
(require 2htdp/image)
(circle 5 "solid" "red")

fully expands to:

(module issue-667 lang/htdp-beginner
  (#%plain-module-begin
   (define-values (repl?) '#f)
   (let-values (((handle) (#%app uncaught-exception-handler)))
     (#%app
      uncaught-exception-handler
      (lambda (exn)
        (if repl? (#%app void) (let-values () (#%app test-display-results!)))
        (set! repl? '#t)
        (#%app handle exn))))
   (#%require 2htdp/image)
   (#%app
    call-with-values
    (lambda ()
      (#%plain-app
       (let-values (((circle) (lambda argv (#%app apply circle argv)))) circle)
       '5
       '"solid"
       '"red"))
    do-print-results)
   (#%app call-with-values (lambda () '"12") do-print-results)
   (module configure-runtime racket/base
     (#%module-begin
      (module configure-runtime '#%kernel
        (#%module-begin
         (#%require racket/runtime-config)
         (#%app configure '#f)))
      (#%require htdp/bsl/runtime)
      (#%app
       call-with-values
       (lambda () (#%app configure '(output-function-instead-of-lambda)))
       print-values)))
   (set! repl? '#t)
   (module*
    test
    #f
    (#%module-begin
     (module configure-runtime '#%kernel
       (#%module-begin
        (#%require racket/runtime-config)
        (#%app configure '#f)))
     (#%app call-with-values (lambda () (#%app test*)) print-values)))))

I don't know what do-print-results does, exactly, but it doesn't seem to print the results. 😄


I bet that

#lang htdp/bsl
(require 2htdp/image)
(print (circle 5 "solid" "red"))

would show an image... except that print is undefined in bsl.

I don't really understand the student languages. If I can't figure it out quickly, but someone can "meet me halfway" and help tell me what needs to change, then I'm happy to change Racket Mode.

Here is the definition of do-print-results:

(define (do-print-results . vs)
  (for-each (current-print) vs)
  (values))

Looking at the value of the current-print parameter for the student languages, it seems to be set to pretty-print-handler.

@camoy Interesting. So in the fully expanded code, now I notice:

   (module configure-runtime racket/base
     (#%module-begin
      (module configure-runtime '#%kernel
        (#%module-begin
         (#%require racket/runtime-config)
         (#%app configure '#f)))
      (#%require htdp/bsl/runtime) ;; <===

Looking there, I see

  (pretty-print-print-hook
   (let ([oh (pretty-print-print-hook)])
     (λ (val display? port)
       (cond
        [(and (not (port-writes-special? port))
              (is-image? val))
         (display img-str port)]

And elsewhere in that file I see

  (define img-str "#<image>")

So when the value is an image, and the port isn't "special" (i.e. not running within Dr Racket), it seems to go out of its way to display that "#<image>" string. It specifically doesn't, for example, give the image value to the the older pretty-print-print-hook value oh (which IIUC would be Racket Mode's) to let it do its thing (which IIUC would be showing the actual image in the REPL).

So this seems to be working as intended by the bsl author (but not as desired in this bug report)? I don't understand the intent so I'm not sure what change or fix would be compatible with that as well as the understandable desire to see the image in the Racket Mode REPL. 😞

Intriguing! Seem like the behavior was introduced in 977271b by @rfindler. The commit message makes me believe that this was not intended as a permanent solution. Maybe Robby has some insight on why it's like this and what a solution might be (either in the teaching languages, in Racket Mode, or both)?

Image printing is usually handled through specials and this code is written with a guard on port-writes-special?. So I am not sure this code is relevant. That is, when a port doesn't handle specials, printing something like #<image> is the correct behavior, iiuc.

  1. @rfindler Do you remember the background motivation for this?

    (IIUC the non-graphical printed value would be otherwise something like (object:image% ... ...). Was the idea that, for students, #<image> would be less confusing and/or lead to fewer off-topic questions?)

  2. In theory I could supply a port-writes-special? wrapper port for the TCP output port ultimately used here by Racket Mode's back end to talk to its Emacs front end. (In reality every time I've looked at make-output-port, I feel like I'm likely to get it wrong; here maybe just cause more, new bugs. But I could give it a shot.)

  • @rfindler Do you remember the background motivation for this?
    (IIUC the non-graphical printed value would be otherwise something like (object:image% ... ...). Was the idea that, for students, #<image> would be less confusing and/or lead to fewer off-topic questions?)

That sounds entirely reasonable and fits with the diff I see. Sadly, I don't actually remember (take no inference from that; my memory is especially bad!). I'd say 95% chance that's exactly what happened.

  • In theory I could supply a port-writes-special? wrapper port for the TCP output port ultimately used here by Racket Mode's back end to talk to its Emacs front end. (In reality every time I've looked at make-output-port, I feel like I'm likely to get it wrong; here maybe just cause more, new bugs. But I could give it a shot.)

I think it would be really great to have a little library that set up some kind of a ports system that could connect via TCP/IP (or just a general bytes-only stream) that could forward some specific set of values that we wanted to render specially for situations like this. It could be used for Emacs but also for VScode integration too, I expect.

Another high-level approach would be to start from the snip% interface (but tune it a little bit) where the thing sent on the network was just a reference/identifier that referred to a specific snip (that survives as a value in the user's program) and then there could be a separate connection for queries to that snip to respond to requests to draw itself or to inform it that it had been clicked on. I don't recommend specifically using snip%'s interface, as that's particularly tuned to "thing that will be put into a text% object" but instead just imagining that a new interface could be designed that would support things similar to what snip% supports (redraw method, mouse interactions, maybe a few more things).

This second approach would be more complex because we'd have to have some way to expire things and we'd have to have a separate layer of communication, but that would be more of a match for the way that DrRacket is looking at this problem currently. Thus, it would be code that DrRacket could also start using which would have the benefit of more sharing between our IDEs. This latter approach might end up looking closer to the way text:ports-mixin works too, which might have some benefits as there is working code there to look at for various tricky issues that'll come up in the IO implementation.

In that code, when the port does handle write-special, the code just passes the image value to the default print handler. But where/how does that give it to write-special? Does each kind of image object have its own custom write/print property?

Over the years I've sometimes noticed write-special and wondered what it's for (so many corners of Racket to learn about someday). I don't think I've ever seen a kind of executive summary/overview of the intended use and examples. (Maybe it's one of those things where, if you have the intended use case, it feels so obvious it's hard to know what to document.)

Also here it does seem to call write-special directly -- with a number struct.

Does that mean Dr Racket is creating a custom port whose write-special proc knows about number structs? Would my custom port for Racket Mode also need to know about that, and require the module where it's defined?? I'm confused how this is supposed to work.

In that code, when the port does handle write-special, the code just passes the image value to the default print handler. But where/how does that give it to write-special? Does each kind of image object have its own custom write/print property?

There is a well-known set of "image values" (I think it includes bitmap% objects, 2htdp/image images, and picts) and once we have one of those and we have a text% object, we know how to make it appear so it can be seen there. So when make-output-port is called, the code that calls it already knows which text% it is going to be putting the images into and so if one of those specific specials shows up, then it knows what to do. (Is this answering the question? I'm not sure I've got what's being asked right.)

Over the years I've sometimes noticed write-special and wondered what it's for (so many corners of Racket to learn about someday). I don't think I've ever seen a kind of executive summary/overview of the intended use and examples. (Maybe it's one of those things where, if you have the intended use case, it feels so obvious it's hard to know what to document.)

I think the way to think of write-special is as an escape hatch to communicate non-bytes to the "other end" of a port. So it is a mechanism for doing that. The port can reject them (as many ports do) but if there is a special set of special values that can actually be printed, then one can make a special port (in the lowest-level case via make-output-port) that will recognize the special and then do something with it. So, for example, we're printing out the results of a program. And this program produces a list of images. We still want to use the pretty-print library to print the list and, at the spots where the pretty printer decides where the images get printed, we can simply call write-special to send the images, without having to actually turn them into text. If, however, the port doesn't support the specials, then the pretty-printer can turn them into text (the way that it turns all other values into text) and still print them.

My thought in the previous message was that a new special-handling port could be created that would handle some specific set of specials that we deem worth supporting, presumably including at least bitmap% objects but possibly going further than that. And that special-handling port would, internally, be connected to either an Emacs (perhaps via a network connection or a pipe or something like that) or connected to another IDE somehow and be able to take advantage of the fact that it has that IDE-specific connection to handle the image.

Also here it does seem to call write-special directly -- with a number struct.

Does that mean Dr Racket is creating a custom port whose write-special proc knows about number structs? Would my custom port for Racket Mode also need to know about that, and require the module where it's defined?? I'm confused how this is supposed to work.

Yes, that's all right, except I don't think that number-snips specials are anywhere near as interesting as the ones for images. So if they weren't supported, it wouldn't be the end of the world. The number snips are handy when you're doing some stuff because you can, after you have the numeric value of your computation already "printed out" you can go back and forth between the decimal expansion and the fraction form. And if the decimal expansion is Very Long you can iteratively explore it (also after the value is computed and not have to go back and change the number->string conversion in the code that computed the value).

If this part all makes sense, then the "back and forth" aspect of the communication would be a refinement/complexification of this basic idea, whereby once some value had, via the write-special mechanism, made its way out of a program and into the IDE, then the value could be interacted with in the ide (clicked on, etc) and there might then be communication going back. But if this is too ambitious, that's okay and it can be tabled.

I hope this is helping to clarify but, if not, apologies in advance and please keep asking questions! This stuff got built up over years and is reasonably considered arcane. Redoing it today would probably not come out the same way! 😄

Before we add snips I'm just trying to make sure I understand the existing pieces.

It seems like various entities may have preferences about how values print, which they can express using various mechanisms:

  • Individual struct objects can control how they print via prop:custom-write, prop:custom-print-quotable.

  • Languages might have opinions, via current-print handler.

  • Tools on behalf of users (preferences like plain vs. pretty print, print as expression?, etc.), via current-print and/or port-print-handler.

    • Question: What is the relation between current-print and port-print-handler? Are they called in succession, and if so in what order?

    • Note: Status quo Racket Mode only sets the current-print handler, which the lang or user program may override, as in this issue. It's on my to-do list to look at using port-print-handler... once I understand how it fits in.

    • Note: Tools may be constrained by need to marshal values like images somehow across process boundaries (e.g. via file/convertible to in-stream bytes or in-stream pathnames of out-of-stream temp files). The tool process might or might not be a GUI process. (Even if it's a GUI process, it might be able to present static images but not a full GUI UX (e.g. Emacs).)

That's my mental inventory so far. Any comments/answers about that?


Now I'm trying to add write-special to my mental inventory. How/where does that fit in? Do objects' prop:custom-write call this?

I think the various handlers are complicated! The other one that comes to mind is the global-port-print-handler as I think that's the one that DrRacket uses to get access to the user program's output. As for your question, I don't know, but it looks like the docs for current-print suggest it is a part of read-eval-print-loop, so if we're not calling that function then we don't need to care about it (maybe?). So I'd guess that the port-print-handler is lower level, as that's called by print. One confusion here is that I don't think current-print and print are related (that is, they aren't related in the way current-eval and eval are related, for example).


The idea of specials is that they exist very close to the ports. Once you have your hand on a port, well, its "other end" is connected somewhere (tcp: the network; file-stream: a file; pipe: the other end of the pipe; the result of make-output-port: a complex API that does stuff). That other place that the port's connected to might be able to accept a racket value that isn't simply a byte. If so you can pass your value to write-special and send it off. But ports in general accept only bytes so you have to arrange things carefully to make specials work (I guess this is why the name "special" was chosen, but I'm not sure).

So, anyone that has access to the port itself can choose to use write-special. So far, I think we've done things kind of "globally" where DrRacket sets various handlers before starting a program and then when the program tries to print a value to a specific spot those handlers kick in an use write-special to send values at a higher level. So, instead of trying to turn a bitmap or a 2htdp/image image or a pict into a stream of bytes to the value of current-output-port, these handlers have interposed and get access to the value before it is converted to a stream of bytes. Then, they refrain from converting it to a stream of bytes and instead use write-special to just send the value itself. If the port handles the special (in practice, this is the same thing as asking "is the current output port the result of make-output-port from text:ports-mixin in the framework but there's no reason it has to be so specific) then voila, the value can be rendered via the IDE in a more sophisticated way.

As for your question, I don't know, but it looks like the docs for current-print suggest it is a part of read-eval-print-loop, so if we're not calling that function then we don't need to care about it (maybe?).

Although not documented there, current-print is also used by the print-values that modbeg.rkt uses to wrap module-level values.

That's why setting current-print has been sufficient until now in Racket Mode as "the hook" to catch and convert image values.

Expressions like (circle _) can be intercepted from module-level values as well as REPL result values, both.

So I'd guess that the port-print-handler is lower level, as that's called by print. One confusion here is that I don't think current-print and print are related (that is, they aren't related in the way current-eval and eval are related, for example).

Yeah. I suppose eval : current-eval :: print : port-print-handler. Sort of. There's also global-port-print-handler. Also they are globals not parameters.

I guess I'm confused if using global-port-print-handler would be good -- it's going to affect all uses of print, even those in the user program, correct? That feels like it might be too invasive?? What if the user program is print-ing to an open-output-string for some purpose? (port-print-handler seems safer-ish: at least it will only affect user programs print-ing to current-output-port).

This kind of segues too..


write-special makes sense in the context of a GUI.

But what if a port wants to support write-special for current-output-port set to a "plain" port (like terminal or TCP) that doesn't support specials?

It could make-output-port a custom port to receive the specials, and weave their conversion as raw bytes into that original plain port along with the normal byte writes. Sounds good. But as soon as I get into the details -- blocking or not, breaks or not, synchronizable events -- it seems quickly non-trivial for my small brain.

For example make-pipe-with-specials seems like a similar problem. Here's the implementation: 😨

;; Not kill-safe.
(define make-pipe-with-specials
  ;; This implementation of pipes is almost CML-style, with a manager thread
  ;; to guard access to the pipe content. But we only enable the manager
  ;; thread when write evts are active; otherwise, we use a lock semaphore.
  ;; (Actually, the lock semaphore has to be used all the time, to guard
  ;; the flag indicating whether the manager thread is running.)
  (lambda ([limit (expt 2 64)] [in-name 'pipe] [out-name 'pipe])
    (let-values ([(r w) (make-pipe limit)]
                 [(more) null]
                 [(more-last) #f]
                 [(more-sema) #f]
                 [(close-w?) #f]
                 [(lock-semaphore) (make-semaphore 1)]
                 [(mgr-th) #f]
                 [(via-manager?) #f]
                 [(mgr-ch) (make-channel)])
      (define (flush-more)
        (if (null? more)
          (begin (set! more-last #f)
                 (when close-w? (close-output-port w)))
          (when (bytes? (mcar more))
            (let ([amt (bytes-length (mcar more))])
              (let ([wrote (write-bytes-avail* (mcar more) w)])
                (if (= wrote amt)
                  (begin (set! more (mcdr more))
                         (flush-more))
                  (begin
                    ;; This means that we let too many bytes
                    ;;  get written while a special was pending.
                    ;;  (The limit is disabled when a special
                    ;;  is in the pipe.)
                    (set-mcar! more (subbytes (mcar more) wrote))
                    ;; By peeking, make room for more:
                    (peek-byte r (sub1 (min (pipe-content-length w)
                                            (- amt wrote))))
                    (flush-more))))))))
      (define (read-one s)
        (let ([v (read-bytes-avail!* s r)])
          (if (eq? v 0)
            (if more-last
              ;; Return a special
              (let ([a (mcar more)])
                (set! more (mcdr more))
                (flush-more)
                (lambda (file line col ppos) a))
              ;; Nothing available, yet.
              (begin (unless more-sema (set! more-sema (make-semaphore)))
                     (wrap-evt (semaphore-peek-evt more-sema)
                               (lambda (x) 0))))
            v)))
      (define (close-it)
        (set! close-w? #t)
        (unless more-last (close-output-port w))
        (when more-sema (semaphore-post more-sema)))
      (define (write-these-bytes str start end)
        (begin0 (if more-last
                  (let ([p (mcons (subbytes str start end) null)])
                    (set-mcdr! more-last p)
                    (set! more-last p)
                    (- end start))
                  (let ([v (write-bytes-avail* str w start end)])
                    (if (zero? v) (wrap-evt w (lambda (x) #f)) v)))
          (when more-sema
            (semaphore-post more-sema)
            (set! more-sema #f))))
      (define (write-spec v)
        (let ([p (mcons v null)])
          (if more-last (set-mcdr! more-last p) (set! more p))
          (set! more-last p)
          (when more-sema
            (semaphore-post more-sema)
            (set! more-sema #f))))
      (define (serve)
        ;; A request is
        ;;  (list sym result-ch nack-evt . v)
        ;; where `v' varies for different `sym's
        ;; The possible syms are: read, reply, close,
        ;;  write, write-spec, write-evt, write-spec-evt
        (let loop ([reqs null])
          (apply
           sync
           ;; Listen for a request:
           (handle-evt
            mgr-ch
            (lambda (req)
              (let ([req
                     ;; Most requests we handle immediately and
                     ;; convert to a reply. The manager thread
                     ;; implicitly has the lock.
                     (let ([reply (lambda (v)
                                    (list 'reply (cadr req) (caddr req) v))])
                       (case (car req)
                         [(read)
                          (reply (read-one (cadddr req)))]
                         [(close)
                          (reply (close-it))]
                         [(write)
                          (reply (apply write-these-bytes (cdddr req)))]
                         [(write-spec)
                          (reply (write-spec (cadddr req)))]
                         [else req]))])
                (loop (cons req reqs)))))
           (if (and (null? reqs) via-manager?)
             ;; If we can get the lock before another request
             ;;  turn off manager mode:
             (handle-evt lock-semaphore
                         (lambda (x)
                           (set! via-manager? #f)
                           (semaphore-post lock-semaphore)
                           (loop null)))
             never-evt)
           (append
            (map (lambda (req)
                   (case (car req)
                     [(reply)
                      (handle-evt (channel-put-evt (cadr req) (cadddr req))
                                  (lambda (x) (loop (remq req reqs))))]
                     [(write-spec-evt)
                      (if close-w?
                        ;; Report close error:
                        (handle-evt (channel-put-evt (cadr req) 'closed)
                                    (lambda (x) (loop (remq req reqs))))
                        ;; Try to write special:
                        (handle-evt (channel-put-evt (cadr req) #t)
                                    (lambda (x)
                                      ;; We sync'd, so now we *must* write
                                      (write-spec (cadddr req))
                                      (loop (remq req reqs)))))]
                     [(write-evt)
                      (if close-w?
                        ;; Report close error:
                        (handle-evt (channel-put-evt (cadr req) 'closed)
                                    (lambda (x) (loop (remq req reqs))))
                        ;; Try to write bytes:
                        (let* ([start (list-ref req 4)]
                               [end (list-ref req 5)]
                               [len (if more-last
                                      (- end start)
                                      (min (- end start)
                                           (max 0
                                                (- limit (pipe-content-length w)))))])
                          (if (and (zero? len) (null? more))
                            (handle-evt w (lambda (x) (loop reqs)))
                            (handle-evt
                             (channel-put-evt (cadr req) len)
                             (lambda (x)
                               ;; We sync'd, so now we *must* write
                               (write-these-bytes (cadddr req) start (+ start len))
                               (loop (remq req reqs)))))))]))
                 reqs)
            ;; nack => remove request (could be anything)
            (map (lambda (req)
                   (handle-evt (caddr req)
                               (lambda (x) (loop (remq req reqs)))))
                 reqs)))))
      (define (via-manager what req-sfx)
        (thread-resume mgr-th (current-thread))
        (let ([ch (make-channel)])
          (sync (nack-guard-evt
                 (lambda (nack)
                   (channel-put mgr-ch (list* what ch nack req-sfx))
                   ch)))))
      (define (start-mgr)
        (unless mgr-th (set! mgr-th (thread serve)))
        (set! via-manager? #t))
      (define (evt what req-sfx)
        (nack-guard-evt
         (lambda (nack)
           (resume-mgr)
           (let ([ch (make-channel)])
             (call-with-semaphore
              lock-semaphore
              (lambda ()
                (unless mgr-th (set! mgr-th (thread serve)))
                (set! via-manager? #t)
                (thread-resume mgr-th (current-thread))
                (channel-put mgr-ch (list* what ch nack req-sfx))
                (wrap-evt ch (lambda (x)
                               (if (eq? x 'closed)
                                 (raise-mismatch-error 'write-evt "port is closed: " out)
                                 x)))))))))
      (define (resume-mgr)
        (when mgr-th (thread-resume mgr-th (current-thread))))
      (define in
        ;; ----- Input ------
        (make-input-port/read-to-peek
         in-name
         (lambda (s)
           (let ([v (read-bytes-avail!* s r)])
             (if (eq? v 0)
               (begin (resume-mgr)
                      (call-with-semaphore
                       lock-semaphore
                       (lambda ()
                         (if via-manager?
                           (via-manager 'read (list s))
                           (read-one s)))))
               v)))
         #f
         void))
      (define out
        ;; ----- Output ------
        (make-output-port
         out-name
         w
         ;; write
         (lambda (str start end buffer? w/break?)
           (if (= start end)
             0
             (begin
               (resume-mgr)
               (call-with-semaphore
                lock-semaphore
                (lambda ()
                  (if via-manager?
                    (via-manager 'write (list str start end))
                    (write-these-bytes str start end)))))))
         ;; close
         (lambda ()
           (resume-mgr)
           (call-with-semaphore
            lock-semaphore
            (lambda ()
              (if via-manager? (via-manager 'close null) (close-it)))))
         ;; write-special
         (lambda (v buffer? w/break?)
           (resume-mgr)
           (call-with-semaphore
            lock-semaphore
            (lambda ()
              (if via-manager?
                (via-manager 'write-spec (list v))
                (write-spec v)))))
         ;; write-evt
         (lambda (str start end)
           (if (= start end)
             (wrap-evt always-evt (lambda (x) 0))
             (evt 'write-evt (list str start end))))
         ;; write-special-evt
         (lambda (v)
           (evt 'write-spec-evt (list v)))))
      (values in out))))

And that's not yet even adding in the wrinkle of using file/convertible etc.

So... the idea that some langs can just write-special to a port seems pretty centered on the notion that the port isn't anything like an OS port, it's a virtual bucket from which things will be inserted into a GUI. Which is fine, it's just nothing like my situation. 😄

Anyway I don't mean this to sound like a rant. I'm not frustrated. I'm more just thinking out loud. And hoping that I'm over-thinking it and someone will point out a simpler way to approach it.

Employing wishful thinking, the following wrap is what I'd like to exist.

I'm really, really not sure about my implementation. "It works". With assumptions.

#lang racket/base

(require racket/async-channel
         racket/match)

;; output-port? (any/c -> bytes?) -> output-port?
;;
;; Given an output port, return a new port that handles write-special
;; by giving each value to a procedure that transforms it to bytes.
(define (wrap port proc)
  (define ach (make-async-channel 128))
  (define ready-evt (async-channel-put-evt ach 'n/a))
  (define (get)
    (match (async-channel-get ach)
      ['exit              (flush-output port)]
      [(cons 'bytes bstr) (display bstr port)     (get)]
      [(cons 'special v)  (display (proc v) port) (get)]))
  (define manager-thread (thread get))
  (define (empty!)
    (async-channel-put ach 'exit)
    (thread-wait manager-thread))
  (define (flush!)
    (empty!)
    (set! manager-thread (thread get)))
  (define (close!)
    (empty!))
  (define (write-out bstr start end _non-block? _breakable?)
    ;; Assumption: #f for get-write-evt below means we'll never be
    ;; called with non-false non-block.
    (cond [(= start end)
           (flush!)
           0]
          [else
           (let ([bstr (subbytes bstr start end)])
             (async-channel-put ach (cons 'bytes bstr))
             (bytes-length bstr))]))
  (define (write-out-special v _non-block? _breakable?)
     ;; Assumption: #f for get-write-special-evt below means we'll
     ;; never be called with non-false non-block.
     (async-channel-put ach (cons 'special v))
     #t)
  (make-output-port 'special-wrapper
                    ready-evt
                    write-out
                    close!
                    write-out-special
                    #f   ;get-write-evt
                    #f   ;get-write-special-evt
                    #f   ;get-location
                    void ;count-lines!
                    1    ;init-position
                    #f)) ; buffer-mode

;; Usage
(define orig-port (open-output-string))
(port-writes-special? orig-port) ;=> #f
(define wrap-port (wrap orig-port
                        (lambda (v) (format "#<SPECIAL ~v>" v))))
(port-writes-special? wrap-port) ;=> #t
(display "Hello" wrap-port)
(flush-output wrap-port)
(get-output-string orig-port) ;=> "Hello"
(write-special 42 wrap-port)  ;=> #t
(close-output-port wrap-port) ;should flush
(get-output-string orig-port) ;=> "Hello#<SPECIAL 42>"

I had thought the goal of the conversation was to find a way to get Emacs to print images in a way inspired by the way DrRacket does it. I think that, if that's the goal, a plausible way to do that, is to use specials in a way similar to DrRacket (where the special is noticed by the racket program and then a side-channel of communication is used to get the image into a format where Emacs will be happy to have it inserted into a buffer). But maybe that's not the goal? IIUC, all of the code that actually does printing of images should already have guards using port-write-special? so we'd already just be getting bytes into the port if the port doesn't support specials and so IO would show things like #<image> instead of the actual image.

I think that's I'm doing (unless I'm misunderstanding you).

Probably I should have put a comment like ;placeholder for file/convertible on the format line:

My minimal make-custom-port example above merely prints (format "#<SPECIAL ~v>" v) as a placeholder -- for using file/convertible to marshal the image (or at least a temp file name) to the Emacs process in the byte stream. I already do that; have no concerns about it.

Instead, I'm obsessing on all the details of make-custom-port -- how I'd implement it with decent performance and correct concurrency. I'm wondering if it could be (nearly) as simple as using an async-channel as I sketched out... as opposed to hundreds of lines of the sort of concerns that make-pipe-with-specials seems to juggle.

p.s. And the goal would be so that the REPL's current-output-port would be this custom port, and anything checking for port-writes-special? would get true, and use write-special, and take this path similar to DrR (IIUC).

Ah, obsessing over the details of make-input-port is totally reasonable. It is very complicated. I think a lot of the complication has to do with buffering / performance / concurrency, not specials. So it is entirely possible there is a missing abstraction that handles all that, but with a reasonably performant escape hatch to cope with specials.

I was worried because it looks like the only thing to do with a special is to turn it into bytes to put into the same stream, but IIUC, this won't work because you won't be able to tell, on the receiving end, if those bytes just happened to look like what a special looks like or if they really are a special.

re: the p.s., okay that's exactly what I was thinking too!

Would it be possible, do you think, to actually just use make-pipe-with-specials or a small variation of it? That is, if you gave one end of the pipe to the user's program and then you took the other end and copied bytes out of it (using read-bytes-avail!, which is complicated but not as complicated as make-input-port) and then used those bytes to communicate with Emacs?

Would it be possible, do you think, to actually just use make-pipe-with-specials or a small variation of it? That is, if you gave one end of the pipe to the user's program and then you took the other end and copied bytes out of it (using read-bytes-avail!, which is complicated but not as complicated as make-input-port) and then used those bytes to communicate with Emacs?

Oh, that's a great idea. I'll explore that.

I was worried because it looks like the only thing to do with a special is to turn it into bytes to put into the same stream, but IIUC, this won't work because you won't be able to tell, on the receiving end, if those bytes just happened to look like what a special looks like or if they really are a special.

Great question. A few answers:

Simple

Status quo, the file/convertible stuffs ends up displaying bytes much like #<<Image /path/to/some/image/bytes/in/temp-file>>. The Emacs REPL buffer looks for that with a regexp and takes it from there (replacing the text with an image created from that file, if it exists).

This obviously isn't bullet-proof -- e.g. what if the user program output happens to look like that -- but in practice it's worked fine.

Better

Instead the back end could output something structured, like s-expressions that are (say) (or/c (cons/c 'output bytes?) (cons/c 'image path-string?)). The Emacs REPL buffer would parse these s-expressions to insert the bytes or image into the buffer.

Maybe not enough of an improvement, practically, to bother doing... except maybe as part of...

Someday/maybe

This reminds me of an idea I've considered for awhile. We all know "the top-level is hopeless". 😄. Also, current-output-port for a REPL is hopeless -- a hopeless lossy mix of (a) prompt text, (b) current-printed Racket values, (c) output from the user program, (d) occasionally messages from the back end, inserted as comments. And so add (e) images to this list.

This all gets mixed together into bytes, and then a REPL buffer has to work at figuring out the pieces again. e.g. Let's color/propertize prompts this way, maybe color-lexer-ize the printed values, don't touch the other miscellaneous output, etc.

So the idea really is just to make the output be s-expressions for an ADT of all these things. It should still be a single "stream", the original order is important; the change is simply to preserve the original pieces.

Just for the record, the code to touch is probably here:

https://github.com/racket/htdp/blob/master/htdp-lib/htdp/bsl/runtime.rkt

(That's also where the #<image> comes from.)

I've been looking at this, including the handling of input: REPL interactions, as well as "raw" input.

@rfindler I have a question about the status quo behavior of DrRacket.

Given this repl-in-repl.rkt:

#lang racket/base
(display "Enter anything: ")
(read-line)
(displayln "Starting REPL...")
(parameterize ([current-namespace (make-base-namespace)])
  (read-eval-print-loop))

Running with command-line racket: you can enter something for read-line, then read-eval-print-loop keeps r-e-p-ing entered expressions until you enter CTRL-d.

Running this in DrRacket:

image

  • It provides the special "raw input box". (This is one element I was contemplating how to handle in Emacs.) Works great.
  • It displays one ">" prompt. When I type "42" and press ENTER... nothing happens. I just get a newline printed. Eventually my laptop fan spins up high, gracket is pegging the CPU at 100%.

Probably that is a bug. But regardless: What is the expected behavior here in DrRacket? Should the user's REPL be "just raw input"? Or, inherit the REPL input interaction of DrRacket? Or, something else?? (This is the other element I was contemplating, for Emacs; I tried this in DrR to see its opinion.)

In answer to your question, I'd expect the same (or analogous) behavior in the DrRacket repl to what you see in the terminal.

I don't know exactly what the bug is, but it looks like something fishy is happening with current-get-interaction-input-port. You can work around that problem like this to keep exploring.

#lang racket/base
(display "Enter anything: ")
(read-line)
(displayln "Starting REPL...")
(parameterize ([current-namespace (make-base-namespace)]
               [current-get-interaction-input-port
                (λ () (current-input-port))])
  (read-eval-print-loop))

In answer to your question, I'd expect the same (or analogous) behavior in the DrRacket repl to what you see in the terminal.

I don't know exactly what the bug is, but it looks like something fishy is happening with current-get-interaction-input-port. You can work around that problem like this to keep exploring.

#lang racket/base
(display "Enter anything: ")
(read-line)
(displayln "Starting REPL...")
(parameterize ([current-namespace (make-base-namespace)]
               [current-get-interaction-input-port
                (λ () (current-input-port))])
  (read-eval-print-loop))

Thanks. With that, it works. Note that DrRacket gives a "raw user input port" box for the user programs' read-eval-print-loop. Which differs from racket with expeditor, where the user program REPL input also gets coloring/nav.

But I'm not sure either one is obviously "correct" or even "better". I'm not even sure how important it is; just trying to think it through before diving in.

The thinking with the DrRacket design is that the input going into the user's program is not known to be a program but just some bytes, whereas the input going into the REPL is known to be a program. So we do highlighting/coloring/navigation-keystrokes on the parts that are known to be a program but not on the ones that aren't known to be a program. In the case of the raw racket repl and expeditor, the separation between the REPL and just regular input isn't so clear. At least, I think that's the idea.

Update: This issue is fixed on the hash-lang branch, because there I did take the opportunity a couple weeks ago to change how REPL I/O is handled. Among many other changes, current-output-port is now one for which port-writes-special? will return true. Hoping to merge that branch in about a week.

Although this isn't narrowly related to the new racket-hash-lang-mode for edit buffers, that new mode's use of the REPL added even more reasons to make a change to the REPL. I'm going to add a slightly misleading label to this issue, as a reminder to mark this fixed when I merge the branch.