Go and C don't give me a sum type declaration. I can reach for records and tags, a C union or a Go interface. Those are the pieces I'm used to thinking with. With Floatyboat, my array language, I'm also trying to limit how many features I add. Each addition is another thing to implement and another thing somebody has to learn. (And when I'm the one implementing it, keeping that list short appeals to me.) So I've been keeping sum types out. Writing the language's own compiler is making that position harder to hold, though.
Which case is this? A compiler cell might be a token carrying an index, a binding carrying a name's index, or a return marker carrying nothing, and a sum type gives that fixed set of cases a common type name. The declaration names each alternative and its data.
I can already lay that out in arrays, giving each cell a tag identifying its case and putting the cases' fields in parallel columns held together by a record. Masks can mark which entries contain usable values. I don't need a new kind of storage.
I dismissed the proposed sum type as a validity-mask record “wearing a costume”. If the existing records can carry the information, why introduce another declaration for it?
This fits comfortably with my enthusiasm for leaving features out. I know how to read the record, and the compiler already knows how to process its columns. A new spelling has to save me something beyond a few characters.
The compiler has its own version
The stream inside Floatyboat's compiler has these four cases. I've packed them into one numeric column:
| Entry | Encoding |
|---|---|
| Source token | its nonnegative token index |
| Binding | -canon - 2 |
| Return | -1 |
| Padding | -1000000 |
Here, canon is the index of the canonical token that identifies a name consistently. A binding with index 12 becomes -14. Readers identify the case from the number's sign and range.
Recognisers accept bindings strictly above -500000 and below -1.
Making the compiler's expanded input longer exposed a problem. Names near the tail of the compiler stopped resolving, nowhere near the source edit. Their binding entries weren't recognised any more.
At canonical index 499,997, the encoding gives -499999, which passes the test, but advance the index to 499,998 and -500000 no longer passes. The actual padding marker's still -1000000. I've lost the binding long before reaching it.
My compiler has acquired a source-size limit. That number looked comfortably far away.
That's annoying when I'm only trying to change how something is written.
The immediate repair derives the binding floor from the token count. That fixes the arbitrary cutoff. Changing a comparison doesn't need a new language feature, which would be a fairly expensive response to this bug alone.
But I keep coming back to what each reader has to know. A binding is one kind of entry, its index is data, and I've made the same number do both jobs. Four recognisers have to remember the encoding. Adding another case means finding all the places that interpret it. An ordinary record with a separate tag would get me out of the numeric ranges too, holding the fields while I assign tag numbers and write comparisons against them. That's a real alternative. The language wouldn't have to change. Then I'm responsible for keeping those tags and fields together in every constructor and reader.
Which fields should a binding have? Who fills the unused fields? Does this number mean a case or an index? I'm already maintaining the answers, but I can't ask the compiler to check them without a declaration.
Let the cases say what they are
What would I rather write? Something that names the cases and their fields:
type Cell variant {
Tok { index Float }
Bind { canon Float }
Return
Pad
}
These numeric indices live in floating-point columns in the compiler. The values in this example are exactly representable as Float.
Construction names the case and supplies its fields. Return and Pad don't need constructor fields. Their tags tell them apart. Recognition asks about the case directly:
cell := Cell.Bind{ canon: 500000.0 }
isBinding := cell is Cell.Bind
Now the compiler can check that I've supplied the fields belonging to Bind and write the tag for me, so the case test becomes a comparison with that tag. Increasing canon doesn't change the kind of entry I've constructed.
Anyway, the columns can stay, which is the part that makes this acceptable to me. Cell still becomes a record with a tag and the fields belonging to its cases. The cases are numbered in declaration order:
| Case | tag | index | canon |
|---|---|---|---|
| Bind | 1 | 0 | 500000 |
| Tok | 0 | 12 | 0 |
| Return | 2 | 0 | 0 |
| Pad | 3 | 0 | 0 |
Over many cells, those fields become parallel columns, as they do for other records. The parts of the compiler that operate on those columns keep working with records, so I've added decisions to the frontend without asking the rest of the compiler to learn a different container.
Unused fields have defined zero values. Reading index from a Bind gives zero, and I don't need a pattern match before every field read. That doesn't make it a Tok. Its tag still says Bind. A case test supplies a mask for selecting the bindings, and that mask works with the column operations I already have for those cells.
I find this much easier to read than the negative-number test, but being able to change the code using Cell without reconstructing that agreement matters more to me. The declaration holds information that was previously scattered across the readers.
More tag comparisons, another reader to update.... I'm realising how much code I'll write to avoid adding a language feature, and it's mildly annoying to be persuaded by my own example when I'm quite fond of leaving this out. I've avoided a language feature, or rather, I've left some of its rules for my compiler's constructors and readers to maintain. Apparently I've decided that's the simpler option.
So the costume is staying. I was looking at whether the existing data structures could hold the information, and they could. I hadn't given enough weight to every caller having to maintain that convention.
I'm making room for named cases and their fields. The existing column layout stays, and I can replace the range tests in my compiler with names I can read. That's a small enough addition for me to get comfortable with it.