`{a : "str"}` evaluates into a function
simonzkl opened this issue · comments
Describe the bug
It feels like this might be intended behaviour (that I don't understand), but I find it weird that {a : "str"}
evaluates into a function. We ran into this issue while migrating a template from YAML to Nickel, and forgot to swap :
for a =
.
The error we got back was completely incomprehensible:
error: non serializable term
┌─ <stdlib/internals.ncl>:101:15
│
101 │ "$record" = fun field_contracts tail_contract label value =>
│ ╭───────────────^
102 │ │ if %typeof% value == 'Record then
103 │ │ # Returns the sub-record of `left` containing only those fields which are not
104 │ │ # present in `right`. If `left` has a sealed polymorphic tail then it will be
· │
146 │ │ else
147 │ │ %blame% (%label_with_message% "not a record" label),
│ ╰─────────────────────────────────────────────────────────^
│
= Nickel only supports serializing to and from strings, booleans, numbers, enum tags, `null` (depending on the format), as well as records and arrays of serializable values.
= Functions and special values (such as contract labels) aren't serializable.
= If you want serialization to ignore a specific value, please use the `not_exported` metadata.
To Reproduce
echo '{a : "str"}' | nickel export --format yaml
Expected behavior
Probably an error message that "str" is not a type? Or show which term (outside of internals) is not serializable?
Environment
- OS name + version: MacOS 14.2.1 (23C71)
- Version of the code: nickel-lang-cli nickel 1.3.0 (rev cargore)
Additional context
Add any other context about the problem here.
That's a bad error message indeed. Thanks for reporting. For the record, here is what happens: {a : "str"}
is interpreted as a type expression as per the syntax rules of the unified types/terms syntax (a record with only field without definition and :
annotations). Indeed, {a: Number}
is a record type.
When used at run-time, a type evaluates to its contract. Its contract is a function of the form fun label value => ...
(at least once evaluated), and is built from the internals built-in contracts. In practice, this is elaborated to $record ..some_params..
where $record
is a builtin internal function. This has type Label -> Dyn -> Dyn
, and indeed is a contract waiting to be applied, which isn't serializable.
That types are automatically translated to their contract function is dubious and has actually been discussed in #1775. A better behavior would probably to keep it as a Type
node in the AST, which would greatly improve error messages like this one (we could keep transforming them when they're used as a contract somewhere).
Separately, we should probably emit a warning when a value is used as a type/contract but has no chance to ever make sense (currently constants indeed don't have any reasonable meaning, although this could change, where {foo | "bar"}
could mean {foo | MustEquals "bar"}
, although I'm not sure this is a great idea - just mentioning that this could change one day).
Separately, we should probably emit a warning when a value is used as a type/contract but has no chance to ever make sense (currently constants indeed don't have any reasonable meaning, although this could change, where
{foo | "bar"}
could mean{foo | MustEquals "bar"}
, although I'm not sure this is a great idea - just mentioning that this could change one day).
I think if {foo : "bar"}
has no valid use case today, it makes sense to just throw an error. It can always be re-enabled in the future without breaking compatibility. No decision about future use of {foo : "bar"}
has to be made right now.