I think I've discovered an issue with our type grammar. It's currently impossible to have a function signature in a) a Record annotation, b) in the requires signatures of a platform header, and c) the where clause - i.e., anywhere it can legally be followed by a Comma. It's because we can either:
I think either is kind of sad
I will hesitantly tag @Richard Feldman here because he'll either tell me there is a way out of this I haven't found in the day or two I've been playing with this, or provide opinions on how we could move forward.
Another option is just require parens around function signatures in these places - which is also not beautiful. If we make a syntax change, I'd like it to be something that we'd be comfortable with in any position.
The problem is in a snippet like this:
platform "foo"
requires { Main } {
main! : List(Str) => {}, # Comma after signature
}
exposes [foo]
packages {
some_pkg: "../some_pkg.roc",
}
provides [bar]
If this parses successfully, then something like:
SomeMl a : {
foo : Ok(a),
bar : Something,
}
Will fail because we are looking for args for a function now and the next meaningful token after the Ok(a) is a lower ident (a valid TypeAnno). We then blow up when we see the OpColon.
If we don't look for args there, then the List(Str) => {} in the platform header needs to be parenthesized
So we either need to look ahead for one of:
After a valid TypeAnno which is tough because a lower ident is a valid type anno which we would have to kind of ignore, backtrack, and use it as a record field name instead if #1 fails.
@Joshua Warner let me know as well if you see a logical error that I'm making
My personal thoughts here is that the best solution is for function args in a signature to match the way they appear in a lambda expr, namely:
platform "foo"
requires { Main } {
main! : |List(Str)| => {}, # Comma after signature
}
exposes [foo]
packages {
some_pkg: "../some_pkg.roc",
}
provides [bar]
We can then maintain the same restriction we have today - that a function as an arg has to be in parens:
map a : | List(a), (| a | -> b) | -> List(b)
But the counter argument of course is we now enter syntactic "Uncanny Valley" where the type signature is very similar to the expr, but just off by not having the arrows.
This would mean that
main! : List(String) => Result({}, _)
main! = |args| {
... # Some body
}
Becomes
main! : | List(String) | => Result({}, _)
main! = |args| => {
... # Some body
}
And
add_one : U64 -> U64
add_one = |num| if num 2 else 5
Becomes
add_one : | U64 | -> U64
add_one = |num| -> if num 2 else 5
Maybe @Anton could move this to a new thread in Ideas, maybe called "Needed Function signature and lambda expr change"?
To TLDR the above, we have three main ways to solve the current contention with function signatures in constructs that are comma separated:
|| (has problems discussed before, with the following potentional solutions):\I'm going to pause the completion of the Parser until we come to a resolution of this topic.
9 messages were moved here from #compiler development > zig compiler - parser by Anton.
Thank you @Anton !
Check out how the old parser implements type parsing in tuple types, we have what I think is the same problem there, and it’s solved by inlining and unifying the logic for comma-separated types and arguments of a function type. Types in records aren’t nearly so tricky since you can just check for [LowerIdent, Colon] in the lookahead and know that can’t be an arg to the function type.
It’s annoying, yeah, and I would love to find a different syntax that makes that ugly logic unnecessary, but I think it (still) works.
But that is still a form of backtracking
It would be unfortunate to have to resort to that for this one construct
I think any of the options under #3 above would be a better state
What about just require wrapping the whole function type annotation in parens in these cases?
That seems the default for these kinds of ambiguities
That is option 2, but I think it will be seen as surprising
I definitely don't think we should wrap the whole function type annotation in either parens or pipes
if this is only a problem in things like where, I wonder if there's a way we could change those instead of changing functions :thinking:
No, it’s tuple and record annotations, and other places listed above
Option 3 is to surround the function ARGS in pipes
that also doesn't sound good :sweat_smile:
Joshua Warner said:
Check out how the old parser implements type parsing in tuple types, we have what I think is the same problem there, and it’s solved by inlining and unifying the logic for comma-separated types and arguments of a function type. Types in records aren’t nearly so tricky since you can just check for [LowerIdent, Colon] in the lookahead and know that can’t be an arg to the function type.
It’s annoying, yeah, and I would love to find a different syntax that makes that ugly logic unnecessary, but I think it (still) works.
this sounds like clearly the best option at the moment...seems like one of those situations where UX for end users is in tension with niceness of implementation, and there doesn't seem to be a way to get both to be nice at the same time
That’s fine, but I wrote it up above and it looks fine to me. But it’s your language
the problem with pipes around args is when the args are on multiple lines
Ok, it’s just at a fundamental tension with the entire design of the parser - but I’ll find a way to
looks fine single-line, doesn't look fine with multiline unfortunately :sweat_smile:
Found the fix, and I didn't have to do any backtracking, just 2 token lookahead:
https://github.com/roc-lang/roc/pull/7733
All headers are now able to be parsed and formatted as per what you see in the tests
I just remembered I haven't implemented where clauses yet, so that will be next.
But we truly are at a point where 95% of real application code in Roc (written in the v0.1 style) can be parsed and formatted correctly
Just as long as you don't use where. :-)
Thank you for your tireless efforts Anthony, this is so exciting! :smiley:
No worries! This parser has actually become one of the funnest projects I've ever done, and one of my favorite parsers to work in.
I can't wait to be done and help the rest of the compiler catch up so I can write some actual Roc again :rofl:
And thanks to @Richard Feldman for pushing back and getting me to persevere here
yoooooo, amazing work @Anthony Bullard!!! :heart_eyes:
Last updated: Jun 16 2026 at 16:19 UTC