tweag / nickel

Better configuration for less

Home Page:https://nickel-lang.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`{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.