Flower
- 28 Devlogs
- 73 Total hours
A compiled bootstrapped low-level programming language focusing on simplicity, versatility, and ability.
A compiled bootstrapped low-level programming language focusing on simplicity, versatility, and ability.
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!
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).
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
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)!
I updated the docs that had drifted the most from reality (relatable):
README.md now reflects the current type-system shapeROADMAP.md treats v1.4 as accomplishedv1.4.0 milestone doc now includes more of the outcome, not just the original planstructure.md is more accurate to what Flower currently is, and what still needs a fuller rewrite laterYea, that’s it.
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:
null in type syntax lower the same way the compiler expectsmaybe_box.value and holder.box.value narrow properlyThe 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…
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
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.
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 >:(
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)
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.
?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.
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:
as
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!
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:
(int*)(maybe)
== null / != null narrowing to the wrong side (this was my fault lol)null getting packed into the wrong union memberThat 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:
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.
?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..
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.
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:
if x == null: return ..., x is non-nullif x is string: return ..., a two-member semantic union can narrow to its remaining memberI 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.
The main addition was a small semantic check for “does this branch definitely return?”
Right now that means:
return
if where both branches definitely returnOnce 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.
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!!
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.
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 bodyif x == null narrows x to non-null in the else bodynull when that is the only meaning leftThe 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.
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.
The compiler now carries semantic union info through:
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.
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:
Alias calls got handled too, so things like:
remote.accept(11)
remote.make_string()
do not become their own weird little cursed island.
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 ⁎⁍̴̛ ₃ ⁍̴̛⁎
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.
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 proofvalue as int is only allowed after proofOtherwise the compiler is just writing checks its own backend cannot cash.
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 theintvariant.
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.
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.
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.
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!!
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
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:
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?
This version was intentionally strict about using unions as values.
You cannot yet 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.
Flower now has:
The next step is the part that will make unions actually usable rather than merely legal:
is
as
?T truly become T | null instead of remaining pointer-first for nowNo fun screenshot this time :p just frontend work for now…
Ivy, signing off <3
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).
The main work was split across lexer, parser, and typecheck.
I added:
? as a type marker in type positionsis_nullable tracking in TypeInfo
null into nullable pointer-like typesnull
@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.
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.
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.
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!”
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 representationnull
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:
?Tall become more error-prone (or so I assume given my track record) than I want need them to be.
I also expanded the example coverage so null: it now has dedicated behavior checks around assignment, passing values around, and comparison behavior.
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:
bin/Flower could be reused safelybash would exist at the expected pathclang would always be availableThat 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.
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:
sh
build / bootstrap / rebuild / test / clean structureCC and CFLAGS from the environment instead of hardcoding clang
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.
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.
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:
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 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!
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 ;)
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:
v1.3 features and implementationsv1.3 style instead of some leftover testing usage1.3.0
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.
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
cstr Foundation
Even after Flower got a real string type, the compiler itself was still leaning directly on C-style string helpers for its own internal work.
That meant a lot of Flower source was still semantically depending on libc behavior:
!strncmp(...)
strcmp(...)
strlen(...)
strcpy(...)
strncpy(...)
strdup(...)
strrchr(...)
That is not where I want the language to stay.
I added a new internal module:
import "src/stdlib/cstr.flo" as cstr
and gave it compiler-facing helpers like:
prop func len(s: @char): int
prop func eq(a: @char, b: @char): bool
prop func eq_n(a: @char, b: @char, n: int): bool
prop func copy(dst: @char, src: @char): @char
prop func copy_n(dst: @char, src: @char, n: int): @char
prop func dup(src: @char): @char
prop func find_last(src: @char, ch: char): @char
This helper module is essentially the precursor to a future strings.flo library that can be distributed. But for now, it uses a basic API layer purely meant for in-house usage.
Then I migrated compiler internals over to that module across:
src/parser.flosrc/module.flosrc/lexer.flosrc/typecheck.flosrc/codegen.flosrc/globals.flosrc/main.floSo instead of using C-string behavior directly everywhere, the compiler now goes through Flower-owned helpers first.
This was one of those changes that sounds mechanical until it absolutely is not.
A big trap was that old code often relied on C comparison functions returning 0 on equality, so patterns looked like this:
not strncmp(...)
But cstr.eq_n(...) returns an actual bool, which means blindly rewriting those calls gives inverted logic.
So this:
if ps.alias_lengths[i] == tok.length and not cstr.eq_n(...):
was completely wrong. It had to become:
if ps.alias_lengths[i] == tok.length and cstr.eq_n(...):
That bug showed up in alias parsing first, which made it especially cursed, because it caused normal namespace-style calls like cstr.eq(...) to stop parsing correctly and crash the bootstrap path in confusing ways. I spent way longer than I wish to admit on trying to figure out why it wasn’t playing nice. But, once I figured it out, it was a really quick fix (I literally just cmd + f’d not cstr and replaced it with cstr lol).
This does not mean Flower is free from the C backend yet.
It does mean something important, though: compiler logic is starting to depend on Flower-defined helper semantics instead of scattered raw libc calls. That is a much better stage for the future backend story, because when C eventually stops being the primary lowering target, the compiler will already have more of its own vocabulary.
In other words, this was less about “remove C” and more about “stop letting C define the language’s internal habits.” Nah just kidding, I really wanted that bloated “builtin_c_call” stuff gone. Unfortunately I only managed to skim it, but that’s closer than before!
After fixing the migration mistakes and the alias/parser fallout, the compiler bootstrapped successfully again.
Flower now has an internal cstr layer, and compiler string handling is a little more its own.
This checkpoint was really two different kinds of progress that ended up helping each other a lot.
The first part was not glamorous, but it was necessary: Flower’s compile driver was tripping over its own default output naming. This was a known issue that I had been putting off, but for some reason it really annoyed me this time so I decided enough and to fix it.
If no explicit output path was provided, the compiler would generate output.c, then strip the .c and try to emit a binary named output. That worked fine right up until the repo itself already had an output/ directory. At that point, linking failed because the compiler was trying to create a file where a directory already existed.
Worse — because it annoyed me more — the driver logic was also mixing up “the generated program exited non-zero” with “compilation failed.” That became especially annoying during bootstrap, because the generated compiler binary would run without CLI arguments, print its own usage error, and then cause bootstrap to report failure even though codegen and C compilation had both succeeded.
So I finally got off my butt and cleaned that up.
The default output path now goes to:
output/out.c
which naturally produces:
output/out
as the binary.
That avoids the old output filename collision.
I also separated compile success from auto-run behavior:
That means bootstrap no longer “fails” just because the compiled compiler exits non-zero with missing arguments, and demo/test files can still run automatically when that is actually useful.
This sounds like a small driver fix, but honestly it removed a lot of noise from string work — and more — immediately.
Once that output weirdness was under control, I could finish doing what I just started and actually wanted: making string behavior live in Flower instead of endlessly leaning on ad hoc C calls (Call back to last devlog, absolute ball knowledge!!).
So I added the first real reusable helpers in src/stdlib/string.flo:
is_empty(s)starts_with(s, prefix)ends_with(s, suffix)find_char(s, ch)These are intentionally simple. They mostly exist to prove that the current string support is now strong enough for normal library code to be written on top of it. Not incredibly useful, at least at the level the compiler needs, but at least I could translate some AP Comp Sci A into Flower hehe
A string feature does not really feel “real” until I can build helpers with the language itself instead of only teaching the compiler more special cases. The day I remove the builtin C handlers is the day I’m at peace…
To exercise that new library surface, I changed string_pass.flo to now act as a small pass file for the experimental library.
Its job is not to be impressive. Its job is to answer a more important question:
can I import string helpers, call them from normal Flower code, and get expected results through the full compile pipeline?
At this point, yes.
This checkpoint did not remove all C string dependencies from the compiler. In fact, it removed essentially none! That is still a separate step, one that may even be in v1.3.7 ;)
But it did establish two important things:
That is a good place to be, because the next string work can build outward from a stable driver and an actual library surface instead of piling more special behavior into the compiler.
This one was fairly quick, and was about making one very normal-looking thing actually work:
name: string = "Ivy" as string
first: char = name[0]
That sounds small, but once string became a real Flower type, indexing could no longer piggyback on the old “arrays and pointers only” behavior. The compiler needed to understand that string[index] is its own case.
I taught the typechecker to treat string subscripts as char.
That means:
string[index] now resolves to char
So this is valid:
first: char = name[0]
but this is not:
name[0] = 'X'
I wanted reads first, writes later. That keeps the rules simple while the string representation is still compiler-owned. I’m not sure how writes would work with a C backend, due to how ‘strings’ are allocated. Perhaps a feature for post-v2.0?
Since Flower strings currently lower to a struct with .data and .length, codegen needed a special path for string subscripts.
Normal arrays and pointers still lower like:
arr[index]
but strings now lower like:
name.data[index]
That keeps the generated C valid without changing how regular subscripts work elsewhere in the language.
So something conceptually like:
This checkpoint also forced one strict-but-correct rule that required me to update a WHOLE SINGLE LINE (hehe):
arg[i] = tolower(arg[i] as char)
to
arg[i] = tolower(arg[i] as char) as char
Because tolower(...) is effectively treated as returning int, assigning it back into a char subscript needs an explicit cast. I kept that strictness on purpose. I do not want Flower silently narrowing values just because C would let it.
I expanded the string example coverage to check:
string[index] works alongside string <-> @char castsWhich, after all the recent bootstrap chaos, was very nice to see.
Now we can:
length
char
This should make it much easier to replace the builtin C functions with a Flower library in the near-near future.
The last round got string into Flower as a real type. This round was about making that foundation actually work lol.
The big problem was that once string became real, plain string literals started living in an in-between state: Flower wanted them to mean “Flower string,” but the compiler’s own source still has plenty of places that really mean “raw C string,” like:
RED: @char = "\033[0;31m"
if not strcmp(arg, "help") or not strcmp(arg, "h"):
...
end
Those cases worked before only because everything was basically pretending string literals were already @char. Once that stopped being true, bootstrap started throwing a fit (my spoiled child).
The main fix was to make string literal lowering contextual instead of all-or-nothing.
If a literal is being used as a real Flower string, it stays a string.
If a literal is being used in a C interop position, it gets lowered as a C string instead.
That meant tightening a few compiler rules so this kind of thing works cleanly again:
name: string = "Ivy" as string
raw: @char = name as @char
if strcmp(raw, "Ivy") == 0:
print(name)
end
I also fixed top-level declarations so globals like:
RED: @char = "\033[0;31m"
no longer generate nonsense like:
char* RED = ((flower_string){ ... });
which, unsurprisingly, C does not appreciate.
The nastiest issue wasn’t even the globals. It was that builtin C calls like strcmp, strncmp, printf, snprintf, and friends are not real Flower functions.
That meant the typechecker could hit one of them inside a larger expression, fail early, and stop walking the rest of the tree. So something like:
if not strcmp(arg, "help") or not strcmp(arg, "h"):
could partially resolve one side, then leave the other side unprocessed. Very cool. Very stable. Definitely what I wanted.
The fix was to add a temporary builtin C-call bridge so the compiler can recognize these calls well enough to keep type resolution and literal lowering consistent during bootstrap.
I’m not happy with this fix by any means, but due to the limitations of emitting C, it was the only solution I found that made sense at the time.
Once that stopped exploding, I finally wired string testing into the normal types suite instead of leaving it commented out and pretending I’d “get back to it later.”
The string tests now cover:
string <-> @char casts.lengthAnd the full test suite passed with string included as a first-class part of Types on its first try. Now that’s what I like to see :D
This does not mean Flower’s long-term answer is “hardcode libc into the compiler forever.”
It means Flower now has a temporary interop bridge that keeps self-hosting stable while string is worked on. The actual language semantics stay in Flower; the backend-specific weirdness is just being tolerated for now so progress does not stall. In the near, I’ll probably replace all of the necessary C libs with minimal Flower-equivalents.
At this point, the string foundation feels much sturdier.
Not “done forever,” not backend-agnostic, and definitely not free of cursed bootstrap energy — but it works.
The next work is no longer rescue work. It is actual forward work: string operations, stdlib basics, and eventually replacing this temporary C bridge with something cleaner when Flower grows past the C backend.
As a prize for your great work, look out for a bonus prize in the mail :)
The core goal was to make this work in a meaningful, compiler-owned way:
name: string = "Ivy" as string
raw: @char = name as @char
other: string = raw as string
if name == other:
print("same\n")
end
print(name.length)
That meant Flower needed more than a typedef in generated C. It needed actual type rules, actual lowering rules, and I needed enough hairs on my head to not be bald after the amount of scares that occurred.
The first step was giving string real compiler support without going too far too fast.
I added support for:
string and @char
string.lengthflower_string_from_cstr(...)flower_string_eq(...)flower_print_string(...)That gave Flower a usable string foundation while still keeping literals conservative for self-hosting. Right now, string literals are not fully “native string values everywhere” yet; the compiler still uses explicit casts like "Ivy" as string as the safe bridge.
The reason for this “safe bridge” is because when I got too ambitious, the compiler would break since everywhere @char was used with a string literal it’d break due to no implicity being allowed.
I had to comment out the following new changes and keep the legacy support so it’d compile:
else if ast.kind == AST_STRING_LIT:
// fprintf(out, "flowe_string_from_cst(")
// fprintf(out, "%.*s", ast.data._string.str_length, src + ast.data._string.str_start)
// fprintf(out, ")")
// LEGACY CODE HERE
and
else if expr.kind == AST_STRING_LIT:
// set_plain_type(out, TOKEN_STRING)
// LEGACY CODE HERE
return 1
The most annoying issue was not string equality or .length. It was print.
Originally, non-string print(...) was basically lowered like this:
printf(expr);
That only works when expr is already a C format string. So something like:
print(name.length)
generated invalid C:
printf(name.length);
The fix was to make print(...) type-aware. Typecheck now records the operand type, and codegen lowers print differently for string, @char, char, floats/doubles, and integer-like values.
That sounds small, but it matters a lot: once string became real, print could no longer get away with pretending every printable value was already a C string.
Somehow Flower’s bootstrap process was breaking, and it will probably break again. Despite the bootstrap requiring it to compile itself, somehow afterwords the binary could become corrupted.
l-2: ~/Documents/GitHub/FloC % make bootstrap
=== Building new version ===
Compiled ./bin/Flower_new.c → ./bin/Flower_new
=== Testing new compiler ===
Compiled ./bin/Flower_test.c → ./bin/Flower_test
Verified bootstrap build complete
l-2: ~/Documents/GitHub/FloC % make bootstrap
=== Building new version ===
Luckily, I always make sure to keep a backup Flower bin stored under /bin/Flower_backup so I used that to compile the new binary and then it worked.
What still remains is the bigger ergonomic question: how fully native string literals should behave, and how to migrate the compiler’s own source to that world without setting bootstrap on fire again. I’m not sure yet how I want to approach this question, or what the goals for it should even be, but I guess I’ll have to find out soon enough!
That is the next fight. But at least now, it is a fight on solid ground.
The original goal, asper usual, was straightforward: before moving on to string, Flower needed bool to actually mean something semantically.
Up to this point, boolean-ish behavior mostly worked by accident. Comparisons, conditions, returns, and general expression checking were still loose enough that the compiler could get away with treating a lot of things as “close enough,” especially since the C backend ultimately lowers bool to int anyway.
That was not going to hold once v1.3 started leaning on richer typing.
So this was about drawing a clean line between Flower semantics and C lowering.
In Flower:
bool should be its own real typebool
bool
as
In C:
bool can still lower to int
That distinction ended up being the easy part.
bool as a proper language type instead of an int-shaped placeholder.if, while, and not use condition-compatible rules.I also kept implicit widening one-directional:
int -> float -> double
while leaving narrowing conversions as explicit casts.
The actual implementation of booleans and their typing, enforcement, and semantics was a breeze. It’s what came after that got me.
As usual, bootstrap had opinions.
The stricter type work exposed older weaknesses that were easy to miss before (This is my coping):
. vs -> emission in generated CSo part of this checkpoint turned into compiler stabilization work rather than just bool semantics due to my crappy ‘temporary’ logic, error handling — or lack thereof, and impeccable ability to miss very important details.
I’m an amazing programmer, if you couldn’t tell :p
Flower now has a much firmer semantic base for bool, and the compiler is back to bootstrapping cleanly with those rules in place.
That matters because this was really the floor for the next part of v1.3: string.
And.. maybe I’ll fix the bootstrap process so it doesn’t blow up in my face again. Oh yea, forgot to mention that. The bootstrapper bootstrapped itself into this very broken parser state that refused to go away so I had to stash my changes, checkout the prior merge (detached), and then recompile manually and fix the parser.
Toodles!
Getting bool into Flower exposed a reallyyyy annoying problem: the typechecker had a couple of tiny logic mistakes (I’m so good at this) which caused the bootstrap to blow up in my face and I ahd no idea why.
At first the errors looked unrelated. The compiler started reporting huge numbers of mismatches across main.flo, lexer.flo, module.flo, typecheck.flo, and codegen.flo. I was so confused as to why out of nowhere 903 lines were reporting various typechecking errors when they shouldn’t be.
As it turns out, types_match() was the culprit. The function had been inverted in a few places: Mismatched base types and pointer depths were returning success, while normal matching primitive types were falling through to failure. That made the new compiler think ordinary initializers, assignments, comparisons, and returns were all invalid.
Once that was fixed, the remaining failures turned out to be a second real compiler bug: pointer arithmetic over arrays was decaying in the wrong direction. Expressions like:
mods.modules + j
env.structs + env.struct_count
arr + 1
should produce pointer types. Instead, the typechecker was reducing pointer depth when arrays decayed, which made perfectly normal indexed access and table walking look type-invalid.
So this dev session ended up doing three important things:
types_match(...) so actual matches succeed and actual mismatches failfloat / double matchingWhat made this especially annoying is that the symptoms looked much larger than the cause. A couple of bad returns in the typechecker created hundreds of downstream errors, and because Flower is self-hosting, that meant the compiler started rejecting its own source.
After correcting those core issues, both bootstrap and the example test suite started working again. That is the real value of issues like this: not just “fewer errors,” but restoring (MY) trust in the compiler and its ability to enforce Flower’s rules and report issues.
With that stable again, the path forward is much clearer: continue bool work from a solid base, then move on to the remaining numeric and string improvements without the whole compiler collapsing under one bad predicate.
The goal sounded small at first: add proper boolean support, while still lowering booleans to integers in C.
In practice, this turned into an overarching type-system problem (typical!).
Flower had reached a point where bool could not just be a naming trick over 1 and 0. The compiler itself needed to understand when an expression was semantically boolean, when it was integer, and when older bootstrap-era shortcuts were no longer valid.
A lot of those shortcuts had survived because the backend lowers bool to int anyway. But self-hosting exposed a major issue: source-level typing and C lowering are not the same thing.
Things like these started breaking:
val: int = parser_peek(ps).kind == TOKEN_TRUE
and helper predicates that returned comparison expressions from int-typed functions.
So the checkpoint became: keep C lowering simple, but make Flower’s typechecker treat booleans as a real source-level type.
The main changes were:
parser -> typecheck -> codegen
true / false now parse into AST_BOOL_LIT
0 / 1 in Cbool
if, while, and not now validate condition-compatible expressionsOne of the more annoying bugs turned out not to be “about bools” at all.
When code like this failed inside the compiler:
defined_structs[i].length
emitted_imports[i].path
the real problem was that for i in 0..count was never registering i in the type environment, so indexed access looked broken when really the loop variable itself had no known type. Once loop indices were typed as int, those field-access failures disappeared (yay!!).
The result is a much better place to build from:
bool is now semantically distinct in FlowerThat makes the next step much easier: real string support, instead of trying to build strings on top of a type system that still half-thinks everything is an int.
v1.2.x was the point where Flower’s impors had to stop being “you can import stuff.. and there’s a fake layer of security” to a proper (get it?) module system.
Before this work, imports and aliases mostly behaved like naming conveniences. You could write:
import "math.flo" as math
math.add(1, 2)
but the compiler was still too loose about what that really meant. Alias names were in emitted C symbol names, top-level declarations were not private, and prop existed more as syntax than as a real feature.
That was fine for early bootstrap work, but it was not strong enough for the next stage of the language.
So the main goal of v1.2.x became:
prop define the public interface of a modulehidden and readonly
One of the biggest changes was separating Flower semantics from C lowering.
Previously, imported aliases were drifting toward becoming part of emitted symbol identity. A local alias should just be a source-level name bound to a module, not the backend symbol.
So now each module has its own lowered symbol prefix, and alias lookup happens semantically during compilation. That means:
import "math.flo" as math
math.add(1, 2)
is resolved as “look up exported add in module math.flo,” not “invent a C symbol from the local alias text as math_add().”
That change also forced a cleanup of emitted names so they stopped leaking absolute filesystem paths. Generated symbols now use project-relative module prefixes instead of machine-specific paths.
prop Became Real
Another major part of this was making prop actually do something.
Instead of treating it as a thin wrapper around only functions, the compiler now recognizes prop as a real top-level export marker. That means the module’s public interface is defined by what it explicitly exports, while non-prop declarations remain internal.
This moved Flower closer to the model I wanted all along: explicit modules, explicit public APIs, no heavy object system (OOP), no fake privacy delegated to the C backend.
This work also exposed a weakness in the compiler’s dot-access resolution.
Flower source always uses ., while the C backend must decide whether a given access becomes . or ->. That mostly worked already, but nested pointer-heavy field chains exposed cases where the type tracker was not propagating enough information, which caused bad fallback behavior during self-hosting.
So before field visibility could be trusted, I had to harden nested dot-access resolution. Not fun, but necessary. As it turns out, I was missing several AST kinds which caused the logic to fall through. Very tedious work, but it was well-worth it in the end!
With module ownership and access resolution in better shape, I added the first field visibility modifiers:
hiddenreadonlyfrozenOriginally I debated having these as bitwise operations, but I decided it’d be best to stay within the current update’s scope, so sometime in the future I’ll refactor.
Right now, hidden and readonly are the meaningful additions:
hidden blocks external field accessreadonly blocks external assignmentfrozen is groundwork for later. It is parsed, stored, and carried through the compiler, but full assignment-once enforcement is still deferred. Unsure how I’ll go about it, but that’s an issue for future me :p
func name(...): type
This feature started out as what I thought would be a straightforward syntax cleanup.
Flower’s old function syntax looked like this:
int add(a: int, b: int):
return a + b
end
and exported functions looked like this:
prop int test():
return 1
end
It worked, technically, but it had started to annoy me more. The biggest issue was that function declarations did not visually match the direction Flower was already heading in, and instead felt much more C-like than I wanted.
prop func create(...): Person
func main(): int
This style says “this is a function” up front, gives a clean place for modifiers like prop, and keeps the return type attached to the signature rather than awkwardly leading it.
The goal was simple:
func name(...): type
instead of:
type name(...):
Simple in theory. not so much when the compiler is written in the language being refactored.
Flower’s parser assumes top-level function definitions start with a type. Old parser logic was built around that:
int main():
means:
That assumption was scattered through a few different places:
prop function declarationsforward declarationsNow, in retrospect, this was actually one of the easier changes. It didn’t take a whole lot of planning nor brain power, and I was able to change it pretty quickly. Luckily for me, I’ve built codegen to rely purely on ast values rather than shaping.
The new syntax is:
func add(a: int, b: int): int
return a + b
end
and exported functions now look like:
prop func test(): int
return 1
end
This is nicer because:
func makes declarations immediately recognizableprop composes more cleanly with func
It also feels more in line with Flower’s broader goals: explicit, readable, and not complex.
Previously, parse_func_def looked for a return type first:
type name(...):
Now it expects:
func name(...): type
So the order changed to:
func
:
end
That part was conceptually easy.
The more annoying part was updating all the places that assumed if it isn’t a var decl and it looks like a func, it’s a func.
That fallback had to go (Good riddance!).
Instead, top-level parsing needed to become more explicit:
prop func → exported functionfunc → normal functionforward func → forward declarationThe old one worked, but it relied on vague “anything else is probably a function” rules.
At one point, I made a bootstrap build so I could use a Flower_new binary to compile the refactored compiler. Unfortunately, I had made a mistake in parse_forward: advancing before peek checks.
This meant my “new compiler to compile the new compiler” was built from a broken parser.
The workaround was cursed, but it worked:
Since the compiled binary was ignored by Git, this was actually fairly simple.
Not the most elegant bootstrap story, but it got the compiler to, well, compile.
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 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 ;)
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