Stream: beginners

Topic: Compiler says optional record fields are missing


view this post on Zulip Ian McLerran (Jan 24 2024 at 18:31):

I have had a bit of trouble with optional record fields: Frequently it seems that when I try to leave optional fields out of a record passed to a function whose argument is a record with optional fields, the compiler complains that the optional fields are missing.

For example:

90│  expect numDaysSinceEpoch {year: 2024} == 19723
                              ^^^^^^^^^^^^
The argument is a record of type:
    { … }

But numDaysSinceEpoch needs its 1st argument to be:
    {
        day : Int Unsigned64,
        month : Int Unsigned64,
        …
    }
Tip: Looks like the day and month fields are missing.

My code looks like:

numDaysSinceEpoch: {year: U64, month? U64, day? U64} -> U64
numDaysSinceEpoch = \{year, month? 1, day? 1} ->
    numLeapYears = numLeapYearsSinceEpoch year ExcludeCurrent
    daysInYears = numLeapYears * 366 + (year - epochYear - numLeapYears) * 365
    isLeap = isLeapYear year
    daysInMonths = List.sum (
        List.map (List.range { start: At 1, end: Before month })
        (\mapMonth ->
            unwrap (monthDays {month: mapMonth, isLeap}) "numDaysSinceEpochToYMD: Invalid month"
        ),
    )
    daysInYears + daysInMonths + day - 1

expect numDaysSinceEpoch {year: 2024} == 19723  # line 90

What gives here? Is my syntax incorrect?

view this post on Zulip Brendan Hansknecht (Jan 24 2024 at 18:44):

So I think we have a bug in specialization or something of that nature.

view this post on Zulip Brendan Hansknecht (Jan 24 2024 at 18:44):

As in I feel like I have seen this before but never minimized

view this post on Zulip Brendan Hansknecht (Jan 24 2024 at 18:45):

With optional record fields, we seem to only generate one version of a function. It is based on the first way that the function is called.

view this post on Zulip Brendan Hansknecht (Jan 24 2024 at 18:45):

Do you also use that function as {year, month, day}, if you comment that call out does that fix things?

view this post on Zulip Brendan Hansknecht (Jan 24 2024 at 18:46):

I need to mess around with this at some point, but I think there should be a really small reproducer.

view this post on Zulip Brendan Hansknecht (Jan 24 2024 at 18:46):

But yeah, a bug.

view this post on Zulip Ian McLerran (Jan 24 2024 at 18:47):

It does not fix things actually. If I comment it out the compiler panics:

thread 'main' panicked at 'Error in alias analysis: error in module ModName("UserApp"), function definition FuncName("\x11\x00\x00\x00\x02\x00\x00\x00\xcbr?\x05\x92\xae\x19\x92"), definition of value binding ValueId(3): expected type '(((), (), ()),)', found type '((),)'', crates/compiler/gen_llvm/src/llvm/build.rs:5761:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

view this post on Zulip Ian McLerran (Jan 24 2024 at 18:51):

I am going to find a work around in code, but have branched my code here to make it available for anyone trying to resolve the issue (once I get it filed).

view this post on Zulip Brendan Hansknecht (Jan 24 2024 at 18:52):

I'll try to figure out a minimal repro for the feature later today and file a bug.

view this post on Zulip Ian McLerran (Jan 24 2024 at 18:55):

Btw, the compiler panic can be resolved by changing:

numDaysSinceEpochToYear = \year ->
    numDaysSinceEpoch {year}

to:

numDaysSinceEpochToYear = \year ->
    numDaysSinceEpoch {year, month: 1, day: 1}

view this post on Zulip Brendan Hansknecht (Jan 24 2024 at 19:01):

min repro was easy to make and is here: https://github.com/roc-lang/roc/issues/6423

view this post on Zulip Elias Mulhall (Jan 30 2024 at 16:35):

Here's a previous thread about this bug: https://roc.zulipchat.com/#narrow/stream/231634-beginners/topic/open.2Fclosed.20record.20type.20inferance

@Ian McLerran this bug has to do with type inference for closed record types. Open records should type-check correctly, so one workaround is to add a * to the record type to make it open

numDaysSinceEpoch: {year: U64, month? U64, day? U64}* -> U64

view this post on Zulip Ian McLerran (Jan 30 2024 at 17:40):

Thanks @Elias Mulhall, that's good information to have. Moving to open record types is a great fix until the compiler bug is resolved. No reason not to allow open records here.

view this post on Zulip Elias Mulhall (Jan 30 2024 at 17:43):

There's definitely a trade-off -- with an open record you won't get an error message for numDaysSinceEpoch {year: 1990, mnth: 5 }. But that's better than the bug :+1:

view this post on Zulip Ian McLerran (Jan 30 2024 at 18:02):

Ah yeah, great point. Maybe I will stick to passing in the optional fields, and leave the API the same to keep the sound error checking.

view this post on Zulip Johannes Maas (Feb 24 2024 at 13:52):

I'm running into this issue, too, and I don't understand how to proceed.

This is the repro code:

app "reproduction"
    packages {
        pf: "https://github.com/roc-lang/basic-cli/releases/download/0.8.1/x8URkvfyi9I0QhmVG98roKBUs_AZRkLFwFJVJ3942YA.tar.br",
    }
    imports [pf.Stdout, pf.Task.{ Task }]
    provides [main] to pf

Xml : {
    xmlDeclaration ? Str,
    root : Str,
}

main : Task {} I32
main =
    root = "root"
    xmlDeclarationParserResult = Ok "xmlDeclaration"

    xml : Xml
    xml =
        when xmlDeclarationParserResult is
            Ok xmlDeclaration ->
                { xmlDeclaration, root }

            _ ->
                { root }

    Stdout.line "Testing"

I get this error:

jojo@Windows-PC:~/code/feed-reader/backend$ roc check Repro.roc

── TYPE MISMATCH in Repro.roc ──────────────────────────────────────────────────

Something is off with the 2nd branch of this when expression:

18│       xml : Xml
19│       xml =
20│>          when xmlDeclarationParserResult is
21│>              Ok xmlDeclaration ->
22│>                  { xmlDeclaration, root }
23│>
24│>              _ ->
25│>                  { root }

The 2nd branch is a record of type:

    { … }

But the type annotation on xml says it should be:

    { xmlDeclaration : Str, … }

Tip: Looks like the xmlDeclaration field is missing.

────────────────────────────────────────────────────────────────────────────────

1 error and 1 warning found in 37 ms

view this post on Zulip Anton (Feb 24 2024 at 14:42):

This is a possible workaround:

Xml : {
    declaration : [Some Str, None],
    root : Str,
}

main : Task {} I32
main =
    root = "root"
    xmlDeclarationParserResult = Ok "xmlDeclaration"

    xml : Xml
    xml =
        when xmlDeclarationParserResult is
            Ok xmlDeclaration ->
                { declaration: Some xmlDeclaration, root }

            _ ->
                { declaration: None, root }

    Stdout.line "Testing"

view this post on Zulip Elias Mulhall (Feb 24 2024 at 16:34):

Because this is a bug in closed record types, one solution is to make the record open. The way to do that is a bit goofy

Xml a : { ... }a

xml : Xml *

view this post on Zulip Anton (Feb 24 2024 at 16:52):

Hah, I tried to do the same thing but got stuck, I didn't have a space between Xml and *

view this post on Zulip Johannes Maas (Feb 24 2024 at 23:00):

I'm guessing I was reaching for the optional field where I shouldn't have been. I think Anton's proposal with the tag union is the better way to represent the optionality of the declaration.

view this post on Zulip Johannes Maas (Feb 24 2024 at 23:02):

I'm coming to this conclusion after rereading the section about optional fields in the Roc for Elm programmers guide. It mentions that optional fields are specifically for config options passend into functions whereas this is a data type with an optional field.


Last updated: Jul 06 2025 at 12:14 UTC