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?
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.
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.
Because I explained that terribly, hopefully this is better:
![]()
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.
Yeah, I completely agree with that, feel free to put that in an issue.
Last updated: Sep 24 2026 at 15:59 UTC