Stream: bugs

Topic: Confusing error locations when generics are underconstrained


view this post on Zulip Jonathan (Sep 15 2026 at 16:18):

I have the following

leaves_of_dag : List(a), (a -> { id : id, depends_on : List(id) }) -> List(a)
    where [
        id.is_eq : id, id -> Bool,
        id.to_hash : id, Hasher -> Hasher,
    ]
leaves_of_dag = |nodes, info_fn| {
    (names, dependencies) = unzip_with(
        nodes,
        |node| {
            info = info_fn(node)
            (info.id, info.depends_on)
        },
    )
    name_set = names |> Set.from_list
    nodes_depended_on = dependencies.iter().fold(Set.empty(), |s, ds| ds.fold(s, Set.insert))
    leaves = name_set.difference(nodes_depended_on)
    nodes.keep_if(|node| Set.contains(leaves, info_fn(node).id))
}

which is called inside another function with the signature

normalise_stages : List(Stage(id, meta)) -> Try(List(Stage(id, meta)), [CycleOrDangling, MultipleLeaves, EmptyFlow, ..])
    where [id.is_eq : id, id -> Bool]

You'll notice that the caller (normalise_stages) is missing the to_hash constraint on id that leaves_of_dag requires. However, when the error is reported, the location is at the final usage inside leaves_of_dag, despite it (in isolation) requiring id to have to_hash:

This to_hash method is being called on a value whose type doesn't have that method.

nodes.keep_if(|node| Set.contains(leaves, info_fn(node).id))
                     ^^^^^^^^^^^^

The value's type, which does not have a method named to_hash, is:

    id where [id.is_eq : id, id -> Bool]

This can be confusing to debug, especially with more complicated type signatures (game of spot-the-difference) or multiple calls of e.g. leaves_of_dag. I can create an issue if this is something that should be changed?

view this post on Zulip Anton (Sep 15 2026 at 18:13):

Reading it like this, I think I prefer the currently highlighted location. But I definitely believe it can be confusing! I am not sure if it can easily be decided which would be the least confusing location to highlight for this error inside the compiler :thinking:

Anyway, issues for error msg improvement are always welcome, so feel free to still open one.

view this post on Zulip Jonathan (Sep 15 2026 at 23:03):

It's not clear to me why the lowest function in the stack (at least before builtins) is the location that is highlighted. For instance, if I had 3 functions between, the cause of the error would be even further removed.

I understand that if the functions are not annotated manually, the lowest point is the most accurate, as it is the point where the constraint is actually violated. But if I have annotated my function, a violation should be noted where found not below, where the constraint is assumed to hold.

view this post on Zulip Jonathan (Sep 15 2026 at 23:06):

Because I explained that terribly, hopefully this is better:

image.png

I would expect f1(a) to error: whatever a is, it is has not been specialised, so we cannot say a meets a.to_hash. However, the use of f2 inside f1 should not be an error, because the constraints specified by f1 satisfy f2. Furthermore, say there was a Set.insert in f2 that justified the to_hash requirement: there is nothing wrong with f2, rather with f0's use of it, and so f0 is where I would expect the error.

view this post on Zulip Anton (Sep 16 2026 at 11:11):

Yeah, I completely agree with that, feel free to put that in an issue.


Last updated: Sep 24 2026 at 15:59 UTC