A Lifetime That Is Not a Scope
A subscription leak in a small app turned into a question about the language, and the answer was no. Two starting premises — faster is better, do not depend on a collector — were a purchase, and the things Mere cannot do are the invoices arriving one at a time.
A demo app grew a tab bar. Two views over one store: a form that builds its controls from a schema, and a read-only summary. Switching tabs drops the old view’s DOM wholesale, which is the easy part. Dropping the nodes did not drop the watchers. Three round trips between the tabs left eleven watchers running, nine of them faithfully painting into nodes that were no longer in the document. A counter in the tab bar is the only reason anyone noticed.
The fix was ordinary: the subscribe call returns a token now, there is a matching release, and a view collects its tokens and hands them back on close. Eleven became two, and stays two however far you navigate.
The interesting part is what happened when I asked whether the language should have prevented it. The answer was no, and the no is worth more than the fix.
The one mechanism, and why it does not reach
Mere has exactly one way to enforce release: a drop type, bound by with, whose close runs
when the scope ends. That is a good mechanism. It is also the wrong shape for a subscription.
A view is built during one event and torn down during another. By the time a with block
would close the handle, the view has not even been shown yet. The lifetime that matters here
is not the scope that created it — it is spread across two separate turns of an event loop,
with an indefinite gap in between that is entirely up to the user.
There is a second, quieter obstruction. A drop type cannot be placed in a region, and the watcher list lives in a region. So even if the timing worked, the storage would not.
So release stayed a convention, written down in the library’s README instead of papered over with a mechanism that does not fit. I have come to think that recording a negative answer plainly is one of the more useful things a language project can do, and one of the easiest to skip.
Two premises, paid in advance
It would be easy to file this under “missing feature”. It is not one. It follows from where the language starts.
Mere begins from a small set of premises, two of which do most of the work: if two programs compute the same thing, the faster one is better, and anything resolvable at compile time should not be carried into run time; and memory should be managed without depending on a collector, with ownership and lifetime tracked statically instead.
Those are not features. They are a purchase. What they buy is real — abstractions that cost nothing when unused, no pause times, a value model small enough that the same language reaches from a browser tab down to a bare-metal RISC-V core with no operating system underneath it.
What they spend is dynamism, all of it, up front. Every later limitation is an invoice for a purchase already made. The watcher case is one invoice arriving in a form specific enough to be surprising: a lifetime that no scope contains, in a language whose only enforcement mechanism is scope-shaped.
The larger invoice: everything must be present at compile time
The second consequence is bigger and less visible day to day.
Generic dispatch in Mere resolves through types, which means a generic gets realized per instantiation, which means realizing it requires the definition body. That single fact propagates outward further than it looks like it should.
It means there is no stable interface across a generic boundary. No stable interface means no loading code the compiler did not see: no plugins, no swapping an implementation after the fact, no host program that accepts an extension written later by someone else. The usual place a language earns its keep as an embedded scripting layer is precisely this slot, and Mere cannot occupy it. Not yet, but structurally.
It also means size and compile time scale with the number of instantiations rather than the amount of source. Two measurements from this year make the shape concrete. The self-hosted compiler, lowered to flat RV32IM machine code, is about 380 KB — an entire compiler, but also an entire compiler with every generic it uses realized in place. And mere-ruby, the Ruby subset interpreter written in Mere, spent a while with a dozen gems segfaulting for reasons that looked like a memory bug and turned out to be program size: it needed to be linked with a 128 MB stack before they stopped. Same bill, delivered twice, in two currencies.
Cycles need a design, not a keyword
The third consequence is the mildest, because there is at least a mechanism for it.
Ownership plus regions means that anything cyclic or self-referential needs to be arranged
rather than merely written. Graphs. Parent pointers in a tree. Doubly linked lists. An
observer holding a reference back to what it observes. Mere’s answer is a bundle type built
inside a region — immutable, non-moving, able to express self-reference without reaching for
unsafe. It works, and it is a design you have to do, every time, where a collected language
would let you type the obvious thing and move on.
I list this third because it is a tax rather than a wall. But it is the same tax, from the same purchase.
Five backends leave no room for a lighter self
Languages with expensive premises usually acquire an escape hatch: a second implementation, lighter and more dynamic, that gives up the guarantees in exchange for reach. It is a common enough pattern that it is worth checking whether it is available here.
It mostly is not, because the main line already absorbed it. Mere has five backends — an interpreter, a C backend, LLVM, WebAssembly, and direct RV32IM machine code. Fast startup, small targets, the browser, bare metal: all of that is in the primary implementation, not in a sibling project. There is no obvious room for a smaller Mere, because the small Mere is already the same Mere.
What is left, if anyone wanted it, is not an interpreter. It is a Mere with a collector, with type erasure instead of per-instantiation realization, and with separate compilation across generic boundaries. That version could load plugins and could free a subscription whose lifetime no scope contains. It would also have abandoned both premises the language starts from — which makes it close enough to a different language that calling it an implementation would be a dodge.
That, I think, is the useful thing to notice. These three limits are not a backlog. Nothing is going to arrive that removes them while leaving the rest intact, because they are not gaps in the design; they are the shape of the choice, seen from the outside.
Which is also why writing the negative answer down was worth the effort it took to be sure of it. A specification is not only what a language promises. It is also what it declines, and how early it declined it. The eleven watchers were cheap. Discovering the same no a second and third time would not have been.