Subtext was a series of experiments to radically simplify application programming. It was not no-code or low-code but yes-code: a general purpose programming environment simplified the hard way: inventing new ideas and recasting old ones. Subtext was a protest against the mind-boggling tech stacks that have taken over programming. It was inspired by the classical era of LISP and Smalltalk and Visual Basic and HyperCard when programming was more humane and new ideas were more welcome.
This paper introduced many of the themes of my research that continue to this day. Inside Eclipse, a Java interpreter traced the execution of a Java program on examples, which were typically simple method calls. The trace was presented as an outline showing the result of all expression evaluations and method calls, which could be expanded as indented traces. Selecting lines in the trace would highlight the corresponding code in the source view.
The interesting idea was that these trace outlines could be persistently annotated to inject behavior into the execution of that example. One such annotation is to make an assertion, turning the example into a test. An actual observed value in the trace could have an assertion added to it checking that the same value was always produced. Red/green highlights showed the results of these tests
Annotations could be made deep within the expanded traces of nested calls, and would apply only on that specific path in the trace, not other calls to the same methods. Annotations were stable across editing of the source because they used Eclipse's stable code markers, though some edits could create orphans that remained as errors in the trace until they were fixed or deleted. Subtext later adopted ubiquitous permanent IDs for edit stability.
In addition to assertions there were a number of other kinds of annotations explored. One was an override that would set a variables value at that point in the traced, overriding its computed value. Override also function like assertions, turning red if the computed value is different. Overrides supported the methodology of Example Driven Development, where you start with an empty stub method with examples, override each example to produce the expected result, then add the correct code outside in to satisfy the annotations. Two code transformations supported example driven development: conditionalisation and generalization that induced stub conditionals and method calls into the source, automatically decorated with overrides and assertions in each example to match that examples behavior, so that the stubs could be filled in while constrained by testing their behavior in all examples.
I have repeatedly returned to idea of materialized execution in a tree inlining calls and unrolling loops, rendered as an indented outline. Materializing execution provides a form of omniscient debugging. Another theme is to edit concrete executions to inject tests and behavior, and use code transformations that abstract concrete executions into general code constructs. Later I called this gradual abstraction.
Most notable paper award 2015. ACM PDF Video
The first Subtext paper took the ideas in Example Centric Programming as a guide for a new non-textual programming language. The tree-structured execution trace became the program itself.
Subtext 1 took non-textuality so far that names were only used in definitions, not references, which were rendered graphically with a compass. I discretely dropped compasses in further research and instead used names to render references, binding them at edit time, not compile-time or run-time as in textual languages, and internally representing them as a cross-tree link not a symbol.
The fundamental primitive was copying, another major theme in all subsequent work. Variable references copied values. Function calls copied function bodies (inlining). Instances of a type became copies of a prototype, which featured in all subsequent work. These copying relationships themselves could be included in copies of their containers. If both sides of the copy were in the same container, a corresponding new copy was made. But if the source of the copy was outside the container then the new copy inherited the same source. This is a nice reformulation of lexical scope I have used ever since.
From the perspective of 2025, the subsequent Subtext papers and experiments were mostly working out the details and consequences of the foundational ideas in these first two papers.
Rejected paper published as a tech report. It tried to formalize the theory of copying from the first Subtext paper.
Subtext 2 introduced a new representation of conditional logic called Schematic Tables. Like decision tables, columns were expected to logically partition all possibilities. Maintaining this partitioning drove useful code refactorings. I also first started using variants of Miller columns to drill into hierarchical structures. Schematic tables had some nice properties, but I haven't been able to fit them into any of my subsequent research.
Another rejected paper that tried to apply the theory of copying to the problem of customizing generated code.
After schematic tables I turned to a series of experiments working on the hard problem of ordering state mutation. This paper was a naive first cut that I dropped when I realized it was irredeemably nondeterministic.
The problem is that asynchronous state mutation causes many nasty anomalies, sometimes called glitches. UI callbacks are infamous for these problems, but even single-threaded programs have these problems, because state mutations hidden inside called procedures are "conceptually" asynchronous. It would be great if we could statically analyze all effects and come up with a safe glitchless execution order. Or build a safe-by-construction order into the PL. The problem with this line of research was that only experts who had experienced these problems took them seriously, as simple examples seemed contrived and easily worked around. But these same experts had no interest in solving the problem with a new PL, which was seen as almost heretical. Thus we end up with React, which was explicitly motivated to solve glitches, but did so at the library level, requiring programmers to master and meticulously obey coding conventions to not shoot themselves in the foot.
I spent years on Subtext 3 and 4 wrestling with glitches. The basic idea was that since subtext is a stateful tree (with sub-trees that are derived by formulas on the rest of the tree), updates could be ordered as a single wavefront backwards through formulas and up the tree. Every tree location could be updated only once per cycle, like in Synchronous Reactive Programming. I got it working but it was kind of weird and I found it hard to to convince anyone it was worth adopting a whole new programming paradigm to solve the problem.
The lesson I took from the technical difficulties of Subtext 3 and 4 was that I needed something like a static type system with what we now call an effect system, but without loosing the development flexibility of dynamic types. Video: type as subtext Video: Two-way Dataflow
I still like this idea of a type system that acts statically or dynamically depending on the context, though now I see the contexts as user mode and developer mode.
Subtext 6 set aside the problem of mutable state and turned to demonstrating the end-user benefits of Subtext in the form of a mobile programming app for building custom social apps. The programming experience was pretty much a failure. But I still like the idea of Social Datatypes that capture common units of functionality like polls or comment threads that cross-cut the PL/DB/UI stacks and are aware of user identity.
Subtext 7 dug into minimizing and gradualizing abstraction. I returned to the well of Subtext 1 with materialized execution and interventions into concrete example execution traces that scaffolded gradual abstraction of code.
The code in Subtext 7 was functional, which increased the abstraction gradient that had to be crossed. Subtext 8 turned to imperative programming as more concrete and intuitive. The goal was to make everything a program could do also be doable by the user as a direct manipulation, which naturally led to all actions being imperative. The principle of program-user equivalence supported Programming by Demonstration where the history of user actions gets lifted into a program with matching semantics. This principle was taken so seriously that even math was imperative like in machine code when you add a number into a register.
I also returned to the idea of Managed Copy & Paste Video. Actual programming involves a lot of copying and pasting and customization. We are told Don't Repeat Yourself and to use proper abstractions instead. Rejecting that idealism, we should embrace copying and build it into the language and programming environment. Copying should be reified in the program. You should see how copies have diverged and (partially) resync them. But this is really hard, and especially hard to make simple. I have repeatedly attacked this problem with limited success.
Subtext 9 was a tangent producing no paper or video, just unrecorded demos and a blog post. It had the interesting property that conditionals were represented in the materialized execution as sum types, which is pleasing because they are two different ways of representing choices, in code and data. If code is data, and also the meaning of code is data, then conditionals ought to be sums. I demonstrated this with parsing code, where the execution of the parser was itself an AST. As with the earlier schematic tables this is an intriguing standalone result that has been hard to integrate into a full system.
In the pandemic I returned to the good old problem of mutable state. I also wanted to produce a somewhat complete language design, with a design rationale of which the most theoretically interesting bit may be Feedback, a semantics of glitchless bidirectional programming. This was a headless implementation with a conventional-looking syntax. I have come to a more nuanced view of syntax: it is a very effective form of communication in small quantities, especially in textual contexts like papers and docs.
I posted a Postmortem.
It is fair to say that Subtext was a series of overambitious failed experiments. I was trying to invent too many things at the same time. There was a reason for that: I believe programming is trapped in a local maximum that we can not escape by varying one dimension at a time. But it was just too hard: I kept running into tar pits of interlocking hard problems with high-dimension spaces of solutions. Nevertheless there were some good ideas, and I did make some progress on them. I am thankful that some people found it interesting and offered encouragement along the way.
I think what was missing was an underlying theory to map a path through the tar pits, like PLs have lambda calculus and type theory, and DBs have relational algebra. Perhaps the central insight of Subtext was that programming is about change: changing data at runtime, and changing programs at edit-time. Most of the new ideas in Subtext were about managing data change, and reimagining the nature of code so that it could be more easily changed by humans. What Subtext needed was a Theory of Change. Thus Baseline.