InjectionIII open source project and "Conversational Programming"

I wrote this "manifesto" a few years ago and thought I'd share it as, on the evolution forums we don't talk much about the other part of the equation, tooling. While there will always be a role for compilers for resource intensive coding e.g. AI and the edit compile test treadmill, as code reuse increases using packages and those packages bring in other packages the developer experience for working a program of modest dimensions continues to degrade and I believe translated languages may be the future in the long run. There is hope however, using the approach outlined below you can have the best of both. The rigour of a strongly typed language and the agility of scripting. On with the manifesto...

When I started programming, a programmer's lot had been long established. You wrote a program in a text editor, you compiled it and ran it to see if it met your requirements. At around the same time however, something far more compelling was in its genesis at Xerox PARC which we seem to have completely lost sight of. Xerox had made a fortune on photocopying technology and had started a small research group to study what the office of the future might look like. I never had the privilege of using a Smalltalk system which was the product of this research but as I understand it, it was a completely integrated development environment where the lines between the programmer's representation of their program (its source code) and the program that ran were completely blurred. It's rather difficult to describe but, you can think of it in "modern" terms as a type of playground along the lines described in Bret Victor's influential video of 2012 where you edited your program and ran it, all in the same virtual machine except taken to its logical extreme. You could browse and modify components of the system and such as a window's chrome and even the language itself and apply changes immediately as the "program" you ran was just byte-code data in the virtual machine you could update at any time. This distinct paradigm came to be called "Conversational Programming" allowing a developer to rapidly iterate and try new ideas and its success can be attested to by what they were able to achieve in a very short period of time at PARC: Bitmapped displays, popularising OOP, ethernet, windowing systems, the laser printer.

By the time I had my first real exposure to some of these ideas such as messaging and, yes, the [weird syntax], it was through Objective-C, but somehow we were back on the edit-compile-run treadmill and so it has been ever since. These strictures of source and object files and executables have become so entrenched, the concept that a program can be malleable at run time is rarely even considered. Languages that are translated can take a different approach. There is less of this distinction between a static "program" the computer understands and "data" we can operate on. It's all just data and can be modified at will. The power of this flexibility has lead to a surge in the fortunes of these languages as they are lightweight and "agile" in the broadest sense of the word although this alone does not provide true interactivity.

"Conversational Programming" has two components. The first, one could term the "Live Coding" part by which I don't mean demonstrating your prowess in front of a tech conference but the dynamic updating of components of a program's code without restarting it. The second component, one could term the "Hot Reloading" part is a more subtle requirement. Once the program has been updated, how does one ensure the artefacts of running the program (its output, its visual appearance on the display) are updated by knowing which sections of the program to rerun? The second problem is not an easy one to solve but newer declarative technologies such as SwiftUI and Combine may move us closer to a satisfactory solution to this.

"Conversational Programming" also requires a commitment on the part of the programmer. It is a novel challenge to be able to reason about a program that is mutating while you are running it and it's been my experience that many programmers are unable or unwilling to take this additional cognitive load on. It is however something you get better at with time and the longer you do it and the bigger the app the potential rewards in terms of saved time start to add up. One is able to avoid distracting, repeated time consuming builds then having to restart your app and navigate back to where you were working to test your change.

Even in the security conscious world where compiled code needs to be "signed" before it has access to the CPU surprisingly, the components are available to free ourselves of this artificial distinction between "code" and "data" and get back to where we nearly were in the mid 1970s. The compilation unit can be as small as a single source file and it can be recompiled and dynamically loaded into a process if a way can be found for it's new "implementations" to take the place of the old. This was an explicit capability of Objective-C's dynamic runtime (Swizzling!) I felt was never really capitalised on as much as it could have been. But what of "statically linked" languages such as Swift? Where there's a will there's a way...

A year ago I'd never heard of "interposing" but it is a method of indirection used by Apple's dynamic linker to "late bind" references to symbols in system libraries explained at length in this article. What was also a surprise was learning that when you link a program you have the option of making all symbol references indirect in this way and therefore "interposable". This re-opens the possibility of what is essentially dynamic binding and the substitution of function implementations at run time and we're in business; A programming environment where we have the type safety and rigour of a compiled language combined with the dynamism of an interpreted language.

The specifics of rummaging around in symbol tables to find the function pointers to "interpose" and using a small piece of code "fishhook" to perform the interposes are not of interest here. What is of interest is that we have the technology to turn a Source code editor such as Xcode into a "program editor" where saving a file takes immediate effect which gives us the prospect we can find our way back to a truly integrated IDE. A proof of concept for this idea can be found in the InjectionIII open source project. While it runs alongside Xcode rather than being being completely integrated it realises much of the potential of "Conversational Programming". If you're interested in more technical detail, it is laid out in the short book Swift Secrets. Perhaps one day Apple will pick it up and turn it into an accessible mainstream technology to renovate their toolchain. This will help compilers see off the threat these upstart interpreted languages pose to their future.

18 Likes

To nystateofhealth@johnno1962 ,

The journey from the early days of programming to the current state of the industry is well captured. The transition from the experimental and integrated environments like Smalltalk to the more rigid edit-compile-run cycles in modern compiled languages resonates with many developers who have experienced the limitations of the latter.

Yeah, I have no idea why developers still put up with the edit/compile/run cycle, like we’re still running batch jobs on an IBM mainframe.

3 Likes

There's a version for you too @Slava_Pestov; YMMV.

2 Likes

While we're here, I cannot help noting that we're also stubbornly stuck with the idea of representing structured code as unstructured text data.

Having to parse text to recapture the underlying syntax tree is a major limiting factor to development ergonomics & productivity.

We need to break the shackles of the Chomsky hierarchy. We need to throw away our typewriters.

The act of coding must be liberated from the rigid confines of a text editor. What does coding look like when the input device is a finger touching a piece of glass? What if all we have are fingers moving around in thin air? It’s 2024 — I’d expected to be able to touch code by now.

13 Likes

Man, now I really want a mutable value semantics version of Factor.

Didn't we have this in 1989 with WYSIWYG a.k.a "Interface Builder". In a sense SwiftUI which you could describe as a form of compiled DHTML (without stylesheets) has chained us to our typewriters ever more.

My point is we're not writing programs that run and exit any more but apps you launch that persist, waiting around for input. There's no excuse for them not to be able to modify themselves on the fly rather than the re-compile/re-launch/re-navigate-to-where-you-were-working treadmill we're on at the moment. That isn't going to scale. It already isn't scaling.

3 Likes

I haven't looked into this closely, but my guess is the reason is efficiency. The languages you'd like to see like Smalltalk lost to languages like C, at least in part because the latter compiled to efficient native code and produced programs that ran much faster, with some devs sticking to assembly and even refusing to use C back in the day.

While you and Bret Victor are no doubt correct that your preferred languages are easier for devs to prototype with, most users don't want to script or modify their software in any way, so the speedier languages that don't do that won out.

The ideal would be a language where the developer could do everything you ask for while developing, then flip a switch for the final release and produce a fast, efficient binary. Swift has an interpreted mode that could enable this, I just don't know if anybody will put in the effort to fully build out the kind of IDE you're looking for.

4 Likes

Apologies, I introduced a red herring talking about "the future is translated" and Smalltalk though I believe that to be true as processing power increases particularly if you retrofit some form of stronger typing e.g. TypeScript.

In a sense, Smalltalk didn't take off because it was so far ahead of its time. There were other issues like there weren't intermediate representations (you could call them source or object files) and all you could do was roll out all of virtual memory then read it back in to pick up where you left off making collaborative development difficult (IIRC).

The implementation I outlined using dynamic loading and patching in implementations gives you the best of both. Fast, compiled, strict code with the flexibility of a form of dynamism inside the existing IDE paradigm. We simply don't have to be restarting our apps and completely relinking them to change the value of a String constant or tweaking the implementation of a function.

You don't have to rewrite the IDE to do this as you can use file watchers etc. but integrating it into the IDE with the support of a trillion dollar company would give it credibility. There are limits to what can be supported but the technical issues (even if Swift wasn't designed for it) are not insurmountable. Heck, there was even a solution for .NET:

2 Likes

They got it: "Compiling!"

I mean, Common Lisp implementations have had this for a while, but combining this with static typing is the dream.

Instead of the Lisp style “image” where the compiler and environment run in the same address space as your program, I think a modern re-imagining of this looks like this:

  • a “server” mode compiler caches all intermediate computations, so that changing a single function in source does the minimum work to bring the server into a consistent state;
  • some kind of incremental linker/loader for dynamic replacement of compiled code while the program runs;
  • for deployment, you just run the usual batch tooling to generate an optimized standalone binary.
8 Likes

It doesn't need to be very involved if you use a single source file as the compilation unit. As InjectionIII has rather a lot of "technical debt" there is a new minimal "reference" implementation.

Adding this small package to a project is a sufficient implementation. When the app launches it:

  • starts a file watcher and backdates it to locate the most recent .xcactivity build log.
  • When a source file changes it greps the build log to find how to recompile the file.
  • Links the resulting object file into a dynamic library and loads it.
  • Scans its symbol table for symbols starting $s and ending F and interposes them.
  • Non-final class vtables also need to be updated.

1300 lines of Swift. Xcode Debug and Release builds continue as they would normally.

4 Likes

So, we want a Swift version of HyperCard that can statically compile the whole thing when you're done prototyping? Or is that just me?

2 Likes

HyperCard aside, Yes, IMHO that's exactly what people should expect.

1 Like

I shudder at the millions of dev hours collectively lost for not having this.

InjectionIII is proof that it is very much possible today, and I imagine the folks who pushed for @_dynamicReplacement will immediately recognize this as a great articulation of the burning problem that it is.

I desperately wish that they'd just open up the build service protocols/deeper hooks into Xcode to let us build our own implementations.

The effort with making this a reality today is not a massively technical one, but a practical one. Free us from private SwiftPM forks and private build service protocols for the one IDE we're all bound to (for iOS developers at least, IntelliJ chose to sunset AppCode for a reason and Visual Studio Code <> Swift just isn't up to the mark).

Relevant: xcbuildkit

4 Likes

Your milage will vary indeed. The Swift project evolves and I've had to re-visit Compilertron and update it after taking a fresh clone of the swift project. It's now a lot easier to setup and use thanks to yet another variant on Injection that taps into SourceKit logging to know exactly how to compile a file. To get started:

  • Apply a small patch to the swift project and build a toolchain.
  • Generate Swift.xcproject and LLVM.xcproject files.
  • Use the InjectionNext.app to launch Xcode to edit these projects.
  • When you save a file it recompiles and will be injected next time you use the toolchain.

This means the turnaround for injecting a debug print or fix into the compiler when testing it inside Xcode is 10 or so seconds, down from a couple of hours to rebuild the toolchain. This also works for local iterations on the compiler using ninja in a terminal and when running tests.

3 Likes

Thanks for your "manifesto",I gained a lot.