Not sure if this is a known bug, but a mismatch between the naming of a record field in its type annotation and its definition / usage will produce a compiler panic:
IE:
month_days : { month : Int *, isLeap ? Bool } -> U64
month_days = \{ month, is_leap ? Bool.false } ->
when month is
1 | 3 | 5 | 7 | 8 | 10 | 12 -> 31
4 | 6 | 9 | 11 -> 30
2 if is_leap -> 29
2 -> 28
_ -> 0
produces:
thread 'main' panicked at crates/compiler/gen_llvm/src/llvm/build.rs:5582:19:
Error in alias analysis: error in module ModName("UserApp"), function definition FuncName("\x12\x00\x00\x00\r\x00\x00\x00r,\xe4idl\x95\x92"), definition of value binding ValueId(3): tuple field index 2 out of range
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
I haven't seen that cause an error like this before, can you make an issue?
Yeah... although in trying to find a min repro, its a little more complex. Trying to identify the exact case that produces this in a min repro
Okay, this one is actually VERY specific, but I think I've narrowed down to a fairly minimum repro.
(Wasn't quite as specific as I thought for a minute...)
Conditions I have been able to establish:
U16
/I16
or larger, Str
, or Bool
, maybe other types (Specifically does not apply to optional U8
/I8
types).Min repro:
main! = \_ ->
_ = foo { bar: 1, baz: 1 }
Ok {}
foo : { qux ? U16, baz : U8 } -> U8
foo = \{ bar ? 0, baz } ->
if bar == 0 then
0
else
baz
Note that I also tried reversing the U16
and U8
types so the optional is U8
and the non-optional is U16
, but this does not trigger the error, so it does not appear to be based on the total size of the record.
Filed an issue @ #7478
Gotta be something weird with alignment and layout
In general, I think optional record fields are more a sharp edge than valuable in current roc.
That feels more like a current problem with a fragile implementation than the eventual state of the feature, right?
Or do you kinda feel like we should get rid of them
They're great for UI building
I think optional fields are a _real difference_ in Roc as a feature
For UI specifically, the amount of noise you can avoid is insanely large
It makes Weaver much cleaner
Brendan Hansknecht said:
In general, I think optional record fields are more a sharp edge than valuable in current roc.
:thinking: I wonder if static dispatch could make "builder" APIs comparably nice for specifying config
If I ever get around to wrapping Clay in Roc, it allows for really nice API (like it has in C, but without dealing with the C preprocessor)
Ooh it would definitely work for Weaver
There is an inherent cost to builder APIs, especially for things like immediate mode rendering systems
Which would be a "zero-cost abstraction" with pure expression compile-time eval
You assume that most of this configuration is static
And that is not the truth a lot of the time
button(Config.default().border(green).enabled(foo))
I know we haven't decided for sure whether we want to do this, but you could eliminate the default config by taking a function that operates on it:
button(.border(green).enabled(foo))
Yeah, we'd need LLVM to pull in clutch 100% of the time on top of constant folding
if the config isn't static then we're doing runtime work anyway, probably makes no difference
also I wouldn't be surprised if llvm would already optimize this away
I think the real question is how it reads
especially if you have a lot of them in a DSL :big_smile:
Richard Feldman said:
I know we haven't decided for sure whether we want to do this, but you could eliminate the default config by taking a function that operates on it:
button(.border(green).enabled(foo))
I actually really like how that reads!
it's about as concise as default record fields, but without the separate language feature
I wonder how we can make a default button nice without needing a separate default_button()
, since you need to do button(|c| c)
here
You could make .
the identity function
button(.)
I'm telling you if we get rid of optional fields, with PNC we will absolutely end up with labelled/named arguments
I don't have perms to move everything from
on into a different channel. Can someone do that?Might as well start bikeshedding the syntax now
I think labelled args are great
If you don't use records
I worked on the Flutter team at Google - I like labelled args :-)
But we have records already
BRO
So jealous
I wasn't on the Glamorous side
So not jealous
I was _mostly_ diving into the Dart compiler and internal tooling/infra
Anthony Bullard said:
I'm telling you if we get rid of optional fields, with PNC we will absolutely end up with labelled/named arguments
labeled arguments are a different thing from optional arguments. We already have records serving the purpose of labeling!
Sam Mohr said:
button(.)
This aligns with the .method()
syntax for desugaring to |x| x.method()
, so .
goes to |x| x
. Literally trimmed off the end.
If we do something like _.method()
goes to |x| x.method()
instead, then maybe _.
or just _
can be the identity function
True-ish. But in a language like dart optional args are almost always labelled.
Sam Mohr said:
button(.)
What if the function arg was....optional?
We've come full circle
Richard Feldman said:
Anthony Bullard said:
I'm telling you if we get rid of optional fields, with PNC we will absolutely end up with labelled/named arguments
labeled arguments are a different thing from optional arguments. We already have records serving the purpose of labeling!
I think there are a lot of reasons for labelling args. One can be the Swift/ObjC/SmallTalk approach where the set of labelled args are part of the function "name". One can be for clarity at the call site. And another is to allow arguments to be reordered and left out at the call site based on usage.
And to be clear - I'm not saying labelled arguments are right for Roc.
Anthony Bullard said:
Sam Mohr said:
button(.)
What if the function arg was....optional?
optional args are a deep topic. I'm happy to talk about it but I've thought about it a lot over the years and I keep coming back to the conclusion that they sound like a good idea but actually just lead to both worse APIs and also a worse experience for API designers
I think that's true - but I think either optional args or optional record fields are needed in the language
Or most UI libraries will be very verbose
But I could be wrong
I'm not sure what use case they are needed for that terse builders can't handle
I know flutter couldn't work (in it's current design) without them
Okay, we can write up an experiment
Sam Mohr said:
terse builders
This sounds like magic
What Richard is describing is what I'm referring to
It seems almost as terse as optional record fields
But we _could_ maybe find a way to have a UI library that has functions for elements that all return a root custom type that has all the config functions defined in the module, and then you can config that way
Like SwiftUI
button("Some text")
.border(Colors.green)
.enabled(foo)
So the "main business" of an element/component happens in it's args. Anything that would wrap it or change some config of the root type would be a SD function
Hmmmm, that works
great point!
I know Flutter has talked about experimenting with a similar design (to compete with SwiftUI I imagine)
Anyway, it would be fun to experiment with this for my hypothetical Clay wrapper
Someone with perms should really clean up this thread :-)
Sorry @Ian McLerran !
@Sam Mohr I'd be curious what weaver would look like with this config idea
I'll draft something later today, probably tonight
Weaver is a great candidate, actually! It could help clean up the number of functions
We don't need Opt.str({ short: "v" })
, we can do opt(.str().short("v"))
or maybe opt().str().short("v")
I'll mess with it
Currently we have 3 times 13 num functions for Opt and Param each
optional args are a deep topic. I'm happy to talk about it but I've thought about it a lot over the years and I keep coming back to the conclusion that they sound like a good idea but actually just lead to both worse APIs and also a worse experience for API designers
I'm using the optional args extensively in https://github.com/lukewilliamboswell/plume
I tried a few different API's, but this just felt the nicest. I'm very interested to know if they is another way!
@Luke Boswell If the Trace
type was a Custom Type and we had static dispatch you could just add a with_orientation
(or just orientation
) function to that module and initialize one with
Trace.new(data)
.orientation(Horizontal)
.name("This is my trace!")
.bar_width(35.5)
And if you don't want to use any of that other stuff it's just
Trace.new(data)
Ohk... yeah they're all custom types. So we can try this out when we have static dispatch.
I'll make a branch of Weaver anyway just to show what the API would look like, but we won't be able to test it, per se
yeah, but it's really useful to know if this looks viable
Agreed
would be a very nice simplification to have static dispatch replacing Abilities, module params, and optional record fields :smiley:
Note, default record fields are by no means fundamentally broken. I would assume the fix isn't even that hard (I just don't know that part of the compiler). Fundamentally, we just need to generate more specializations based on if the default is used or not (that or we could generate it at the call sight. Just fill in the full record).
I feel like default record fields are a lot simpler and require less API overhead, but builders do work. I find builders quite heavy for simple things like preparing an SQlite transaction for example. Default is deferred transactions, but you can explicitly specify other options.
And for a new user I don't want them to think about transaction types at all. So they will just use the default and not even know what it means
I'd still like to explore what APIs would look like. It's not just about the implementation, and how the code looks, but also about whether the language primitive is worth it overall in comparison to how the code would look if we didn't have it (in the static dispatch world)
plus they have been a recurring source of misunderstanding about how they work
another way to say it is: if we already had APIs based around static dispatch, would we accept a proposal to introduce optional record arguments?
My issue with the chaining config function approach is that I usually want an immutable struct for stability across the codebase but it has to be mutable for the construction chain to work. I've seen a two struct workaround where one builds up the config and then whatever is finalizing copies the values over to an immutable struct but it's verbose. Is there a fix for this that I don't know about?
You could use record builders maybe to enforce that using types?
Richard Feldman said:
plus they have been a recurring source of misunderstanding about how they work
I strongly feel most of this is due to naming, poor explanations, and bugs rather than anything innate to the feature. (For example we still regularly call them optional record fields when they are actually default value record fields)
But I totally agree we should explore other features. I think builder style apis can be great. They just suck for anything simple.
Karl said:
My issue with the chaining config function approach is that I usually want an immutable struct for stability across the codebase but it has to be mutable for the construction chain to work. I've seen a two struct workaround where one builds up the config and then whatever is finalizing copies the values over to an immutable struct but it's verbose. Is there a fix for this that I don't know about?
in Roc they'd all be (semantically) immutable, e.g.
button("Done").border(green).enabled(is_enabled)
the types of these functions would be something like:
Elem.button : Str -> Elem
Elem.border : Elem, Color -> Elem
Elem.enabled : Elem, Bool -> Elem
But the Elem that's actually holding all the values would still be mutable, yes?
Or are we producing new immutable Elems that are partially initialized?
Roc doesn't have semantically mutable values :big_smile:
https://www.roc-lang.org/functional#opportunistic-mutation
Here's my experimental results for a Weaver API using terse builders: https://gist.github.com/smores56/dc7b37f73114df11d28cd6a148987dea#file-weaver-builders-roc
app [main!] { cli: platform "<basic-cli>" }
import weaver.Opt
import weaver.Cli
import cli.Stdout
main! = |args|
data =
cli_parser.parse_or_display_message(args)
.on_err!(|message|
Stdout.line!(message)?
Err(Exit(1, "")))
)?
Stdout.line!(
"""
Successfully parsed! Here's what I got:
${data.to_str()}
"""
)?
Ok({})
cli_parser =
{ Cli.weave <-
force: Opt.flag(.short("f").help("Force the task to complete.")),
alpha: Opt.single(
.u64()
.short("a")
.help("Set the alpha level.")
.default_fn(|| 1.left_shift_by(7)),
),
files: Param.list(.str().name("files").help("The rest of the files.")),
}
.finish({
name: "basic",
version: "v0.0.1",
authors: ["Some One <some.one@mail.com>"],
description: "This is a basic example of what you can build with Weaver",
})
.assert_valid()
In the linked gist, I put most of the code we'd need to parse an Opt
. This API just plugs into the existing Weaver codebase, so it's not much of a change for this to work
Some nice things:
To parse a U64 arg, we can shorten the function
|arg|
str = arg.as_str()?
str.to_u64()
to the following with methods and Result.try
u64 : ArgTypeSelector -> ArgValueParser U64
u64 = |ArgTypeSelector.()|
ArgValueParser.({ type_name: "u64", parser: .as_str().try(.to_u64()) })
We can now ensure that either short
or long
must be called, whereas that's not possible to handle nicely in a single record with optional fields
Since { short ?? Str, long ?? Str }
has both as optional to allow either to be passed, but then both could be empty
We can ensure short
or long
are empty by making the builder function passed to Opt.single(...)
go through a few types:
ArgTypeSelector
that has str
or u64
methodsArgValueParser a
, which only has short
and long
methodsOptionConfigParams a
, which can further receive short
, long
, default
, etc.I can't really type check this code, but I'm 99% sure that all of this will work with the planned static dispatch and custom types behavior
Anyway, there might be a slight aesthetic drop? But otherwise this seems like an improved API over optional record fields to me, since you get more opportunity for type safety with basically the same number of characters
The one cost I don't know how to fix is field name punning, but I don't think syntax sugar is important for that
Though it's something we could consider
Do you have to call things in the correct order?
This API requires you to call in certain groups: type, then arg name, then everything else
But you could definitely make it handle arbitrary order
Like
Opt.single(
.u64()
.short("a")
...
Versus
Opt.single(
.short("a")
.u64()
...
I required ordering because I thought it would improve readability
I like it
We could require the type at the end
A weakness of this API stemming from an intentional weakness of the API: I couldn't get just opt(.single().u64())
to work because we'd need to allow representing HKTs. This is now another place where HKTs are useful, but they're still not necessary for a good API
How hard would this to be rework using the SD-style approach I laid out? Where the required args are in the main constructor, and all optionals are set in SD funcs?
Roc-ai is already doing something similar to this for client configuration, albeit without PNC or static dispatch, so not exactly the same…
client = Client.init { apiKey }
|> Client.setUrl url
|> Client.setModel model
|> Client.setProviderOrder providers
|> Client.setTemperature temp
The init function takes a record with 1 required field and 17 optional ones, but then there are 18 corresponding set functions. That makes for a very bloated signature for the init function. At least in such an extreme example (similar to weaver) I think chained function calls really is cleaner than optional record fields.
Additionally, the init function actually has to call calls the setter functions itself for 6 of the params, because the parameter accepted in as an argument is actually a different type (simplified) than what is actually stored, so optional record fields can’t handle this type conversion at all.
So that could be with PNC/SD:
client =
Client.init(apiKey)
.setUrl(url)
.setModel(model)
.setProviderOrder(providers)
.setTemperature(temp)
The big advantage of optional record fields here is performance. In this case you would be avoiding four function calls
Yeah, if you have to create 18 stack frames with 18 copies of client, that’s a lot less optimal than just one…
It depends on inlining
Which I have no idea if/when LLVM would inline these
If they are reliably inlined, there is probably no perf difference with --optimize
Richard Feldman said:
would be a very nice simplification to have static dispatch replacing Abilities, module params, and optional record fields :smiley:
Wait how will/could SD replace module params? Zulip link anyone? :fingers_crossed:
Anthony Bullard said:
If they are reliably inlined, there is probably no perf difference with --optimize
As much as llvm inlines everything, I would expect this to still manifest some copies (especially with more complex types). At the same time, I would be quite surprised if that generally affects perf.
As a simple example, refcounting between copies likely forces llvm to copy.
@Ian McLerran check out Richard's recent Realworld application impl. Having tried to use module params vs. just a record that saves the relevant effectful closures, it seems like the latter with SD is simpler and has fewer sharp corners like testing and all.
Last updated: Jul 06 2025 at 12:14 UTC