But usually there is something around it. Typically a Lisp system can run several REPLs at the same time, one might be a break loop. You can define a function in another REPL and use it in the break-loop. You can define the function by loading code in a break-loop.
An interaction might be (here in LispWorks):
CL-USER 6 > (> (sin-x2 3) 0)
Error: Undefined operator SIN-X2 in form (SIN-X2 3).
1 (continue) Try invoking SIN-X2 again.
2 Return some values from the form (SIN-X2 3).
3 Try invoking something other than SIN-X2 with the same arguments.
4 Set the symbol-function of SIN-X2 to another function.
5 Set the macro-function of SIN-X2 to another function.
6 (abort) Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
CL-USER 7 : 1 > (load "~/sin-x2.lisp")
; Loading text file /Users/foo/sin-x2.lisp
#P"/Users/foo/sin-x2.lisp"
CL-USER 8 : 1 > :c 1
T
A function SIN-X2 does not exist. There is an error and we get a break-loop. The break-loop is one level deep. All of Lisp is available in a break-loop: the compiler, the code loader, the evaluator, ... Lisp stays with the break-loop in the context of the error.
I define/write the function in a file and the load the file. Loading the makes the function available in the running Lisp.
Then I use the continue restart to try to find the function again -> the error is gone
In Smalltalk, the code lives inside the image. You can dump it to a file/set of files using "file-out" operation, and import it from files with file-in. But the code is never executed directly from the files - it's parsed, compiled, and executed on read, which creates runtime representation of the code which is saved in the image. When you break into a debugger in the running Smalltalk image, it works on top of the same infrastructure, giving you direct access to the actual code as well as its textual representation at all times. Smalltalk is also highly reflective, so the compiled code (methods) and objects (classes are objects) is available for introspection and modification. Though it's not the best introduction, this post may help get you interested in the paradigm: https://blog.bracha.org/exemplarDemo/exemplar2021.html?snaps...
It's impossible to do with Python, or any other language that wasn't created with image-based runtime. I tried. This approach only works when the "vertical integration" reaches from the very bottom (compilation, stack handling, memory allocation, object creation) to the very top (editing the source, refactoring, test runners, etc.) It's incredibly powerful when it works and is done right. Imagine your whole OS being built around GDB, with extremely late binding of everything and each method being a separate .so/.dll. It would probably be incredibly slow, which is why Smalltalk and Smalltalkers pioneered JITs - but it would also allow you to override any part of your running system while it's running.
My understanding is that you usually write the code in your editor and use an editor command to send the code directly to the Lisp process (that’s what I do, anyway). Or just copy-paste.
I’m not aware of a way to get code back out of the Lisp environment, but I’m fairly new to all this.
There’s very rarely any where you can actually interact with the running program. At best you can usually load in a method and poke it, but that’s not the same.
One of the other comments in this thread links to: https://mikelevins.github.io/posts/2020-12-18-repl-driven/ which makes a distinction between "having a REPL" and "supporting repl-driven programming". Modern languages which have a REPL generally don't support the sort of repl-driven programming they refer to. Julia's Revise.jl [1] supports one way of repl-driven programming, and it is in fact the recommended way of doing Julia, but from my understanding it's still very different from the Lisp ways.
tl;dr
1. no part of the system off limits
2. pervasive interactivity
3. homoiconicity