Stream: compiler development

Topic: nominal type declaration statements - how to represent?


view this post on Zulip Luke Boswell (Jul 04 2025 at 00:36):

In our CIR we currently have this... which I think isn't quite right.

/// A declaration of a new type - whether an alias or a new nominal nominal type
///
/// Only valid at the top level of a module
s_type_decl: struct {
    header: CIR.TypeHeader.Idx,
    anno: CIR.TypeAnno.Idx,
    where: ?CIR.WhereClause.Span,
    region: Region,
},

In my understanding both Foo : ... and Foo := ... are statements. They are both type declarations. One is an Alias Type delcaration, and the other is a Nominal Type declaration.

I could imagine a future where people default to refer to these as;

  1. Nominal Type Declaration becomes "that's a Type declaration"
  2. Alias Type Declaration becomes "that's a Alias declaration"

I'm wondering, how should we represent the nominal types in our CIR? Currently I think the s_type_decl is being used for Aliases because that is all we have implemented so far. Should we make another separate variant to distinguish between these two, or maybe a flag is_alias or similar in this variant?

view this post on Zulip Richard Feldman (Jul 04 2025 at 00:47):

that's how we do it in the Rust version of the compiler but I'm not thrilled with how it has worked out

view this post on Zulip Richard Feldman (Jul 04 2025 at 00:47):

I'd rather we just had .s_alias_decl and .s_nominal_decl or something like that

view this post on Zulip Jared Ramirez (Jul 05 2025 at 22:49):

This is somewhat related, but can nominal tag union be open/extensible? In particular, are these allowed?

Color := [Red, Green, Blue]*
Color other := [Red, Green, Blue]other

OtherColor : [Purple]
Color := [Red, Green, Blue]OtherColor

view this post on Zulip Luke Boswell (Jul 05 2025 at 23:39):

I didn't think that was permitted

view this post on Zulip Luke Boswell (Jul 05 2025 at 23:39):

Maybe it would be useful though. I really dont know

view this post on Zulip Brendan Hansknecht (Jul 05 2025 at 23:44):

I think this would be allowed:

Color other := [Red, Green, Blue]other

OtherColor : [Purple]

MixedColor : Color(OtherColor)

At least I see no reason it wouldn't be allowed off the top of my head....but I may be missing something

view this post on Zulip Jared Ramirez (Jul 05 2025 at 23:44):

I'm not arguing for or against!

When checking types of nominal tag unions, if the above examples are not allowed, then we can really easily check if a tag (eg Blue or Yellow) is a member of the closed tag set and raise a diagnostic in Can if not. But if the above is allowed, then we have to check the tag membership in type-checking (so we can fully expand any extensible types)

view this post on Zulip Brendan Hansknecht (Jul 05 2025 at 23:45):

This on the other hand:

Color := [Red, Green, Blue]*

Would just be equivalent to:

Color := [Red, Green, Blue]

view this post on Zulip Richard Feldman (Jul 06 2025 at 01:07):

no, nominal tag unions can't be extended

view this post on Zulip Richard Feldman (Jul 06 2025 at 01:07):

this is actually very important!

view this post on Zulip Richard Feldman (Jul 06 2025 at 01:07):

if we allow them to be extensible, then they don't solve https://github.com/roc-lang/rfcs/pull/1 and we have that problem again

view this post on Zulip Jared Ramirez (Jul 06 2025 at 01:11):

Ooo okay, good to know.

view this post on Zulip Richard Feldman (Jul 06 2025 at 01:11):

we could allow some special syntax sugar for them, that's just sort of like "let me copy and paste that for you" e.g.

Color := [Red, Green, Blue, ..OtherColor]

OtherColor : [Purple]

view this post on Zulip Jared Ramirez (Jul 06 2025 at 01:11):

That will make validating nominal tags easier then!

view this post on Zulip Richard Feldman (Jul 06 2025 at 01:11):

but ..a would need to be disallowed

view this post on Zulip Richard Feldman (Jul 06 2025 at 01:12):

extension variables in nominal tag unions must be compile errors

view this post on Zulip Jared Ramirez (Jul 06 2025 at 01:14):

Cool. Can nominal records be extensible?

view this post on Zulip Richard Feldman (Jul 06 2025 at 01:16):

also no

view this post on Zulip Brendan Hansknecht (Jul 06 2025 at 01:59):

How is

OtherColor : [Purple]
Color := [Red, Green, Blue, ..OtherColor]

Different from:

OtherColor : [Purple]
AbstractColor(a) := [Red, Green, Blue, ..a]

Color : AbstractColor(OtherColor)

view this post on Zulip Richard Feldman (Jul 06 2025 at 02:00):

the problem is allowing the type variable:

AbstractColor(a) := [Red, Green, Blue, ..a]

now you can write functions where the type's layout at runtime varies based on the arguments you pass to that function

view this post on Zulip Luke Boswell (Jul 06 2025 at 03:56):

Are we permitting nominal records? I thought the intent was just Tags for now

view this post on Zulip Luke Boswell (Jul 06 2025 at 04:02):

Or maybe that was only specifically recursive types must be tags.

view this post on Zulip Brendan Hansknecht (Jul 06 2025 at 04:51):

Richard Feldman said:

the problem is allowing the type variable:

AbstractColor(a) := [Red, Green, Blue, ..a]

now you can write functions where the type's layout at runtime varies based on the arguments you pass to that function

Ah yeah, I guess it essential makes the extension types not nominal which kinda defeats the whole point of making the type nominal in the first place.

view this post on Zulip Luke Boswell (Jul 08 2025 at 05:58):

Luke Boswell said:

Are we permitting nominal records? I thought the intent was just Tags for now

I'd love to know the answer to this... is this valid 0.1 syntax?

ImaginaryNumber := { real: F64, imag: F64 }

view this post on Zulip Brendan Hansknecht (Jul 08 2025 at 06:05):

Last this was discussed, I think the decision was to start simple and require a wrapper tag:

ImaginaryNumber := [Wrapper { real: F64, imag: F64 }]

Not sure if any opinions have changed though.

view this post on Zulip Brendan Hansknecht (Jul 08 2025 at 06:05):

Definitely an @Richard Feldman question

view this post on Zulip Kiryl Dziamura (Jul 08 2025 at 06:11):

Would be bice to learn why nominal types feature cant be just a "uniqueness" wrapper around any type. Like if there was a special tag Unique so Id : Unique(U64)

view this post on Zulip Luke Boswell (Jul 08 2025 at 06:16):

@Kiryl Dziamura #ideas > custom types @ 💬 It was originally the Custom Types proposal... and we subsequently renamed them to "nominal types" because that was what everyone called them anyway

view this post on Zulip Luke Boswell (Jul 08 2025 at 06:20):

So I think the only reason is just we are starting simple and only implementing Tag unions to start with...

view this post on Zulip Luke Boswell (Jul 08 2025 at 06:21):

There's a huge amount of discussion to recall though, and as I learn more and implement things I keep revisiting these discussions and learning more about them each time.

view this post on Zulip Richard Feldman (Jul 08 2025 at 11:54):

Luke Boswell said:

Luke Boswell said:

Are we permitting nominal records? I thought the intent was just Tags for now

I'd love to know the answer to this... is this valid 0.1 syntax?

ImaginaryNumber := { real: F64, imag: F64 }

we had definitely planned it at some point. I was talking with @Jared Ramirez about implementation challenges but I don't remember where we ended up. :sweat_smile:

view this post on Zulip Luke Boswell (Jul 08 2025 at 11:55):

Yeah, since I wrote that comment, I (re)discovered the Custom-Types proposal and linked that just above.

view this post on Zulip Jared Ramirez (Jul 08 2025 at 13:47):

Richard and I discussed how nominal wrappers for all types (beyond just tag unions) and that convo ended with something similar to how opaque types currently work:

module X exposes
  [ Color(..) // transparent
  , ImaginaryNumber // opaque
  ]

Color := [Red, Green, Blue]
green = Color.Green

ImaginaryNumber := { real: F64, imag: F64 }
number = ImaginaryNumber.{ real = 1, imag = 2 }

Point := ( F64, F64 )
point = Point.(1.0, 2.0)

UserId := Dec
userId = UserId.(384618)

But then, to pull a value out of the nominal type, you would do:

getColor : Color -> [Red, Green, Blue]
getColor = |Color(inner)| inner

view this post on Zulip Jared Ramirez (Jul 08 2025 at 13:50):

Related to nominal records though, what we can't do is access record fields without unwrapping the type:

ImaginaryNumber := { real: F64, imag: F64 }

number = ImaginaryNumber.{ real = 1, imag = 2 }

x = number.real // Can't do

x = match number  is ImaginaryNumber(r) => r.real // Can do

Without introducing something akin to type classes

view this post on Zulip Richard Feldman (Jul 08 2025 at 14:01):

ahh right, that was it

view this post on Zulip Richard Feldman (Jul 08 2025 at 14:01):

accessing fields was the problem

view this post on Zulip Brendan Hansknecht (Jul 08 2025 at 15:11):

Can we just use nominal type static dispatch to make it work....or at least make it mostly work?

view this post on Zulip Brendan Hansknecht (Jul 08 2025 at 15:12):

number.real() calls static dispatch to unwrap the type and get the real field.

view this post on Zulip Brendan Hansknecht (Jul 08 2025 at 15:13):

Clearly not quite as nice

view this post on Zulip Jared Ramirez (Jul 08 2025 at 15:26):

Yeah, you could do

real =  |ImaginaryNumber(inner)| inner.real

// then later

num.real()

view this post on Zulip Jared Ramirez (Jul 08 2025 at 15:27):

or even:

inner =  |ImaginaryNumber(inner)| inner

// then later

num.inner().real

view this post on Zulip Jared Ramirez (Jul 08 2025 at 15:27):

Depends on how much you want to expose of the type's internals i guess

view this post on Zulip Brendan Hansknecht (Jul 08 2025 at 15:30):

I guess we just need an @property decorator... :laughing:

view this post on Zulip Anton (Jul 08 2025 at 15:51):

We need a pitchfork emoji

view this post on Zulip Jared Ramirez (Jul 09 2025 at 22:19):

How should this work with imported transparent nominal types?

// styles/Color.roc
module [
    Color(RGB, RGBA, ...)
]

Color := [
    RGB(U8, U8, U8),
    RGBA(U8, U8, U8, Dec),
    Named(Str),
    Hex(Str),
]
// MyModule.roc

import styles.Color as CC

blue1 : CC.RGB
blue1 = CC.RGB(0,0,255)

// or

blue2 : CC.RGB
blue2 = CC.Color.RGB(0,0,255)

Here, can you access the RGB constructor directly with CC.RGB? Or do you have to do CC.Color.RGB?

view this post on Zulip Richard Feldman (Jul 09 2025 at 22:19):

CC.Color.RGB

view this post on Zulip Richard Feldman (Jul 09 2025 at 22:20):

but if you did import styles.Color as CC exposing [Color] then Color.RGB should work

view this post on Zulip Richard Feldman (Jul 09 2025 at 22:20):

so it's about the type being in scope vs not

view this post on Zulip Brendan Hansknecht (Jul 10 2025 at 03:06):

Anton said:

We need a pitchfork emoji

I got you a few options:

:pitchfork:
:what:
:dead:

And we still have:
:thinkies:
:facepalm:
:rip-intense:


Last updated: Jul 26 2025 at 12:14 UTC