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

dracula

@dracula

Joined May 31st, 2026

  • 1Devlogs
  • 1Projects
  • 0Ships
  • 0Votes
Open comments for this post
Reposted by @dracula

2h 25m 43s logged

v1.4.4 Devlog: Giving Flower Real Semantic Union Types

I made it so that semantic unions are now in Flower’s parser and type system — but not yet its codegen.

The goal was to support types like:

value: int | string
next: @Node | null
type Result = int | string

What’s new

The parser now understands | inside type positions and builds semantic union type information instead of relying on those old plain aliases or backend-shaped hacks.

On the typecheck side, unions are now carried through normal type copying and alias resolution. That means named aliases like:

type MaybeName = string | null

make sense after resolution, instead of collapsing into nonsense.

The typechecker also now accepts union-compatible usage in the places that matter for a foundation:

  • variable initialization
  • assignment
  • parameter passing
  • return checking
  • alias resolution

So if a value matches one union member, it is accepted. This is sorta implicit behavior, but I think it gets a pass due to its somewhat explicit nature?

What it does not do yet

This version was intentionally strict about using unions as values.

You cannot yet directly:

  • index a semantic union
  • access a field on a semantic union
  • use arithmetic / comparison operators on one
  • print one directly

Instead, Flower now requires an explicit cast before using a union value that way.

That may sound harsh, but it is the right restriction.. for now. There is not any narrowing yet, so pretending unions were fully usable would just make the language lie and spit out 2,000 errors.

Where this leaves v1.4

Flower now has:

  • transparent type aliases
  • null as a real type-system concept
  • pointer-first nullable types
  • semantic union type representation

The next step is the part that will make unions actually usable rather than merely legal:

  • narrowing with is
  • better explicit extraction with as
  • eventually making ?T truly become T | null instead of remaining pointer-first for now

No fun screenshot this time :p just frontend work for now…

Ivy, signing off <3

2
1
46
Open comments for this post
Reposted by @dracula

8h 15m 31s logged

Devlog: Making Flower’s Type Tracking “Enough” for Stdlib / String Work

The original motivation was simple and — supposedly — quick; I wanted to move on to v1.1.1, “stdlib basics,” especially string utilities. But Flower’s current compiler did not really know what types expressions had. It mostly had pointer_depth on declarations, which was enough to generate * in C types and sometimes guess whether field access should become . or ->.

That worked for very simple cases:

p: @Vector2 = new Vector2
p.x = 5

But it broke down for nested access:

outer.inner.value
outer.inner_ptr.value

The compiler could see the syntax chain, but it did not know whether inner was a value field or pointer field. Since Flower source only uses ., the C backend needs to decide whether each hop becomes . or ->.

That meant stdlib work was blocked. If I want something like:

if strings.equal(string_a, string_b):
    print("same\n")
end

then Flower needs to understand that string_a.length, string_a.data, etc. are real typed fields, not just random syntax to dump into C.

Originally I thought I could just simply create a Symbol Table to get that to work. Unfortunately, that plan soon failed as it introduced a mariad of issues relating to the import system. At the time, this frustrated me because I knew the import system sucked, but I didn’t want to fix it until a later version.

Eventually, I settled on a specific plan which did actually end up changing how imports are handled.. kinda.

The Plan

The design I landed on was to add a typecheck/type-tracking stage between parsing and codegen:

lexer -> parser -> typecheck -> codegen

The parser should stay mostly syntax-only. It builds AST nodes like “dot access,” so I didn’t really change it.

The typecheck pass now collects declarations into a TypeEnv:

struct TypeEnv {
    structs: StructInfo[1024],
    struct_count: int,

    vars: VarInfo[8192],
    var_count: int,

    funcs: FuncInfo[8192],
    func_count: int,

    error_count: int
}

Then it walks expressions and annotates dot access nodes with an access kind:

ACCESS_UNKNOWN: int = 0
ACCESS_DOT:     int = 1
ACCESS_ARROW:   int = 2

So codegen no longer has to fully guess. It can do:

if ast.data._dot_access.access_kind == ACCESS_ARROW:
    fprintf(out, "->%.*s", ...)
else if ast.data._dot_access.access_kind == ACCESS_DOT:
    fprintf(out, ".%.*s", ...)
else:
    // fallback
end

That fallback is important because Flower is self-hosted-ish right now. The compiler is written in Flower, and the compiler’s own source code is full of field access. If the new typechecker is too strict too early, it breaks the compiler while trying to compile the compiler. Very elegant. Very cursed. Just how I like it ;)

Import-Aware Type Tracking

At first, typechecking only saw the main file, so I added a module loading pass. Instead of codegen discovering and parsing imports late, the compiler now loads modules earlier:

load main module
load imports recursively
collect declarations from all modules
typecheck all modules
codegen all modules

That required a new module structure:

struct Module {
    path: @char,
    src: @char,
    tokens: @TokenStream,
    ast: @AST
}

struct ModuleSet {
    modules: Module[128],
    count: int
}

Unfortunately, it didn’t go as smoothly as I wanted. Near the end when I was sure everything would work, I ran into an Idempotency failure due to the new handling of system imports. I.. just manually passed it :p

4
1
357

Followers

Loading…