You are browsing as a guest. Sign up (or log in) to start making projects!

4h 52m 31s logged

Devlog: Making ?T Actually Mean T | null

Before v1.4.10, ?T was treated as its own nullable system and not really integrated with the rest of the semantic unions. This was unideal, because it created a separation of logic which was cumbersome to maintain.

The idea was to make the following be the default:

?int     == int | null
?string  == string | null
?@int    == @int | null

Prior to the refactor, nullable types were still mostly pointer-oriented. That worked when ?T basically meant “maybe this pointer is null,” but it did not really fit anymore once semantic unions became actual runtime values.

If Flower has T | null, then ?T should use that instead of being its own special thing. This is a more pragmatic choice, as it keeps the compiler easier to manage and update later on.

What changed

Typecheck now lowers ?T into the same semantic-union machinery used by normal unions.

That means nullable values now go through the same general path as union values:

  • null checks use union-aware narrowing
  • narrowed nullable values can be extracted with as
  • codegen emits tagged union storage
  • assignments pack into the right union member

So this now works as an actual nullable flow:

maybe: ?@int = raw

if maybe == null:
    return false
end

real: @int = maybe as @int

The annoying part is that Flower has to track two things at once: the type the programmer is allowed to see after narrowing, and the actual union shape that remains underneath it.

After the guard, maybe behaves like an @int.

But codegen still has to remember it came from @int | null, because apparently the compiler cannot simply “vibe” its way through storage layout. Tragic!

The bugs that annoyed the heck out of me

Most of the work was not surface syntax. The syntax was just standing nearby looking innocent.

The actual bugs were in the places where Flower knew “this is non-null now,” but then forgot how to lower, such as:

  • narrowed nullable values falling back to raw C casts like (int*)(maybe)
  • false branches of == null / != null narrowing to the wrong side (this was my fault lol)
  • null getting packed into the wrong union member

That last one was the rude one.

For ?@int, generated C could end up doing something like:

.tag = 1, .m0 = NULL

instead of packing into the actual null member.

So the value looked almost right, but the tag/member pairing was wrong. Then later null checks would fail, because the compiler had technically put null somewhere. Just not the correct somewhere. Very helpful.

The fix was separating two questions I had accidentally merged:

  • is this type compatible with this union?
  • which exact union member should this value occupy?

Those look related, but they are not the same question.

Flower was using the first answer for the second problem, and then acting surprised when everything became soup.

Where this leaves Better Types

?T now lowers through the same path as T | null, which means null checks, narrowing, extraction, and storage all line up with the semantic union model.

This should make later work cleaner, such as nullable inference, nicer union ergonomics, maybe pattern-like narrowing eventually.

Not easy. I am not saying easy near a compiler. That is how they hear you. You do not want them to hear you..

0
7

Comments 0

No comments yet. Be the first!