Note that Rust does have a fully featured and tested interpreter: MIRI (it's not just for detecting undefined behavior)
I just don't know how well you can hammer the behavior that encodes into a usable REPL.
Note that Rust does have a fully featured and tested interpreter: MIRI (it's not just for detecting undefined behavior)
I just don't know how well you can hammer the behavior that encodes into a usable REPL.
The Cranelift codegen backend might be a much better starting point than MIRI, it seems.
one problem is that the rust compiler is generally built around the fact that query results are immutable.
Redefinition would indeed be a pain. What could work instead is creating internal distinct type names.
For example, if the user defines a struct Foo
, the runtime can call it Foo__0
internally and call it Foo__1
after one redefinition. This way, the compiler is agnostic to struct redefinitions, and, there cannot be any fishy type mismatch at run time. The runtime should alias Foo
to the latest definition when the user types it, so the user is not affected if they do not mess with the previous definitions; however, if they do want to mess with the previous definitions, they can access them through the internal names.
I don't know about in practice, but the high level descriptions of the query based compiler design is that the output is solely determined by the inputs, and changing the inputs will invalidate and refresh anything that depended on them; the goal being that you can use the same compiler for both offline and language server cases.
They might be immutable in the sense of giving you a representation you can't mutate (so they can be cached), but that by itself doesn't mean you can't change what the meaning of "Foo" is.
What would happen in Rust if you reload a module and a struct now has a different layout?
You 'just' don't support that.
REPLs are incredibly useful. Even if they don't support every possible feature, that only makes them moderately less useful.
Take GDB and LLDB. They provide expression evaluation for C and C++[1], so they act as a form of REPL. Except that they're absolute garbage at it. They don't support macros[2], so referring to constants often just… doesn't work. You have to look up the value by hand. They don't support calling inline functions or instantiating templates, so calling functions often just… doesn't work. You have to recreate the behavior by hand. Disabling optimizations helps with the second one, but only partly.
And yet despite those ridiculous limitations, I still use expression evaluation all the time. Not just for basic field accesses like foo.bar
; also function calls, and sometimes even loops. In some cases I can't use these features (they're often not supported on embedded targets), and then I miss them dearly.
With the right compiler design, most of those limitations can be lifted without making any compromises in the language design. As people have mentioned in this thread, you don't make the generated code less static; you mainly just keep IR around so you can generate more code. For a REPL that means you can call any function (even if the original program inlined it or never generated code for it), and use any generic parameters, regardless of optimization level. However, you can't necessarily replace existing types or functions.
Swift does it. Haskell does it. C++, despite the limitations of its debuggers, has seen attempts at more featureful REPLs, including this newish one in Clang. Apparently CERN has used various C++ REPLs for a long time. (Yes, they are trying to do interactive data science in C++.)
Another highly-related feature is hot reloading, or Edit and Continue. With this, you can replace existing functions, to a limited extent. Combined with a REPL, this can further approach the power of a dynamic language's REPL. MSVC supports this for C++, though apparently enabling it comes at the cost of disabling many optimizations. There's also this thing that apparently makes fewer compromises on optimizations and sees use in the game industry. Swift also has some support for hot reloading (@_dynamicReplacement
), which Xcode uses for UI previews.
Hot reloading is harder to implement than a REPL. You need better compiler backend integration. You may have to make compromises on performance (i.e. requiring some optimizations to be disabled), and you certainly need to make compromises on functionality (not all changes can be hot-reloaded). But in exchange for those compromises, you still don't need to compromise the language design.
Is this the #1 thing I'd wish to see Rust's limited compiler-team bandwidth be spent on? No; there's lower-hanging fruit. And anyway I'm not their boss.
But I just want to forcefully push back against the idea that Rust's design inherently prevents it from having good interactivity support. In this particular respect, it absolutely could "please everyone". Maybe it can't be quite be as interactively pleasing as a dynamic language, but it can get a lot closer than it does today.
GDB supports Rust expression evaluation too but this support is apparently quite limited. ↩︎
Without compiler flags that nobody uses. ↩︎
As I mentioned, you can and people often already do interpret Rust with MIRI, so you could probably technically handle swapping out a module definition that way, but you still need to answer what happens with existing data in the old types, in particular when you add fields.
There is some prior art here as you mention with VS Edit And Continue (which IIRC basically just gives up and restarts if you change structures), and JavaScript Hot Module Reload (HMR), but my experience with REPLs have a different enough UX from those that it might not make sense to treat them the same.
Rust should have a reactive notebook to resolve the issues of redefinition and of hidden state. Two successful examples are Pluto.jl for julia and Marimo for python.
That is certainly a better model than jupyter. However that is about half the issues only: debugging, setting breakpoints etc was also terrible in notebooks. How do you propose to sole that?
If we are doing hot code reload debugging is tricky unless the debugger is build especially around that. Gdb and lldb (that Rust use currently) certainly aren't built with that in mind.
An interesting alternative to interactive notebook usage (e.g. the typical Jupyter Notebook model) could be exploring some kind of time travel debugging such as that provided by rr. The basic idea is that instead of only seeing program state at the current breakpoint and being able to step forward, it's also possible to step backwards and inspect what the program state used to be earlier in the execution chronology.
If you always recompile and re-run an entire notebook program as a unit, then notebooks can be viewed as a form of literate programming with a limited form of time travel debugging support, allowing you to inspect (parts of) program state at various checkpoints in the execution after its been run.
I don't know how useful this lens is in practice, as I've never had the opportunity to utilize rr, but I've heard strong claims that (when utilized well) rr can be as revolutionary to debugging over forwards breakpoint debuggers as those same interactive debuggers are over log based debugging.
I wonder what @davidlattimore would say here .
Thanks, I hadn't noticed this thread. I wrote Evcxr originally because it seemed like an interesting thing to write. I'm not a big user of REPLs, and I use Jupyter notebooks even less. Jupyter notebooks do however seem to be important for certain kinds of work - especially data science. There are features that are currently lacking in evcxr_jupyter that I think would make it more useful for experimentation. Specifically providing a way for the Rust code to communicate with Javascript code running in the browser. This would let you do things like have some slider widgets where you can adjust some values and have the Rust code recompute a graph or something. I wanted to implement that, but got sidetracked by other projects (cackle and the wild linker).
I think a REPL / Jupyter kernel could benefit from being built on top of hot code reloading rather than the way evcxr currently works which is by loading .so files for each bit of code you run. I do hope to eventually implement hot code reloading as part of my work on the Wild linker. I figure once I have incremental linking working, it's not a massive step further to change the code of a running program.
I'd really like to see a Jupyter notebook like thing built that runs entirely in the browser, where you can write Rust code and have it compile in the browser to wasm, then run the resulting wasm and display results in the form of text, images, interactive graphs etc. When I started evcxr, I even contemplated building it with wasm, but I couldn't see any way to dynamically load new wasm code - i.e. I couldn't find an equivalent of dlopen. Not having dlopen or hot code reload makes it kind of hard to have persistent state. That said, maybe for many uses, people could live without persistent state - or maybe if there's APIs for quickly and easily saving and loading state, then the lack of persistent state might matter less. It'd be a fun project to try to build. I'd like to do it, but (a) I have too many projects already and (b) it's probably best if it were built by someone who would use their own tool - i.e. a data science person.
In the browser you can share memory between different wasm modules with some effort, from what I can tell? So at least in theory you should be able to pull it off in other runtimes. Sounds like the sort of thing that would turn into an endless tar pit, but now you've nerd sniped me...
Swift REPL uses the LLDB debugger to run code: Swift.org - REPL and Debugger. Not sure how that translates to Rust.
Looks like Nim's REPLs are not sophisticated enough. The one that come with the compiler uses a VM; INim just recompiles and reruns everything on each input: Variables don't persist · Issue #130 · inim-repl/INim · GitHub.
What if Rust wouldn't have/get "First-class support for interactive use" but instead "First-class support for hot-reloading"?
I think that would have even more use cases where it is applicable, for example in game development (I don't know if bevy has something like this) or during development in general.
Imagine the following:
// Code responsible for things like window creation or other long-running things
#[hot_reload]
mod some_functionality {
pub fn do_something() {}
}
#[hot_reload]
fn do_something_else() {}
// Alternative syntax that allows having multiple unrelated functions in the same hot-reload bundle
#[hot_reload("name")]
fn do_something_else() {}
Instead of statically linking those functions they'd be loaded as a dll or so (or similar) at runtime, similar to how I assume you're thinking about doing it and how (I think) hot reloading can be done in C.
This would allow the following:
cargo run --hotreload
and cargo build --hotreload
: Adds extra logic to the binary for re-linking the dll/so file when it changed (alternatively there could be a function in std to request a hot reload (e.g. on key press). When changing code and running cargo build --hotreload
the dll/so files are updated and thus the code that is running changes. When compiled without --hotreload
those attributes can be ignored.
As far as I can tell this works as long as you don't change the rust version (which can change the abi), makes iterative development easier because you can just change the function without recompiling everything and could be used for interactive use via REPL/notebooks.
With normal crates it is currently not possible (correct me if I'm wrong), unless every hot-reload module/bundle is its own crate and the crate adds the required dll loading and file watching. And even then I'm not sure if it would work with normal functions that are not C ffi.
For a REPL/notebook you'd probably also want the ability to run/load an arbitrary amount of modules/bundles instead of just the modules/bundles the binary already knows about, but that should be possible by calling a function (e.g. to load everything from a file, similar to how dynamic linking already works.
The downside would be that those hotreload bundles behave more like a crate than a module, but you wouldn't have to completely restructure your code when you want to iteratively debug/implement something new.
This breaks when you change any struct
/trait
or just anything that changes the memory layout of some data type, since data with the old layout could be in use by the running program and changing the expected layout would be very much UB. You also cannot unload the memory of the old DLL, as there could be function pointers pointing to its functions stored somewhere.
Keeping in mind that a user is perfectly able to create type aliases: even if the REPL does it, could that itself not be considered a redefinition?
I believe that compiler design is precisely the same design that works really well for IDEs, at least in terms of large-scale architecture l: query-based, aggressive caching with invalidation-on-redefinition, etc.
So basically, a very similar effort as is planned for what could feasibly become rustc2
...at some indeterminate point in the future.
I didn't know about this, but it somehow makes intuitive sense: you'd need an army of PhD's for that
That makes sense. All the more reason then to include it into the list of requirements from day 1 when work on a hypothetical rustc2
begins in earnest.
Indeed they aren't. And if there ever were efforts to create a new debugger to accommodate this, I suspect that the rr model of debugging may be a much better starting point, since it allows for backwards time travel, as opposed to just restarting the debug process.
Ninja'd
The whole point of hot code reloading is to accommodate interactive iteration isn't it? I mean for non-interactive use (e.g. deployment to production) the entire concept doesn't really seem matter all that much.
I do agree that hot code reloading, if done properly, would be quite a nice feature to have during development.
I don't know if there's a better term than "interactive" for a REPL then just REPL, and that's a quite different flow than hot reloading. Better? I'm not sure: but certainly having both is nice.
True, every type that crosses this module boundry effectively is part of the Signature of this bundle and would require a complete reload/restart if changed, the same for things like the layout unless the compiler can ensure (or at least check) it stays the same. Might have to mess with the vtable of those types if just a function is added, otherwise that would also be breaking this. But even then: I'd argue that a lot of times (during development) you don't change this module/bundle signature (same as you often don't change a function's signature (or the API of a library for non-breaking changes) when updating the logic in it.
As for function pointers: Good point, I'm not sure what could be done there. That might require the compiler/backend checking if this happens.
They are different, yes. Initially I thought you could build repl/notebook functionality using this, but trying to formulate this showed that it's more complex than I thought. Especially in regards to data/types shared between hot-reload bundles if you don't know them in advance and how global variables would have to be handled. Regardless: I think there is a decent overlap in requirements and compiler changes for those two features that it might be a good idea to design both at the same time.
Looks like the compiler team did discuss how to implement a REPL in 2019:
They were looking at Miri, and @alexreg was interested to work on it.
The topic of a Miri REPL was previously talked about here: