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?
So I think we have a bug in specialization or something of that nature.
As in I feel like I have seen this before but never minimized
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.
Do you also use that function as {year, month, day}
, if you comment that call out does that fix things?
I need to mess around with this at some point, but I think there should be a really small reproducer.
But yeah, a bug.
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
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).
I'll try to figure out a minimal repro for the feature later today and file a bug.
Btw, the compiler panic can be resolved by changing:
numDaysSinceEpochToYear = \year ->
numDaysSinceEpoch {year}
to:
numDaysSinceEpochToYear = \year ->
numDaysSinceEpoch {year, month: 1, day: 1}
min repro was easy to make and is here: https://github.com/roc-lang/roc/issues/6423
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
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.
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:
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.
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
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"
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 *
Hah, I tried to do the same thing but got stuck, I didn't have a space between Xml
and *
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.
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