Stream: compiler development

Topic: Record extension


view this post on Zulip JRI98 (Jul 03 2026 at 13:28):

Hi there. I have been porting some old Roc code I have but I don't seem to be able to get record extension to work. Here is a small example of what I would expect to work, but the error is clearly telling me this is not what I want:

main! = |_| {
    r = { a: 1 }
    _ = { ..r, f: 0 }
    Ok({})
}
➜  roc git:(main) ✗ roc main.roc
    TYPE MISMATCH - This record does not have a f field.

    This is often due to a typo. The most similar fields are:

        - a

    So maybe f should be a?

    Note: You cannot add new fields to a record with the record update syntax.


Found 1 error(s) and 0 warning(s) for .../main.roc.

Is this something that is supposed to be supported?

view this post on Zulip Anton (Jul 03 2026 at 13:34):

Hi @JRI98,
We have this code in all_syntax_test.roc:

record_update_2 : { name : Str, age : I64 } -> { name : Str, age : I64 }
record_update_2 = |person| {
    { ..person, age: 31 }
}

So looks like that failure could be a bug.

view this post on Zulip JRI98 (Jul 03 2026 at 13:40):

But in that case the age field is already part of the type. In my case, I'm trying to create a record from an existing one and add additional fields.

view this post on Zulip JRI98 (Jul 03 2026 at 13:43):

Here is another related case that I'm not understanding:

f : { ..a } -> { f: U64, ..a }
f = |r| {
    { ..r, f: 0 }
}

main! = |_| {
    Ok({})
}
TYPE MISMATCH - The r record does not have a f field.
It is actually a record with no fields.

view this post on Zulip JRI98 (Jul 03 2026 at 13:44):

It could be the case that it doesn't work like I want it to :sweat_smile:

view this post on Zulip Anton (Jul 03 2026 at 14:05):

I think this may be a performance foot gun if people start using it liberally so that could be why it is not supported.

view this post on Zulip Anton (Jul 03 2026 at 14:08):

It could lead to a lot of data needing to be copied to a new place in memory which could become problematic with large records and lists of records.

view this post on Zulip JRI98 (Jul 03 2026 at 14:17):

I understand that. Maybe there is a way to be explicit about wanting that behavior?
Because otherwise I don't see how I can extend a record I don't know the full shape of.

view this post on Zulip Anton (Jul 03 2026 at 14:34):

Perhaps we could make a Record builtin function append_slow?

view this post on Zulip Jared Ramirez (Jul 03 2026 at 14:58):

i’m not sure this is allowed — in the zig type checked we based thing off of elm, and in elm you cannot use record update to add fields. i don’t recall if in the rust compiler we loosened up that restriction

but in either case, we need to make a change:

i’m not sure the pros/cons of allowing it (outside the perf point above).

as a short term fix, you should be able to create a new record manually writing out the fields to copy from the old, then adding the new fields

view this post on Zulip Jared Ramirez (Jul 03 2026 at 14:59):

oh wait, well the first error does say “you cannot add new fields with the record update syntax”, but the second error is confusing

view this post on Zulip Jared Ramirez (Jul 03 2026 at 15:00):

maybe relevant elm topic: https://discourse.elm-lang.org/t/extending-an-existing-record/5330/2

view this post on Zulip JRI98 (Jul 03 2026 at 15:02):

as a short term fix, you should be able to create a new record manually writing out the fields to copy from the old, then adding the new fields

That's the thing, in the second case I don't know which fields there are.

view this post on Zulip JRI98 (Jul 03 2026 at 15:04):

oh wait, well the first error does say “you cannot add new fields with the record update syntax”, but the second error is confusing

Yeah that error message was what led me to open this thread. I figured I was trying to do something that wasn't supported.

view this post on Zulip Norbert Hajagos (Jul 03 2026 at 21:54):

JRI98 said:

as a short term fix, you should be able to create a new record manually writing out the fields to copy from the old, then adding the new fields

That's the thing, in the second case I don't know which fields there are.

Just to make sure, you mean you don't know, but you could know if you were to inspect the code statically, right? So in your example, if you were to call f with a struct, you'd know what fields are in it. I'm asking because while this is doable (i think), javascript-style spread operator is not, since they are working on dynamic collections, whereas the compiler needs to know the size of every struct in Roc at compile time, so it needs to know all the fields that go into a struct.

If my intuition is correct, you cannot even have proper code with an entrypoint (from "main" or an expect) that calls f where you statically don't know what's in the extended struct. I don't know of another language that has this feature for unboxed, C-style structs, which Roc structs are, so I think the need for it arises as the naturally feeling next step after assembling a structurally typed record with this syntax.
Maybe the extension is the problem in the name and it should be more like record composition / field composition / field inference. Or I think it was called record update sytax originally. I don't know whether I've helped with the matter. Getting late and I'm rambling, so I'm going to go now, have a good one!

view this post on Zulip JRI98 (Jul 03 2026 at 22:01):

When I say "I don't know which fields there are" I mean myself in the context of the example above where I want a function to add a field to a generic record. But the compiler could always infer it statically.

view this post on Zulip Richard Feldman (Jul 03 2026 at 22:05):

yeah I need to dig into the perf to figure out if it's actually a footgun, and if so how much :smile:

view this post on Zulip Richard Feldman (Jul 03 2026 at 22:09):

in the original paper that Elm's extensible records are based on, it supported:

view this post on Zulip Richard Feldman (Jul 03 2026 at 22:09):

Evan had all of those features implemented in...I want to say Elm 0.12? But they turned out to be performance footguns and/or more complicated/nonobvious than useful, so he took them out in a later release

view this post on Zulip Richard Feldman (Jul 03 2026 at 22:09):

and that's where we started

view this post on Zulip Richard Feldman (Jul 03 2026 at 22:10):

the copying concern _could_ be a footgun, but I'm not sure if it actually would be in practice depending on how it would be used. Like if we're talking about a small record on the stack, it might end up being zero because LLVM breaks it down into registers anyway

view this post on Zulip Richard Feldman (Jul 03 2026 at 22:11):

also, if you have a bigger record that you're going to be copying it into regardless, and it would just be a convenience for what you'd do anyway, then it's also zero cost

view this post on Zulip Richard Feldman (Jul 03 2026 at 22:11):

the concern is more like "I used this language feature to implement something that wouldn't otherwise be something anyone would try, and the performance is terrible"

view this post on Zulip Richard Feldman (Jul 03 2026 at 22:11):

so the specific thing I'd like to investigate is how this is used in the TypeScript world

view this post on Zulip Richard Feldman (Jul 03 2026 at 22:12):

because "I used this in that ecosystem and I liked it" would be a likely way that something would end up in Roc, except that of course people have higher perf expectations of Roc than TypeScript :smile:

view this post on Zulip Richard Feldman (Jul 03 2026 at 22:13):

a concrete example of something I'd be concerned about would be database queries - are these used for building those up? if so, how does that look? would it be a perf footgun if we did it in Roc? etc.

view this post on Zulip Richard Feldman (Jul 03 2026 at 22:13):

so it's definitely doable on a technical level, but I don't want to add a feature to the language where the langref would be like "hey this exists, and it's sometimes a convenience but other times blows up your performance, so probably best to avoid it"

view this post on Zulip Luke Boswell (Jul 04 2026 at 01:33):

Is this something that a more performant runtime thing like a Dict is more suitable?

view this post on Zulip Luke Boswell (Jul 04 2026 at 01:33):

I know the syntax isn't as ergonomic... but is it somewhat related here?

view this post on Zulip Norbert Hajagos (Jul 04 2026 at 10:19):

I think they have a different goal. You'd use this capability to 'add a field to an arbitrary struct'.
Suppose you're writing an lsp library. You accept a struct from the user which is the capailities of his lang server, that you indted to just serialize and send off. But because your library doesn't support hover, you want to make sure that the client knows this. So you update the capabilities struct's hoverProvider field to be false and leave the rest. You as the library author wouldn't know what's in the struct, it may not even have a hoverProvider field, in which case you didn't update it, you added it in. This way people could use structural records more freely and I'd argue in a way that's very fitting for structurally typed records. If the perf isn't a problem, I'd use this feature for even the most basic things, like 'I have a struct of x,y coordinates. You run them through this function and they become 3d coordinates'. I'd want to write it as

new_coords = {
    ..2d_coord,
    z: 0
}

view this post on Zulip Prokop Randacek (Jul 10 2026 at 06:32):

i skimmed through this and I think the relevant optimization here is scalar replacement of aggregates. The compiler should disassemble all structs stored in local variables into the scalar values they are made up of, then shuffling these scalars becomes a noop and the compiler only actually materializes the full struct once it is passed to a function or written to memory. It never needs to actually shuffle values between structs before that, even if the programmer wrote it like that.


Last updated: Jul 23 2026 at 13:15 UTC