Devlog: Replacing Flower’s Old Function Syntax with 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.
The Problem
Flower’s parser assumes top-level function definitions start with a type. Old parser logic was built around that:
int main():
means:
- parse a type,
- an identifier,
- params,
- and a body
That assumption was scattered through a few different places:
- normal function definitions
-
prop function declarations
-
forward declarations
- examples and docs
- the compiler’s own source
Now, 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 Shape
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 recognizable
-
prop composes more cleanly with func
- return types now live where people expect them to
- future syntax has more room to grow without feeling backwards
It also feels more in line with Flower’s broader goals: explicit, readable, and not complex.
The Refactor
Previously, parse_func_def looked for a return type first:
type name(...):
Now it expects:
func name(...): type
So the order changed to:
- expect
func
- parse function name
- parse parameter list
- expect
:
- parse return type
- parse body until
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 function
-
func → normal function
-
forward func → forward declaration
The old one worked, but it relied on vague “anything else is probably a function” rules.
The Bootstrap Mistake
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.
Feel free to check out the bootstrapper to see why this was an issue ;)
This meant my “new compiler to compile the new compiler” was built from a broken parser.
The workaround was cursed, but it worked:
- check out the commit before the refactor
- fix the bug
- compile a new binary
- switch to the refactor branch
- use the binary to compile the new version
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.