Stream: ideas

Topic: Needed Function signature and lambda expr change


view this post on Zulip Anthony Bullard (Apr 08 2025 at 13:06):

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:

  1. Be cautious and not look for them (outside of parens) in those places
  2. Be greedy and implement backtracking for the first time in the parser

I think either is kind of sad

view this post on Zulip Anthony Bullard (Apr 08 2025 at 13:08):

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.

view this post on Zulip Anthony Bullard (Apr 08 2025 at 13:21):

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

view this post on Zulip Anthony Bullard (Apr 08 2025 at 13:26):

So we either need to look ahead for one of:

  1. [Comma, (valid TypeAnno), Comma|OpArrow|OpFatArrow]
  2. [OpArrow|OpFatArrow]

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.

view this post on Zulip Anthony Bullard (Apr 08 2025 at 13:28):

@Joshua Warner let me know as well if you see a logical error that I'm making

view this post on Zulip Anthony Bullard (Apr 08 2025 at 13:40):

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.

  1. Does that seem like a bad outcome?
  2. Would it be a worse outcome to also include the syntactic change of lambda exprs requiring the correct sort of arrow given its effectfulness?

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

view this post on Zulip Anthony Bullard (Apr 08 2025 at 13:43):

Maybe @Anton could move this to a new thread in Ideas, maybe called "Needed Function signature and lambda expr change"?

view this post on Zulip Anthony Bullard (Apr 08 2025 at 13:48):

To TLDR the above, we have three main ways to solve the current contention with function signatures in constructs that are comma separated:

  1. Use backtracking (note that we have basically zero backtracking today)
  2. Don't use backtracking, always require parens around function signatures (a syntactic change, and inconsistent)
  3. Change function arg syntax in type annotations to be bounded by || (has problems discussed before, with the following potentional solutions):
    a. Do that and leave lambda exprs alone
    b. Do that and make all lambda exprs require an arrow after the args that matches the function's effectufulness.
    c. We could instead use some other syntax to mark the start of function args like \

view this post on Zulip Anthony Bullard (Apr 08 2025 at 13:50):

I'm going to pause the completion of the Parser until we come to a resolution of this topic.

view this post on Zulip Notification Bot (Apr 08 2025 at 14:00):

9 messages were moved here from #compiler development > zig compiler - parser by Anton.

view this post on Zulip Anthony Bullard (Apr 08 2025 at 14:00):

Thank you @Anton !

view this post on Zulip Joshua Warner (Apr 08 2025 at 15:12):

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.

view this post on Zulip Anthony Bullard (Apr 08 2025 at 15:34):

But that is still a form of backtracking

view this post on Zulip Anthony Bullard (Apr 08 2025 at 15:34):

It would be unfortunate to have to resort to that for this one construct

view this post on Zulip Anthony Bullard (Apr 08 2025 at 15:35):

I think any of the options under #3 above would be a better state

view this post on Zulip Brendan Hansknecht (Apr 08 2025 at 16:24):

What about just require wrapping the whole function type annotation in parens in these cases?

view this post on Zulip Brendan Hansknecht (Apr 08 2025 at 16:25):

That seems the default for these kinds of ambiguities

view this post on Zulip Anthony Bullard (Apr 08 2025 at 20:53):

That is option 2, but I think it will be seen as surprising

view this post on Zulip Richard Feldman (Apr 08 2025 at 21:45):

I definitely don't think we should wrap the whole function type annotation in either parens or pipes

view this post on Zulip Richard Feldman (Apr 08 2025 at 21:46):

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:

view this post on Zulip Anthony Bullard (Apr 08 2025 at 22:33):

No, it’s tuple and record annotations, and other places listed above

view this post on Zulip Anthony Bullard (Apr 08 2025 at 22:33):

Option 3 is to surround the function ARGS in pipes

view this post on Zulip Richard Feldman (Apr 08 2025 at 23:09):

that also doesn't sound good :sweat_smile:

view this post on Zulip Richard Feldman (Apr 08 2025 at 23:11):

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

view this post on Zulip Anthony Bullard (Apr 08 2025 at 23:11):

That’s fine, but I wrote it up above and it looks fine to me. But it’s your language

view this post on Zulip Richard Feldman (Apr 08 2025 at 23:11):

the problem with pipes around args is when the args are on multiple lines

view this post on Zulip Anthony Bullard (Apr 08 2025 at 23:11):

Ok, it’s just at a fundamental tension with the entire design of the parser - but I’ll find a way to

view this post on Zulip Richard Feldman (Apr 08 2025 at 23:12):

looks fine single-line, doesn't look fine with multiline unfortunately :sweat_smile:

view this post on Zulip Anthony Bullard (Apr 09 2025 at 14:10):

Found the fix, and I didn't have to do any backtracking, just 2 token lookahead:

https://github.com/roc-lang/roc/pull/7733

view this post on Zulip Anthony Bullard (Apr 09 2025 at 14:12):

All headers are now able to be parsed and formatted as per what you see in the tests

view this post on Zulip Anthony Bullard (Apr 09 2025 at 14:12):

I just remembered I haven't implemented where clauses yet, so that will be next.

view this post on Zulip Anthony Bullard (Apr 09 2025 at 14:13):

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

view this post on Zulip Anthony Bullard (Apr 09 2025 at 14:14):

Just as long as you don't use where. :-)

view this post on Zulip Niclas Ahden (Apr 09 2025 at 14:14):

Thank you for your tireless efforts Anthony, this is so exciting! :smiley:

view this post on Zulip Anthony Bullard (Apr 09 2025 at 14:15):

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.

view this post on Zulip Anthony Bullard (Apr 09 2025 at 14:15):

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:

view this post on Zulip Anthony Bullard (Apr 09 2025 at 14:16):

And thanks to @Richard Feldman for pushing back and getting me to persevere here

view this post on Zulip Richard Feldman (Apr 09 2025 at 17:51):

yoooooo, amazing work @Anthony Bullard!!! :heart_eyes:


Last updated: Jun 16 2026 at 16:19 UTC