-
Notifications
You must be signed in to change notification settings - Fork 98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow custom contracts to customize the label #2176
base: master
Are you sure you want to change the base?
Conversation
|
Branch | thread-label |
Testbed | ubuntu-latest |
Click to view all benchmark results
Benchmark | Latency | microseconds (µs) |
---|---|---|
fibonacci 10 | 📈 view plot 🚷 view threshold | 386.46 µs |
foldl arrays 50 | 📈 view plot 🚷 view threshold | 1,575.60 µs |
foldl arrays 500 | 📈 view plot 🚷 view threshold | 5,778.00 µs |
foldr strings 50 | 📈 view plot 🚷 view threshold | 6,635.90 µs |
foldr strings 500 | 📈 view plot 🚷 view threshold | 57,457.00 µs |
generate normal 250 | 📈 view plot 🚷 view threshold | 45,176.00 µs |
generate normal 50 | 📈 view plot 🚷 view threshold | 1,808.50 µs |
generate normal unchecked 1000 | 📈 view plot 🚷 view threshold | 2,726.40 µs |
generate normal unchecked 200 | 📈 view plot 🚷 view threshold | 641.23 µs |
pidigits 100 | 📈 view plot 🚷 view threshold | 2,736.10 µs |
pipe normal 20 | 📈 view plot 🚷 view threshold | 1,295.60 µs |
pipe normal 200 | 📈 view plot 🚷 view threshold | 8,298.30 µs |
product 30 | 📈 view plot 🚷 view threshold | 744.27 µs |
scalar 10 | 📈 view plot 🚷 view threshold | 1,295.80 µs |
sum 30 | 📈 view plot 🚷 view threshold | 734.96 µs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have the following reservations with the current approach:
- In the example, why is the label explicitly included for the
std.is_string
check but not for thestd.is_array
check? Seems like both should blame the whole contract. If I understand correctly, with the proposed solution, not including a label will always automatically blame the parent contract, which is a strange choice to make, as a child. - I think having to change all leafs contracts (here
Number
,String
, etc.) is costly. Now all leaf contracts need to explicitly propagate their label. It's probably very easy to forget for a custom contract implementer, especially given thatlabel
is optional, so this won't be caught by a runtime error.
From what I recall, contract.apply
takes the following decision: if you call a child contract, then the blame will point to the child's contract argument, basically. Couldn't we make check do something similar? With the following reasoning:
- blaming the child contract is the default behavior you want (and you don't want to choose your behavior as a child in the place of your parent contract). The fact that all standard contracts are changed without distinction to include their label seems to corroborate that this sounds like a default behavior. So maybe we should make
check
just embed the label in the returned error automatically, unless there is one already. - If we go with this, a parent contract can't decide how a child may blame. But I think it makes sense: the child should know better. However, the parent can decide to short-circuit the child contract if it wants to blame something else, which is an exceptional case (you apply a child contract, but you still blame the whole contract).
I think with your example, that would give the same result without having do to anything special: you don't need to include the label when blaming yourself as a whole (it's the default behavior), and check
would automatically propagate the child label. We wouldn't have to change a single custom contract of the stdlib, I think. This would be a change in behavior for existing code, but having thought more about the weekly's discussion 1. it's not a true backward incompatibility, it's just different error messages for contract failures 2. It seems to be the right default choice.
A last topic is documentation. I fear things start to become complicated: we have immediate contracts (predicate and validator), and general custom contracts, that can have an immediate part and a delayed part. At least, up to now, there is a simple conceptual distinction: we use ADTs for direct error reporting, which makes sense (à la Rust, OCaml, Haskell, etc.), and a label object for delayed throwing, which is a different mechanism. Although it's already a bit confusing that a label and the error data of a validator are quite similar; in fact the latter can bee seen as a Nickel projection of a subset of the label. Still, we need to use getters/setters on labels which are opaque, while error data are vanilla record. Well, such is life.
Now, we can embed a label within an ADT for immediate error reporting. This label itself can have notes and error messages inside, which is a bit puzzling. What happens if I do 'Error { message = "hello", label = std.contract.label.with_message "world" label}
? I'm sure we can find a reasonable technical answer to this, but is it something that makes sense and that we should allow? If yes, how are we going to explain that in the docs?
The solution could be as simple as not calling that a label (even if it is under the hood - that's an implementation detail), but something else, like blame_location
(because I believe in most of those cases we don't care about the error message and notes, which should be conveyed by the error data ADT, but rather about the location information in the label). Maybe there are other ideas to better articulate label and error messages that would reduce the friction?
Good points.
An oversight -- you can leave it out and the behavior is the same.
I think that would make sense, but I had some trouble with the implementation. The problem is that in |
Indeed Another solution is to make nickel/core/src/eval/operation.rs Lines 1899 to 1928 in 0733850
We could do the same thing, giving the label and the result to the post-processor (which would basically be what you added to Another possible benefit of doing that in |
@@ -102,8 +102,8 @@ pub mod internals { | |||
|
|||
generate_accessor!(stdlib_contract_equal); | |||
|
|||
generate_accessor!(prepare_custom_contract); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like this doesn't exist anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, it's possible. Probably the first version that was quite inefficient.
Thanks for the hint, I went for pushing an extra op after |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, but it's missing a documentation update. I'm setting "request changes" mostly as a self remainder of running the benchmarks to see if this effects on real use cases.
@@ -1708,7 +1712,8 @@ | |||
'Ok Dyn, | |||
'Error { | |||
message | String | optional, | |||
notes | Array String | optional | |||
notes | Array String | optional, | |||
blame_location | Dyn | optional, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One detail: we don't have to make this optional, because in fact check
ensures that blame_location
is always set. On the other hand, this would make the contract from check
and the return value of custom
to mismatch, which might be annoying for typed code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, the annoyance with typed code was why I left it optional, especially since I'd expect it to be common in custom contracts to just return the result of a std.contract.check
. I think at some point we discussed subtyping for records with optional fields? If we had that, this would be nicer...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In any case, let's leave it optional 👍 . It's not breaking to change an optional field to required in the return type, so we can wait for a better situation around those kind of things.
@@ -102,8 +102,8 @@ pub mod internals { | |||
|
|||
generate_accessor!(stdlib_contract_equal); | |||
|
|||
generate_accessor!(prepare_custom_contract); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, it's possible. Probably the first version that was quite inefficient.
Co-authored-by: Yann Hamdaoui <[email protected]>
Co-authored-by: Yann Hamdaoui <[email protected]>
This adds an additional (optional)
label
field to the functions that define validators and custom contracts. When applying a contract, if that contract returns alabel
along with the error data, the returned label will be blamed instead of the label that was passed to the contract.For example, here is a custom array contract that blames the element location if it's a boolean but blames the array as a whole if it finds a string:
This gives
but