What if I could remove a language feature and the runtime work it requires together?

I'm exploring an array language of my own alongside the Go interpreter. With Go I have rules to implement, here I get to choose them, which is very tempting when I've been reading the assembly needed to keep those rules working.

I'd write readings * 2 to double every value in a column of readings, describing work across the array and leaving the compiler to arrange the loops and storage. That's the level I'd like to write the calculation at.

A program also checks indices, tracks references and preserves values across calls because the language makes those promises. Which promises would I keep in a language of my own?

The attraction for me is predictable lifetimes, fewer trips through memory, and results I can reproduce. One of my early wishes for this language is to replay an execution and audit it, with the same bits in the result on different computers. I have to design for that, writing “deterministic” on the wish list won't make it happen.

“No surprises” would be an excellent feature, I mean, it's a great song. Regrettably, it needs a more precise specification.

Every omission needs to buy something

If I leave out user pointers, there are fewer ways to keep another reference to the same value, an alias. Leave out shared mutation too, and readers and writers can't race over a shared update. I'm also interested in a dataflow graph because seeing which calculation needs which value might let the compiler find its last reader.

Crossing features off is quite satisfying, I can make the runtime sound wonderfully simple while there's nothing running on it. An algorithm that changes linked nodes would need another way to represent them, though. Ordinary calculations will tell me which features I start wishing I'd kept.

The remaining work has to go somewhere

Removing user pointers narrows aliasing, but containers can still retain values. To omit a collector, I need to know when those values are finished with, which makes last uses in a dataflow graph a question worth trying next.

I'm not yet sure how much work that removes and how much it hands to the compiler, or worse, to whoever writes the program. A smaller runtime won't feel like much of a gift if using it becomes a chore.

An omitted bounds check leaves a similar question. What establishes that the access is valid? Putting the choices together helps me see what I've actually removed:

What I leave outWhat that could saveWhat I still need to solve
User pointersSome aliasing complexityContainers can still retain values
Shared mutationSynchronising shared writesExpressing algorithms that depend on those updates
A garbage collectorRuntime collection workKnowing when storage can be reclaimed
Bounds checksChecks during executionEstablishing valid accesses, or accepting an explicit unsafe contract
Figure 1 - What each omission leaves behind.

What interests me is how the restrictions support each other, because if the compiler can see who reads a value it can look for its final use. If it can also keep the next calculation near that use, an intermediate array may never need to be written down.

Can a small graph of ordinary calculations tell me when its storage is finished? That gives me a place to begin.

This entry came over from blog.haddon.me. I’ve tidied up the grammar and flow, bringing it closer to how I write now. I used AI to help with the grammar, structure and flow. I have dyslexia, and my writing often comes out as a rambling scrawl, so it can be hard to see what I was actually working on at the time. I think the tidied versions are easier to digest and more interesting to read. The artefacts got the same tidy-up while I was at it.

  • Related article

    Keep the data moving

    Why I want Floatyboat to keep tiles moving, and what happens when an operation needs a whole array.