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

IvyMycelia

@IvyMycelia

Joined June 3rd, 2026

  • 29Devlogs
  • 3Projects
  • 2Ships
  • 30Votes
✦·˚ Ivy Mycelia ˚·✦

“When a peel bites your thigh, that’s a moray.”
Open comments for this post

4h 6m 25s logged

Devlog: Documentation and v0.1.0

Date: 2026-07-22

Instead of jumping right into code — like I normally do — I decided to instead write documentation on how the project would be handled from a development standpoint. This included deciding on things such as Semantic Versioning, Conventional Commits, short-lived branches, and squash merges. None of it was particularly original, but the point was to remove any ambiguity regarding the actual developm from the actual work. I no longer need to wonder whether a feature belongs directly on main, what a release branch will be named, or whether generated binaries should be comitted.

I also decided that CHANGELOG.md would remain at the root similar to the README, and other policy documents / actual project-facing documentation would live under docs/.

The first code

The original CLI I wrote walked over every argument and then tried to interpret options and commands inside that very same loop. This causes conflicts and issues, especially relating to reading a filepath that might not exist in argv.

I replaced it with the rule that one invocation has one command, and everything after it belongs to that command.

The accepted shape thus is now:

    bioflo <command> [arguments]

Commands may officially be written with zero, one, or two leading dashes — though any amount of dashes is current accepted. Dash removal is bounded rather than handled by an open-ended pointer loop, so inputs such as - and -- cannot be walked beyond the end of the string.

Testing this produced a strange error where for whatever reason my system got stuck on a kernel write request, which froze all other kernel requests and muddied any other programs from running in my terminal. I restarted my computer and that was that. Really annoying because for like 20 minutes I thought the issue was me. It was not.

Sequence decisions

The first actual library module handles basic DNA sequences.

Currently, a valid sequence can contain A, C, G, T, and N. I chose to accept N because unknown bases are common in real sequence data — from personal experience. Lowercase input is rejected rather than normalized implicitly purely just because I haven’t gotten to it yet. Empty strings are invalid sequences, but I did make it so that GC content for an empty string returns 0.0 to avoid division by zero.

GC content is returned as a fraction between 0.0 and 1.0, not as a percentage. This is to keep the value reusable rather than prioritize presentability.

I deliberately kept the public API small for now as to avoid overextending the first release:

is_valid_dna
count_nucleotide
gc_count
gc_content

The base-checking helper I have kept private. This is my first attempt at treating prop as an explicit public-facing endpoint rather than using it for everything.

What v0.1.0 means

Version 0.1.0 is by no means a feature-complete release (obviously). It’s my first steps at not only making a specific library for bioinformatics, but also testing Flower’s ability to even create usable libraries.

The CLI still does not read files, FASTA parsing does not yet exist, and case normalization / additional ambiguity codes are also deferred.

In the coming planned work, I intend for Bio-Flow to reach a somewhat usable state in the hopes of utilizing it for some of my own personal projects :)

0
0
23
Open comments for this post

9h 11m 33s logged

Devlog: v1.5.1 – v1.5.5

Following all of the changes Flower has undergone, I felt it was time for some much-needed documentation. On paper this was supposed to be fairly quick, simple, and not require much brain power. Unfortunately for me I got a bit too over-zealous, and included things I thought should work, but in reality hadn’t.

For example, in a lot of the code snippets I’d have something like:

for i in range 0..10:
   print(i)
end

Printing non-string values was not yet supported in Flower. To fix this, I took a small detour back to v1.4 and implemented support for it — mostly in a single codegen case:

if ast.data._print.value_type.base == TOKEN_FLOAT or ast.data._print.value_type.base == TOKEN_DOUBLE:
   fprintf(out, "printf(\"%%f\", ")
   gen_expr(ast.data._print.value, out, src)
   fprintf(out, ");\n") 
end

Afterwards I added more smoke-tests in examples/io/print.flo. Every few runs, however, I’d get a segfault. This was especially odd because it wasn’t simply an error but rather a silent memory bug. Eventually I found out where it was originating — thanks, ASan! Turns out after the new type system changes, I had forgotten to initialize some values to null / 0 in for loop handlers and a few other places. I just added typeInfo.data = null / 0 where applicable and it fixed the issue!

What is v1.5, and why is this devlog so long?

To answer the latter first.. I lowkey just forgot to post a devlog for the first two docs I made. That’s it. For the former question, v1.5 is supposed to be explicitly a documentation pass.

First thing I worked on was structure. I cleaned up the docs layout, moved things toward a more consistent lowercase docs/ tree — I had been trying for so long to do that and I just finally got it to work — and wrote down the documentation process itself so new pages aren’t just floating about. The point was to try and make the docs feel like a book instead of a folder full of unrelated notes.

I wrote and checked the language pages for types, functions, and control flow by reading the compiler itself, not by trusting my (poor) memory. That meant checking parser rules, typecheck behavior, codegen boundaries, and example files before writing anything solid. Types especially needed this, given how Flower now has enough real type machinery that the docs can quickly go from being coherent to completely incorrect: nullable sugar, semantic unions, explicit casts after narrowing, alias behavior, current member limits, and the difference between what the language wants to become and what the compiler supports currently.

Functions and control flow had a similar issue. I had to document prop, return rules, direct call forms, logical conditions, range-based for, and the rough edges like break and continue not yet being enforced cleanly as loop-only. That kind of thing is small until someone learns the language from the docs and hits the edge head-first (been there, done that).

What Now?

I’m just going to keep writing docs. Maybe do one devlog for every new doc, or a batch of docs, who knows! I certainly don’t :p

5
0
64
Ship

The compiler is written in Flower itself and currently targets C, so this release was mostly geared towards making the type system have actual Flower semantics instead of a thin little blanket over whatever the backend happened to be doing.

The big additions in `v1.4` were:

* transparent type aliases
* real `null`
* nullable types as `?T == T | null`
* semantic unions like `int | string`
* explicit narrowing with `is`
* explicit extraction with `as`

That support now works across locals, parameters, return types, struct fields, and stable field-expression chains.

The work had to touch parser logic, typecheck, narrowing, lowering, codegen, bootstrap, and then all the weird regressions those changes politely dragged into the room. It was.. a process for sure

What I’m most proud of is that `v1.4` pushed Flower toward a clearer identity that wasn't defined by C as much anymore.

This release also brought the docs back in line with the compiler, which was LONGGGGG overdue.

If you want to test it, the main things to look at are the Better Types examples and the self-hosting flow. The compiler should bootstrap cleanly, the test suite should pass, and the examples now cover aliases, nullable values, semantic unions, inference, and struct/field narrowing behavior.

For those who will be testing it, DO NOTE that I develop exclusively on my MacBook which is a UNIX system! I do not have the ability to properly develop and provide releases for Linux-specificalities or non-POSIX systems. I'm extremely sorry for this, but alternatively watch the provided video to see Flower in action :)

If you're not able to test it, watch this short (crappy) video instead: https://youtube.com/shorts/DyPtIru6cjY?feature=share

  • 15 devlogs
  • 26h
  • 19.07x multiplier
  • 503 Stardust
Try project → See source code →
Open comments for this post

34m 38s logged

Devlog: Syncing Flower’s Docs Back to Reality

Just a doc cleanup lol. No code change or compiler updates. Probably did more than needed for now but oh well!

The mismatch was REALLY bad. The old docs preserve old assumptions, and someone reading them could walk away with a version of Flower that no longer really exists (or, in some cases, doesn’t exist yet or never ever existed)!

What changed

I updated the docs that had drifted the most from reality (relatable):

  • README.md now reflects the current type-system shape
  • ROADMAP.md treats v1.4 as accomplished
  • the v1.4.0 milestone doc now includes more of the outcome, not just the original plan
  • contributor docs no longer have stale semantics
  • structure.md is more accurate to what Flower currently is, and what still needs a fuller rewrite later

Yea, that’s it.

0
0
30
Open comments for this post

1h 38m 59s logged

Devlog: Finishing Nullable Field Narrowing

At this point, Flower could already narrow semantic unions through field access in the is / as case, and nullable field checks were supposed to be the obvious follow-through:

if maybe_box.value != null:
    label: string = maybe_box.value as string
end

Instead, I had forgotten to remove a LOT of stale code, and thus part of it was still behaving like only plain local variables could ever be narrowed.

There was also a second, smaller mismatch hiding, where null in type position was not lining up with the compiler’s actual internal null representation. ?string and string | null weren’t leading to a congruent output and at times were wobblier than they should be.

So the fix was mostly cleanup, but the meaningful kind:

  • make null in type syntax lower the same way the compiler expects
  • remove the stale var-only branches in nullable narrowing
  • let stable field paths like maybe_box.value and holder.box.value narrow properly

The funny part is that this felt bigger initially than it actually was once fixed. Nothing fundamentally new had to be invented, I just had to stop the old code from interfering with the newer code, which once I figured out how to do so it was quite quick. I spent an embarrassingly long time figuring it out…

0
0
16
Open comments for this post

1h 46m 58s logged

Devlog: Letting Union Narrowing Follow Field Access

Before v1.4.13, union narrowing only really worked when the thing being narrowed had a variable name due to almost every similarity check relying on name_start and name_length.

This was dumb, because semantic unions already knew how to test tags and extract members. Codegen was not the issue, but rather the annoying part was that the typechecker could remember this:

if value is int:
    n: int = value as int
end

but not this:

if box.value is int:
    n: int = box.value as int
end

So I still had to do a little temp-variable ritual:

tmp: Value = box.value
if tmp is int:
    n: int = tmp as int
end

Which was gross. Not necessarily catastrophic, just gross and annoying. I wanted needed convenience

What changed

The compiler was remembering narrowings by variable name, not by expression shape, so box.value looked obvious in source (to me, at least), but to the typechecker it was basically some random expression and not something it could reliably recognize again later.

A variable name already had a stable identity, whereas field access chain did not. So the change was to let narrowable expressions represent more than just AST_VAR_REF.

For now, that only means stable dot-access chains.

The typechecker can record a narrowing for an expression like:

box.value

and then recognize the same expression later when a cast shows up.

So this works directly now:

if box.value is int:
    n: int = box.value as int
end

Same for the other union members, like string.

The detour

My first example accidentally wandered into a different feature:

if maybe_box.value != null:

That exposed a real nullable-field-guard gap, but that is not what this was supposed to be about. Very rude of the test case to have ambition hmph >:(

Where this leaves it

Semantic union narrowing now persists past field access — at least for stable dot-access chains — and the temp-local is no longer needed.

Butttt now I gotta work on nullable-field-guards, ugh (hehe)

0
0
6
Open comments for this post

25m 45s logged

Devlog: Direct Semantic Union Parameters

Now instead of having to do this workaround:

type Value = int | string
func accept(value: Value): bool

You can just pass semantic unions directly:

func accept(value: int | string): bool

That was it. All I had to do was remove a stale type_error clause in register_params and allow binding variables. Then I updated the two relevant accept functions in /examples.

0
0
11
Open comments for this post

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
0
7
Open comments for this post

24m 57s logged

Devlog: Teaching Flower to Understand Guard Clauses

gave Flower branch-local inference.

Last update, I added support for branch-local inference, so the compiler could understand things like:

if maybe == null:
    return false
else:
    real: @int = maybe
end

and:

if value is int:
    return false
else:
    s: string = value as string
end

That was an improvement, but it still meant narrowing stopped at the end of the branch. That meant stuff like returninging in some conditions still did not really work as naturally as it should:

if maybe == null:
    return false
end

real: @int = maybe

or:

if value is string:
    return false
end

n: int = value as int

In v1.4.9 I fixed that.

What changed

Now narrowing propagates past very obvious guard clauses. If one branch definitely returns and the other falls through, the compiler now carries the interpretation forward after the if.

This means that the compiler can now understand:

  • after if x == null: return ..., x is non-null
  • after if x is string: return ..., a two-member semantic union can narrow to its remaining member

I kept this intentionally conservative for now. The compiler is not doing full control-flow analysis yet. It is only recognizing the simplest and most explicit early-exit shape.

How it works

The main addition was a small semantic check for “does this branch definitely return?”

Right now that means:

  • direct return
  • if where both branches definitely return

Once the compiler knows one side exits and the other survives, it applies the branch’s narrowing to the following statements.

Yea.. that’s it lol.

0
0
8
Open comments for this post

1h 10m 10s logged

Devlog: Adding Obvious Branch-Local Inference to Flower

Before this, narrowing mostly worked in the obvious “true branch” cases:

if maybe != null:
    real: @int = maybe
end

and:

if value is string:
    s: string = value as string
end

But the else branch was still kind of standing there empty-headed, even when the meaning was extremely obvious. Like, if this happens:

if maybe == null:
    return false
else:
    real: @int = maybe
end

then inside the else, maybe is clearly not null.

So now it doesn’t!!

What changed

Flower now does branch-local narrowing in else for small, explicit cases.

That means these now work:

if maybe == null:
    return false
else:
    real: @int = maybe
end

and:

if value is int:
    return false
else:
    s: string = value as string
end

It is only applying inference when the condition says something directly and the opposite branch has one obvious meaning.

Nullable narrowing

For nullable pointer-like values, Flower now understands both sides of simple null checks:

  • if x != null narrows x to non-null in the if body
  • if x == null narrows x to non-null in the else body
  • the opposite side can narrow to null when that is the only meaning left

Semantic union narrowing

The same idea applies to semantic unions, but a bit more ‘held back.’

Essentially, if Flower sees:

if value is int:

then the if branch narrows to int, like before.

Now, if the union only has one other possible member, the else branch can narrow too. So for:

int | string

the else after if value is int can be treated as string.

This is not full control-flow wizardry, since it doesn’t reason across deep boolean chains, early-returns, or giant branches yet. It is deliberately small and local, because I do not want Flower “helping” so much that I need a detective board to figure out what type something is.

0
0
7
Open comments for this post

46m 14s logged

Devlog: Letting Semantic Unions Work In Functions

After getting semantic unions working in local declarations, the next problem was fairly obvious (and in my roadmap hehe).

This did not work before:

func make_value(): int | string
    return 7
end

func accept_value(value: int | string): bool
    return value is int
end

Which was annoying, because int | string was already a real union type at this point. Flower could use it in locals, it could pack assigned values, it could check variants. But if I returned 7 from a function declared as int | string, or passed 7 into a parameter expecting int | string, the compiler did not know how to carry that union-lowering information across the boundary.

This has been one of the easier changes I’ve made in a long while. It compiled properly first try (if you ignore a tiny misspell I made which I fixed really quickly), and there were no prior hidden bugs that surfaced. This.. is too suspicious.

What changed

The compiler now carries semantic union info through:

  • function parameters
  • function returns
  • imported alias calls

If a function expects:

int | string

and I call:

accept_value(7)

typecheck records that 7 needs to be packed as the int member of that union.

Same for returns:

func make_value(): int | string
    return 7
end

That matters because codegen cannot guess later due to the separation of responsibilities I’ve laid out. By the time Flower is emitting C, it needs to know which tag to set and which member to write.

Lowering it properly

Codegen already knew how to pack union values for declarations and assignments.

The missing part was doing the same thing for call arguments and return statements.

So now a value can be packed into a tagged union when it is:

  • stored locally
  • passed into a function
  • returned from a function

Alias calls got handled too, so things like:

remote.accept(11)
remote.make_string()

do not become their own weird little cursed island.

Why this mattered

Without this, semantic unions only worked as long as I never organized my code.

They worked in local tests, but not in helper functions, imported providers, or normal inputs and outputs.

Now union values can actually move around the program instead of being trapped in the one scope where they were declared — and for me, forgotten!

Very exciting, the union can leave the house. They grow up so fast ⁎⁍̴̛ ₃ ⁍̴̛⁎

0
0
7
Open comments for this post

3h 19m 7s logged

Devlog: Semantic Unions Become Actual Values (Part I)

In v1.4.6, I made semantic unions actual runtime values instead of just parsed/typechecked shapes.

Before this, Flower could understand:

type Value = int | string

as a type. It could point at it and say, “yes, that is a union,” but that was mostly theoretical.

The goal was to make this work-work for realsies:

type Value = int | string

value: Value = 7

if value is int:
    n: int = value as int
end

value = "Ivy"

if value is string:
    s: string = value as string
end

That meant Flower needed more than TypeInfo.is_union = true.

It needed a full path from syntax, to type proof, to lowered storage, to runtime extraction. Otherwise unions would just be fancy typechecker thingy-ma-bobbies, which is not very useful when the backend still emits C.

What changed

Instead of treating a union as a vague “one of these types, probably,” Flower now lowers it into a tagged structure.

Conceptually, this:

type Value = int | string

becomes something like:

struct Value {
    int tag;
    union {
        int as_int;
        flower_string as_string;
    } data;
};

Not the final sacred form forever, but that is the shape of it: a tag saying which variant is active, plus storage for the possible members. Since Flower emits C right now, the lowering is C-shaped. I am not fully sure how I want this to look in ASM yet, but I do know it would be way more technical, and uglier.

This required a few things to play nicely with one another:

  • if value is int creates a union proof
  • the guarded branch stores narrowing information
  • value as int is only allowed after proof
  • assignments pack values into the right tagged member
  • explicit casts extract the right member from storage

Otherwise the compiler is just writing checks its own backend cannot cash.

The part that makes it ACTUALLY semantic

For pointer narrowing, if value is T mostly refines how the compiler treats an existing value. For semantic unions, the check also becomes a runtime tag test.

So this:

if value is int:

means:

check the active tag of value, then inside this branch, treat it as the int variant.

Then this becomes valid:

n: int = value as int

because the branch already proved the active variant.

Without that proof, value as int should not be casually allowed. Flower is supposed to be explicit, not psychic, and I am not confident enough yet to implement a proper intuitive implicit checker without summoning something cursed.

Assignment and extraction

This:

value: Value = 7

now packs 7 into the int member and sets the tag to the int variant.

Then this:

value = "Ivy"

does the same thing, but for the string member.

So a semantic union is not just some bytes, but rather a tagged runtime object where Flower knows which member is active.

Then explicit casts like:

value as string

can lower into extracting the string member, but only when the typechecker has accepted that the operation is safe in that control-flow path.

Why this checkpoint matters

Flower can now store variant values, prove which variant is active, and require explicit extraction instead of relying on random magic I threw together.

The flower has grown another concerning little organ.

0
0
3
Open comments for this post

2h 2m 57s logged

Devlog: is Checks

v1.4.5 added is checks, mostly so code like this can work:

if maybe_ptr is @int:
    real_ptr: @int = maybe_ptr
end

The idea is (fairly) simple: if the condition proves maybe_ptr is an @int, the inside of the branch should be allowed to treat it like one.

Flower did not know how to do that before. It knew what a variable was declared as, but it did not really learn anything from control flow yet. Now it can, at least for the pointer/nullable-ish cases I care about first.

This touched the parser, AST, typechecker, narrowing logic, and codegen. So, naturally, the “small feature” went everywhere.

I’d actually consider this one of the bigger refactors so far, mostly because it added a new TypeAtom structure. Before this, types were still a bit too attached to the places they appeared, and all fell under the TypeInfo. That worked for simpler declarations, but is checks needed type syntax to have something the compiler could pass around to use for comparisons, validations, and narrowing.

The parser had to recognize the new expression shape. The AST had to store the left side and the tested type. The typechecker had to make sure the test was allowed and not totally garbage. Then the narrowing logic had to say, “inside this branch, this variable has the proven type of x and therefore we won’t flag you for being an idiot.”

That last part was where the bug goblin lived. I’m calling them a goblin because they were sneaky (and I also think it’s quite a funny and cute name).

For a while, Flower had applied narrowing from the if body instead of the if condition, which meant the compiler saw this:

if maybe_ptr is @int:
    real_ptr: @int = maybe_ptr
end

and somehow still failed on the assignment inside the branch.

Very helpful. Thank you, compiler. EVen though it’s obviously 100% my fault, I made the compiler, but oh well!!

Once I changed the narrowing step to inspect the actual condition expression, it started behaving like it should. The typechecker could see maybe_ptr is @int, attach that narrowed type to the branch scope, and allow maybe_ptr to be used as @int inside the guarded block. For now this explicitness is how I want it. There may come a time in the near future where I decide some implicity is ok, but we’ll see ;)

is checks are the first real step towards control-flow-aware typing in Flower. Not full union support yet, but definitely the beginning of that road soon to come.

Right now it mostly helps with nullable and pointer-like cases. Later, the same idea can support richer unions, better narrowing, and more type-driven control flow without making the programmer manually restate everything the compiler should already know.

Also, it bootstrapped cleanly twice — which.. is obviously the bare minimum, but still!!

0
0
9
Open comments for this post

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

1h 48m 41s logged

Devlog: Adding Pointer-First Nullable Types to Flower

I worked on giving Flower its first real nullable type surface without
being overambitious and pretending the compiler is ready for full
optional-everything semantics yet.

The new syntax now works like this:

ptr: ?@int = null
text: ?@char = null

That means there is now an explicit way to say “this pointer-like value
may be absent,” instead of relying entirely on raw pointer habits from C
(If you couldn’t tell by now, I despise C).

What changed

The main work was split across lexer, parser, and typecheck.

I added:

  • ? as a type marker in type positions
  • is_nullable tracking in TypeInfo
  • type rules for nullable pointer/reference values
  • explicit compatibility for:
    • assigning null into nullable pointer-like types
    • comparing nullable values against null
    • casting between @T and ?@T

I also kept the scope intentionally narrow: this implementation is
pointer-first nullable support, not full nullable scalars.

So ?@int makes sense right now, but ?int is still deferred until I
make a better semantic union / optional representation for plain values.

Why this matters

It would have been easy to overreach here and claim “nullable types are
done,” but that would have been a big phat lie.

Pointer-like nullability lowers naturally enough with the current
backend and current compiler model. Scalar nullability, however, does
not. Once you let int or bool become nullable, you need a more real
representation for “has value / no value,” and that belongs in the later
union work, not here.

Result

  • explicit absence
  • compiler-owned semantics
  • better future unions
  • stronger type meaning without hidden magic

The next real step after this is not more patching around null — I
consider that done for now — but instead giving Flower the deeper
machinery needed for nullable non-pointer values and semantic unions
later in v1.4 so it doesn’t come back to bite me in my butt.

0
0
6
Open comments for this post

19m 17s logged

Devlog: Formalizing Flower’s Null Foundation

Flower already had some of the pieces for null, but they were a bit scattered. The goal here was to make it explicit, consistent, and safe before moving on to nullable syntax like ?T or full union-based nullability later in v1.4.

Another shorter devlog, but still important and distinct enough for me to think “Hmm devlog time? Yes, devlog time!”

What changed

The main work was in typechecking. I added to the compiler’s idea of what null is, how it is represented internally, and what kinds of values are actually allowed to accept it. Rather than letting null-related behavior stay implicit across a few checks, null now follows a clearer rule set:

  • null has one centralized internal type representation
  • pointer-like values can explicitly accept null
  • null comparisons and assignments now go through the same semantic path

Why this matters

This isn’t all that important on its own, but it matters a lot for where Flower is going next.

If null is not defined well, then later features like:

  • ?T
  • better unions
  • smarter narrowing
  • more expressive type flow

all become more error-prone (or so I assume given my track record) than I want need them to be.

Validation

I also expanded the example coverage so null: it now has dedicated behavior checks around assignment, passing values around, and comparison behavior.

0
0
15
Open comments for this post

1h 0m 5s logged

Devlog: Fixing Flower’s Compiler Process So It Stops Fighting the Host

This wasn’t really about language design at all buttttt it is much more embarrassing, AND more important: Flower’s compiler process itself was too fragile.

The immediate issue showed up on Linux, specifically NixOS (thanks shipwrights <3). The build flow was assuming too much:

  • a previously existing bin/Flower could be reused safely
  • bash would exist at the expected path
  • clang would always be available
  • the checked-in compiler binary would be runnable on whatever machine the repo landed on

That worked well enough on my machine until it didn’t. On NixOS — and by extension probably any other machine without the prexisting binaries I had — it broke: make targets were inconsistent, the shell script failed on interpreter assumptions, and the compiler binary itself could be the wrong format for the host.

What changed

The fix was to separate “the compiler source exists” from “the current compiler binary is usable on this machine.”

The build process now starts by compiling a fresh host-native bootstrap compiler from bin/Flower.c, instead of trusting whatever happens to already exist in bin/Flower.

That means the flow is now:

bin/Flower.c -> host-native bootstrap compiler -> new Flower compiler -> self-check

instead of:

maybe existing bin/Flower -> hopefully works -> maybe bootstrap

Ok well that’s kinda a lie; it still relies on hopes and dreams to bootstrap sometimes. BUT it’s MUCH safer! And now everyone gets to experience crippling bootstrap anxiety, instead of being met with a failure before the actual compiler gets to.. well, do its thign!

I also cleaned up the surrounding tooling:

  • switched the bootstrap script to portable sh
  • restored a clearer build / bootstrap / rebuild / test / clean structure
  • made the compiler use CC and CFLAGS from the environment instead of hardcoding clang

What this actually improves

Flower is still not universally cross-platform yet. The compiler driver still has POSIX-shaped assumptions in places, and the emitted C can still depend on host APIs depending on what the program uses. I’m not yet at the point of wanting to make it completely universal, especially given how I’m only one person and with v2.0 coming up soon it’d — in my humblest of opinions — be a waste of time.

I am honestly grateful for people reviewing my code and otherwise viewing it catching onto details I have overlooked. My goal is to not make a ‘just-barely working on my machine’ project; I enjoy sharing development with others, and it hurts knowing people can’t appreciate code, whether it be mine or others’, due to something as simple as hard-coded sh flags.

2
0
26
Open comments for this post

3h 46m 32s logged

Devlog: Making Flower’s Type Aliases Transparent

This version was simple to develop on paper (and even in practice it wasn’t too bad, only around 4 or so hours!):

type Count = int
type Total = Count
type Name = string

and then have those aliases behave like the real type everywhere:

n: Count = 12
total: Total = identity(n)
name: Name = "Ivy"

if name != "Ivy":
    return false
end

if name.length != 3:
    return false
end

That meant aliases could not just be parsed and stored, but rather had to be transparent to the compiler’s semantic checks.

What changed

I added type alias declarations to the AST, parser, and type environment, then added the ability to resolve aliases in typechecking before comparing types.

That included:

  • return type checks
  • variable initializers
  • function call arguments
  • binary comparisons
  • string-specific comparison logic
  • struct field access

The important rule here is that aliases are semantic, not codegen-facing. Count should behave as int, Name should behave as string, and chained aliases like Total -> Count -> int should collapse correctly. In the future, especially post v2.0, this could change and custom types might become real — but for now, they’re just aliases.

The bug that exploded bootstrap

The first real failure was not in aliases themselves, but in function argument checking.

Inside check_call_args(...), the compiler was resolving the argument expression into expected_type instead of arg_type. That overwrote the parameter type, left the actual argument type uninitialized, and caused the second bootstrap stage to explode into over a thousand misleading errors. This was not a pre-existing issue, it was instead from human error (my error) during the initial implementation where I accidentally put the wrong variable.

Once that was fixed, the remaining failures were much more helpful than the bright flash of red and “Typecheck failed with 1224 error(s)” message!

Where this leaves v1.4

With the first step toward “better types” done, aliases are no longer just syntax. That gives v1.4 a solid base for whatever comes next in the type system without having to fake around raw names.

During this whole dev session, I did discover that a whole lot more of the source code was left using legacy work-arounds: int instead of bool, name_start; name_length and @char instead of string. In all fairness, some of these aren’t exactly the easiest to refactor. However, I will probably hold off on refactoring the codebase until either right before v2.0, or right after. Just know it’s on my radar ;)

0
0
7
Ship

Flower is a self-hosted programming language for writing simple, explicit systems code without the abstraction bloat and OOP-heavy direction a lot of modern languages have. The compiler is written in Flower itself, currently targets C, and follows a straightforward pipeline: lexing, parsing, module loading, typechecking, and code generation.

Over the past few months, Flower grew from a small lexer/parser/codegen experiment in C into a way bigger language project than I couldv'e imagined. This wasn't my first attempt at a language, but it has been my favorite! I implemented compiler-enforced module semantics, stronger type-aware codegen, real boolean support, real string support, and a small standard library foundation. Strings now have equality, .length, indexing, and explicit casts to and from @char, while modules support exported prop declarations, aliases as namespaces, and private-by-default top-level declarations.

The hardest part was bootstrap stability. I ran into idempotency failures, parser edge cases, compiler corruption scares, and plenty of my own bad logic. A lot of the challenge was getting new features to work not just once, but consistently enough for the compiler to rebuild itself cleanly. One bootstrap it succeeds, and then the next I'm scrambling to figure out why the new binary executable has a corrupted parser despite just working (Still haven't fixed that yet! It.. was really confusing and I've got no clue why it happened lol). I’m proud that I kept pushing through that and ended up with something much more fun to use than where I started — I mean, I don't even have to write that much C anymore!!

If people want to test Flower, the important thing to know is that it is self-hosted and still experimental, but it does work. The repo includes a bootstrap path, a test suite, and example programs covering types, control flow, memory, imports, strings, and bools. The current release is focused on making the core language feel real and usable before moving on to a larger standard library and a future non-C backend, so don't be surprised if a C file is shoved in your face!

  • 12 devlogs
  • 38h
  • 18.19x multiplier
  • 690 Stardust
Try project → See source code →
Open comments for this post

1h 24m 50s logged

Devlog: Cleaning Up v1.3 For Release

After many hours of work, bool and `string finally became real, and it was time to cleanup the mess I left behind from the version’s development.

Mainly the only thing worked on was some cleanup:

  • roadmap now reflects actual v1.3 features and implementations
  • updated README and contribution documents to match the current compiler pipeline
  • cleaned up string examples so they have the full intended v1.3 style instead of some leftover testing usage
  • pushed the compiler’s version to 1.3.0
  • updated help / usage text so it matches the new output-path and auto-run behavior

Prior to this, if I had just merged and shipped, the release wouldv’e been messy: README, Contributing, example files, and guidance commands were all mixing older details which made it confusing — even for me — when trying to explore the language and what it has to offer.

For a bootstrapped compiler especially, making sure everything is cleaned up and polished is something that should not be ignored. I’m definitely guilty of overlooking this, but I’ve outlayed a whole plan for v1.3 unlike past versions, and part of it was setting up a proper versioning system and polishing its release as it goes. This format will be adopted in future versions, since it’s just way cleaner and nicer to look at.

Future work

At this point, v1.3 is done and has implemented, at minimum, a concrete foundation for post-v2 bools and strings. Next will likely be more subtle improvements, and features that are currently reliant explicitly on C rather than Flower-hosted code. In order for v2.0, Flower will have to be effectively independent from C if it wants to emit machine code. Currently I am planning on adding features such as Type Aliases and maybe a stdlib prior to that, but anything can change!!

Thanks for being here for all of v1.3 <3

3
0
118
Loading more…

Followers

Loading…